CW-266 verbose access controls for TOTP 2FA (#967)

* chore: Setup

* feat: Verbose controls for TOTP 2FA WIP [skip-ci]

* feat: Implement verbose controls for sends to contact, non contacts and internal wallets

* feat: Implement verbose 2FA control for exchanges to internal wallets [skip-ci]

* Implement verbose controls

* chore: PR cleanup

* fix: Implement fixes and recommendations on verbose controls

* feat: Localization for verbose controls settings

* fix: disable pin when 2fa is not activated

* fix: Naming error

* chore: Reformat code with linelength of 100

* fix: Wallet type page and type bug when creating wallet

* fix: add new values to be stored in local storage to both reload function and import/export functions in back_service.dart

* fix: White spaces with localization files

* fix: Switch observers in modify_2fa page to individual observer

* chore: Switch custom tab widget to reusable SettingsChoicesCell widget

* chore: Remove unneeded argument in create wallet entrypoint

* fix: Switch type for selectedCakePreference when importing preferences from backup file

* fix: Await all values being saved to local storage

---------

Co-authored-by: David Adegoke <blazebrain@Davids-MacBook-Pro.local>
This commit is contained in:
Adegoke David 2023-08-04 14:49:26 +01:00 committed by GitHub
parent e9df03b49b
commit 4120394121
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1974 additions and 832 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@
.buildlog/
.history
.svn/
.fvm/
# IntelliJ related
*.iml

View file

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true

View file

@ -25,6 +25,10 @@ class AuthService with Store {
Routes.setupPin,
Routes.setup_2faPage,
Routes.modify2FAPage,
Routes.newWallet,
Routes.newWalletType,
Routes.addressBookAddContact,
Routes.restoreOptions,
];
final FlutterSecureStorage secureStorage;
@ -81,21 +85,26 @@ class AuthService with Store {
}
Future<void> authenticateAction(BuildContext context,
{Function(bool)? onAuthSuccess, String? route, Object? arguments}) async {
{Function(bool)? onAuthSuccess,
String? route,
Object? arguments,
required bool conditionToDetermineIfToUse2FA}) async {
assert(route != null || onAuthSuccess != null,
'Either route or onAuthSuccess param must be passed.');
if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) {
if (onAuthSuccess != null) {
onAuthSuccess(true);
} else {
Navigator.of(context).pushNamed(
route ?? '',
arguments: arguments,
);
if (!conditionToDetermineIfToUse2FA) {
if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) {
if (onAuthSuccess != null) {
onAuthSuccess(true);
} else {
Navigator.of(context).pushNamed(
route ?? '',
arguments: arguments,
);
}
return;
}
return;
}
}
Navigator.of(context).pushNamed(Routes.auth,
@ -104,7 +113,7 @@ class AuthService with Store {
onAuthSuccess?.call(false);
return;
} else {
if (settingsStore.useTOTP2FA) {
if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) {
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
@ -19,8 +20,8 @@ import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cake_backup/backup.dart' as cake_backup;
class BackupService {
BackupService(this._flutterSecureStorage, this._walletInfoSource,
this._keyService, this._sharedPreferences)
BackupService(
this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences)
: _cipher = Cryptography.instance.chacha20Poly1305Aead(),
_correctWallets = <WalletInfo>[];
@ -67,9 +68,8 @@ class BackupService {
}
@Deprecated('Use v2 instead')
Future<Uint8List> _exportBackupV1(String password,
{String nonce = secrets.backupSalt}) async
=> throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.');
Future<Uint8List> _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async =>
throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.');
Future<Uint8List> _exportBackupV2(String password) async {
final zipEncoder = ZipFileEncoder();
@ -112,8 +112,7 @@ class BackupService {
return await _encryptV2(content, password);
}
Future<void> _importBackupV1(Uint8List data, String password,
{required String nonce}) async {
Future<void> _importBackupV1(Uint8List data, String password, {required String nonce}) async {
final appDir = await getApplicationDocumentsDirectory();
final decryptedData = await _decryptV1(data, password, nonce);
final zip = ZipDecoder().decodeBytes(decryptedData);
@ -161,10 +160,8 @@ class BackupService {
Future<void> _verifyWallets() async {
final walletInfoSource = await _reloadHiveWalletInfoBox();
_correctWallets = walletInfoSource
.values
.where((info) => availableWalletTypes.contains(info.type))
.toList();
_correctWallets =
walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList();
if (_correctWallets.isEmpty) {
throw Exception('Correct wallets not detected');
@ -191,14 +188,12 @@ class BackupService {
return;
}
final data =
json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
final data = json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
String currentWalletName = data[PreferencesKey.currentWalletName] as String;
int currentWalletType = data[PreferencesKey.currentWalletType] as int;
final isCorrentCurrentWallet = _correctWallets
.any((info) => info.name == currentWalletName &&
info.type.index == currentWalletType);
.any((info) => info.name == currentWalletName && info.type.index == currentWalletType);
if (!isCorrentCurrentWallet) {
currentWalletName = _correctWallets.first.name;
@ -212,138 +207,173 @@ class BackupService {
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
final disableBuy = data[PreferencesKey.disableBuyKey] as bool?;
final disableSell = data[PreferencesKey.disableSellKey] as bool?;
final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?;
final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?;
final currentTransactionPriorityKeyLegacy =
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
final allowBiometricalAuthentication =
data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?;
final currentBitcoinElectrumSererId =
data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?;
final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?;
final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?;
final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?;
final currentPinLength = data[PreferencesKey.currentPinLength] as int?;
final currentTheme = data[PreferencesKey.currentTheme] as int?;
final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?;
final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?;
final currentDefaultSettingsMigrationVersion =
data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?;
final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?;
final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?;
final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?;
final shouldRequireTOTP2FAForAccessingWallet =
data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?;
final shouldRequireTOTP2FAForSendsToContact =
data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?;
final shouldRequireTOTP2FAForSendsToNonContact =
data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?;
final shouldRequireTOTP2FAForSendsToInternalWallets =
data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?;
final shouldRequireTOTP2FAForExchangesToInternalWallets =
data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?;
final shouldRequireTOTP2FAForAddingContacts =
data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?;
final shouldRequireTOTP2FAForCreatingNewWallets =
data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?;
final shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?;
await _sharedPreferences.setString(PreferencesKey.currentWalletName,
currentWalletName);
await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
if (currentNodeId != null)
await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey,
currentNodeId);
await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId);
if (currentBalanceDisplayMode != null)
await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey,
currentBalanceDisplayMode);
await _sharedPreferences.setInt(
PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode);
await _sharedPreferences.setInt(PreferencesKey.currentWalletType,
currentWalletType);
await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType);
if (currentFiatCurrency != null)
await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey,
currentFiatCurrency);
await _sharedPreferences.setString(
PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency);
if (shouldSaveRecipientAddress != null)
await _sharedPreferences.setBool(
PreferencesKey.shouldSaveRecipientAddressKey,
shouldSaveRecipientAddress);
PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress);
if (isAppSecure != null)
await _sharedPreferences.setBool(
PreferencesKey.isAppSecureKey,
isAppSecure);
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
if (disableBuy != null)
await _sharedPreferences.setBool(
PreferencesKey.disableBuyKey,
disableBuy);
await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy);
if (disableSell != null)
await _sharedPreferences.setBool(
PreferencesKey.disableSellKey,
disableSell);
await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
if (currentTransactionPriorityKeyLegacy != null)
await _sharedPreferences.setInt(
PreferencesKey.currentTransactionPriorityKeyLegacy,
currentTransactionPriorityKeyLegacy);
PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy);
if (allowBiometricalAuthentication != null)
await _sharedPreferences.setBool(
PreferencesKey.allowBiometricalAuthenticationKey,
allowBiometricalAuthentication);
PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication);
if (currentBitcoinElectrumSererId != null)
await _sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey,
currentBitcoinElectrumSererId);
PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId);
if (currentLanguageCode != null)
await _sharedPreferences.setString(PreferencesKey.currentLanguageCode,
currentLanguageCode);
await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode);
if (displayActionListMode != null)
await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey,
displayActionListMode);
await _sharedPreferences.setInt(
PreferencesKey.displayActionListModeKey, displayActionListMode);
if (fiatApiMode != null)
await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey,
fiatApiMode);
await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
if (currentPinLength != null)
await _sharedPreferences.setInt(PreferencesKey.currentPinLength,
currentPinLength);
await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength);
if (currentTheme != null)
await _sharedPreferences.setInt(
PreferencesKey.currentTheme, currentTheme);
await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme);
if (exchangeStatus != null)
await _sharedPreferences.setInt(
PreferencesKey.exchangeStatusKey, exchangeStatus);
await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus);
if (currentDefaultSettingsMigrationVersion != null)
await _sharedPreferences.setInt(
PreferencesKey.currentDefaultSettingsMigrationVersion,
currentDefaultSettingsMigrationVersion);
await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion,
currentDefaultSettingsMigrationVersion);
if (moneroTransactionPriority != null)
await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority,
moneroTransactionPriority);
await _sharedPreferences.setInt(
PreferencesKey.moneroTransactionPriority, moneroTransactionPriority);
if (bitcoinTransactionPriority != null)
await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority,
bitcoinTransactionPriority);
await _sharedPreferences.setInt(
PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority);
if (selectedCake2FAPreset != null)
await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset);
if (shouldRequireTOTP2FAForAccessingWallet != null)
await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet,
shouldRequireTOTP2FAForAccessingWallet);
if (shouldRequireTOTP2FAForSendsToContact != null)
await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact,
shouldRequireTOTP2FAForSendsToContact);
if (shouldRequireTOTP2FAForSendsToNonContact != null)
await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact,
shouldRequireTOTP2FAForSendsToNonContact);
if (shouldRequireTOTP2FAForSendsToInternalWallets != null)
await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets,
shouldRequireTOTP2FAForSendsToInternalWallets);
if (shouldRequireTOTP2FAForExchangesToInternalWallets != null)
await _sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets,
shouldRequireTOTP2FAForExchangesToInternalWallets);
if (shouldRequireTOTP2FAForAddingContacts != null)
await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts,
shouldRequireTOTP2FAForAddingContacts);
if (shouldRequireTOTP2FAForCreatingNewWallets != null)
await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets,
shouldRequireTOTP2FAForCreatingNewWallets);
if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null)
await _sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
shouldRequireTOTP2FAForAllSecurityAndBackupSettings);
await preferencesFile.delete();
}
Future<void> _importKeychainDumpV1(String password,
{required String nonce,
String keychainSalt = secrets.backupKeychainSalt}) async {
{required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async {
final appDir = await getApplicationDocumentsDirectory();
final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
final decryptedKeychainDumpFileData = await _decryptV1(
keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData))
as Map<String, dynamic>;
final decryptedKeychainDumpFileData =
await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
final keychainJSON =
json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
final keychainWalletsInfo = keychainJSON['wallets'] as List;
final decodedPin = keychainJSON['pin'] as String;
final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final backupPasswordKey =
generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPassword = keychainJSON[backupPasswordKey] as String;
await _flutterSecureStorage.write(
key: backupPasswordKey, value: backupPassword);
await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword);
keychainWalletsInfo.forEach((dynamic rawInfo) async {
final info = rawInfo as Map<String, dynamic>;
await importWalletKeychainInfo(info);
});
await _flutterSecureStorage.write(
key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
keychainDumpFile.deleteSync();
}
@ -352,27 +382,24 @@ class BackupService {
{String keychainSalt = secrets.backupKeychainSalt}) async {
final appDir = await getApplicationDocumentsDirectory();
final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
final decryptedKeychainDumpFileData = await _decryptV2(
keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData))
as Map<String, dynamic>;
final decryptedKeychainDumpFileData =
await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
final keychainJSON =
json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
final keychainWalletsInfo = keychainJSON['wallets'] as List;
final decodedPin = keychainJSON['pin'] as String;
final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final backupPasswordKey =
generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPassword = keychainJSON[backupPasswordKey] as String;
await _flutterSecureStorage.write(
key: backupPasswordKey, value: backupPassword);
await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword);
keychainWalletsInfo.forEach((dynamic rawInfo) async {
final info = rawInfo as Map<String, dynamic>;
await importWalletKeychainInfo(info);
});
await _flutterSecureStorage.write(
key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
keychainDumpFile.deleteSync();
}
@ -386,35 +413,26 @@ class BackupService {
@Deprecated('Use v2 instead')
Future<Uint8List> _exportKeychainDumpV1(String password,
{required String nonce,
String keychainSalt = secrets.backupKeychainSalt}) async
=> throw Exception('Deprecated');
{required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async =>
throw Exception('Deprecated');
Future<Uint8List> _exportKeychainDumpV2(String password,
{String keychainSalt = secrets.backupKeychainSalt}) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final encodedPin = await _flutterSecureStorage.read(key: key);
final decodedPin = decodedPinCode(pin: encodedPin!);
final wallets =
await Future.wait(_walletInfoSource.values.map((walletInfo) async {
final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async {
return {
'name': walletInfo.name,
'type': walletInfo.type.toString(),
'password':
await _keyService.getWalletPassword(walletName: walletInfo.name)
'password': await _keyService.getWalletPassword(walletName: walletInfo.name)
};
}));
final backupPasswordKey =
generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPassword =
await _flutterSecureStorage.read(key: backupPasswordKey);
final data = utf8.encode(json.encode({
'pin': decodedPin,
'wallets': wallets,
backupPasswordKey: backupPassword
}));
final encrypted = await _encryptV2(
Uint8List.fromList(data), '$keychainSalt$password');
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey);
final data = utf8.encode(
json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword}));
final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password');
return encrypted;
}
@ -423,46 +441,57 @@ class BackupService {
final preferences = <String, dynamic>{
PreferencesKey.currentWalletName:
_sharedPreferences.getString(PreferencesKey.currentWalletName),
PreferencesKey.currentNodeIdKey:
_sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences
.getInt(PreferencesKey.currentBalanceDisplayModeKey),
PreferencesKey.currentWalletType:
_sharedPreferences.getInt(PreferencesKey.currentWalletType),
PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
PreferencesKey.currentBalanceDisplayModeKey:
_sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey),
PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType),
PreferencesKey.currentFiatCurrencyKey:
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences
.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
PreferencesKey.disableBuyKey: _sharedPreferences
.getBool(PreferencesKey.disableBuyKey),
PreferencesKey.disableSellKey: _sharedPreferences
.getBool(PreferencesKey.disableSellKey),
PreferencesKey.shouldSaveRecipientAddressKey:
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey),
PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
PreferencesKey.isDarkThemeLegacy:
_sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy),
PreferencesKey.currentPinLength:
_sharedPreferences.getInt(PreferencesKey.currentPinLength),
PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences
.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences
.getBool(PreferencesKey.allowBiometricalAuthenticationKey),
PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences
.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
PreferencesKey.currentTransactionPriorityKeyLegacy:
_sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
PreferencesKey.allowBiometricalAuthenticationKey:
_sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey),
PreferencesKey.currentBitcoinElectrumSererIdKey:
_sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
PreferencesKey.currentLanguageCode:
_sharedPreferences.getString(PreferencesKey.currentLanguageCode),
PreferencesKey.displayActionListModeKey:
_sharedPreferences.getInt(PreferencesKey.displayActionListModeKey),
PreferencesKey.currentTheme:
_sharedPreferences.getInt(PreferencesKey.currentTheme),
PreferencesKey.exchangeStatusKey:
_sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences
.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme),
PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
PreferencesKey.currentDefaultSettingsMigrationVersion:
_sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
PreferencesKey.bitcoinTransactionPriority:
_sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority),
PreferencesKey.moneroTransactionPriority:
_sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority),
PreferencesKey.currentFiatApiModeKey:
_sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
_sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
PreferencesKey.selectedCake2FAPreset:
_sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset),
PreferencesKey.shouldRequireTOTP2FAForAccessingWallet:
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet),
PreferencesKey.shouldRequireTOTP2FAForSendsToContact:
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact),
PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact:
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact),
PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets:
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets),
PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets),
PreferencesKey.shouldRequireTOTP2FAForAddingContacts:
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts),
PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets:
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets),
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
};
return json.encode(preferences);
@ -476,28 +505,23 @@ class BackupService {
}
@Deprecated('Use v2 instead')
Future<Uint8List> _encryptV1(
Uint8List data, String secretKeySource, String nonceBase64) async
=> throw Exception('Deprecated');
Future<Uint8List> _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async =>
throw Exception('Deprecated');
Future<Uint8List> _decryptV1(
Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async {
Future<Uint8List> _decryptV1(Uint8List data, String secretKeySource, String nonceBase64,
{int macLength = 16}) async {
final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));
final secretKey = SecretKey(secretKeyHash.bytes);
final nonce = base64.decode(nonceBase64).toList();
final box = SecretBox(
Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
nonce: nonce,
mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
final plainData = await _cipher.decrypt(box, secretKey: secretKey);
return Uint8List.fromList(plainData);
}
Future<Uint8List> _encryptV2(
Uint8List data, String passphrase) async
=> cake_backup.encrypt(passphrase, data, version: _v2);
Future<Uint8List> _encryptV2(Uint8List data, String passphrase) async =>
cake_backup.encrypt(passphrase, data, version: _v2);
Future<Uint8List> _decryptV2(
Uint8List data, String passphrase) async
=> cake_backup.decrypt(passphrase, data);
Future<Uint8List> _decryptV2(Uint8List data, String passphrase) async =>
cake_backup.decrypt(passphrase, data);
}

View file

@ -247,7 +247,9 @@ Future setup({
nodeSource: _nodeSource,
isBitcoinBuyEnabled: isBitcoinBuyEnabled,
// Enforce darkTheme on platforms other than mobile till the design for other themes is completed
initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme,
initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile
? null
: ThemeList.darkTheme,
);
if (_isSetupFinished) {
@ -389,7 +391,9 @@ Future setup({
final authStore = getIt.get<AuthenticationStore>();
final appStore = getIt.get<AppStore>();
final useTotp = appStore.settingsStore.useTOTP2FA;
if (useTotp) {
final shouldUseTotp2FAToAccessWallets =
appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
if (useTotp && shouldUseTotp2FAToAccessWallets) {
authPageState.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
@ -525,17 +529,22 @@ Future setup({
getIt.get<SendTemplateStore>(),
getIt.get<FiatConversionStore>()));
getIt.registerFactory<SendViewModel>(() => SendViewModel(
getIt.registerFactory<SendViewModel>(
() => SendViewModel(
getIt.get<AppStore>().wallet!,
getIt.get<AppStore>().settingsStore,
getIt.get<SendTemplateViewModel>(),
getIt.get<FiatConversionStore>(),
getIt.get<BalanceViewModel>(),
_transactionDescriptionBox));
getIt.get<ContactListViewModel>(),
_transactionDescriptionBox,
),
);
getIt.registerFactoryParam<SendPage, PaymentRequest?, void>(
(PaymentRequest? initialPaymentRequest, _) => SendPage(
sendViewModel: getIt.get<SendViewModel>(),
authService: getIt.get<AuthService>(),
initialPaymentRequest: initialPaymentRequest,
));
@ -570,8 +579,8 @@ Future setup({
));
getIt.registerFactoryParam<WalletEditViewModel, WalletListViewModel, void>(
(WalletListViewModel walletListViewModel, _) => WalletEditViewModel(
walletListViewModel, getIt.get<WalletLoadingService>()));
(WalletListViewModel walletListViewModel, _) =>
WalletEditViewModel(walletListViewModel, getIt.get<WalletLoadingService>()));
getIt.registerFactoryParam<WalletEditPage, List<dynamic>, void>((args, _) {
final walletListViewModel = args.first as WalletListViewModel;
@ -583,7 +592,6 @@ Future setup({
editingWallet: editingWallet);
});
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet!;
@ -654,10 +662,11 @@ Future setup({
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) => ContactListViewModel(_contactSource, _walletInfoSource, cur));
(CryptoCurrency? cur, _) =>
ContactListViewModel(_contactSource, _walletInfoSource, cur, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) => ContactListPage(getIt.get<ContactListViewModel>(param1: cur)));
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) =>
ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>()));
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
@ -702,13 +711,13 @@ Future setup({
));
getIt.registerFactory(() => ExchangeViewModel(
getIt.get<AppStore>().wallet!,
_tradesSource,
getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(),
getIt.get<AppStore>().settingsStore,
getIt.get<SharedPreferences>(),
));
getIt.get<AppStore>().wallet!,
_tradesSource,
getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(),
getIt.get<AppStore>().settingsStore,
getIt.get<SharedPreferences>(),
getIt.get<ContactListViewModel>()));
getIt.registerFactory(() => ExchangeTradeViewModel(
wallet: getIt.get<AppStore>().wallet!,
@ -716,7 +725,8 @@ Future setup({
tradesStore: getIt.get<TradesStore>(),
sendViewModel: getIt.get<SendViewModel>()));
getIt.registerFactory(() => ExchangePage(getIt.get<ExchangeViewModel>()));
getIt.registerFactory(
() => ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>()));
getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get<TradesStore>()));
@ -890,7 +900,7 @@ Future setup({
getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactory(()=> MarketPlaceViewModel(getIt.get<IoniaService>()));
getIt.registerFactory(() => MarketPlaceViewModel(getIt.get<IoniaService>()));
getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>()));

View file

@ -0,0 +1,35 @@
import 'package:cw_core/enumerable_item.dart';
class Cake2FAPresetsOptions extends EnumerableItem<int> with Serializable<int> {
const Cake2FAPresetsOptions({required String super.title, required int super.raw});
static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0);
static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1);
static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2);
static Cake2FAPresetsOptions deserialize({required int raw}) {
switch (raw) {
case 0:
return Cake2FAPresetsOptions.narrow;
case 1:
return Cake2FAPresetsOptions.normal;
case 2:
return Cake2FAPresetsOptions.aggressive;
default:
throw Exception(
'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize',
);
}
}
}
enum VerboseControlSettings {
accessWallet,
addingContacts,
sendsToContacts,
sendsToNonContacts,
sendsToInternalWallets,
exchangesToInternalWallets,
securityAndBackupSettings,
creatingNewWallets,
}

View file

@ -39,15 +39,31 @@ class PreferencesKey {
static const lastPopupDate = 'last_popup_date';
static const lastAppReviewDate = 'last_app_review_date';
static String moneroWalletUpdateV1Key(String name)
=> '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static String moneroWalletUpdateV1Key(String name) =>
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static const exchangeProvidersSelection = 'exchange-providers-selection';
static const clearnetDonationLink = 'clearnet_donation_link';
static const clearnetDonationLink = 'clearnet_donation_link';
static const onionDonationLink = 'onion_donation_link';
static const lastSeenAppVersion = 'last_seen_app_version';
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
static const shouldShowMarketPlaceInDashboard =
'should_show_marketplace_in_dashboard';
static const isNewInstall = 'is_new_install';
static const shouldRequireTOTP2FAForAccessingWallet =
'should_require_totp_2fa_for_accessing_wallets';
static const shouldRequireTOTP2FAForSendsToContact =
'should_require_totp_2fa_for_sends_to_contact';
static const shouldRequireTOTP2FAForSendsToNonContact =
'should_require_totp_2fa_for_sends_to_non_contact';
static const shouldRequireTOTP2FAForSendsToInternalWallets =
'should_require_totp_2fa_for_sends_to_internal_wallets';
static const shouldRequireTOTP2FAForExchangesToInternalWallets =
'should_require_totp_2fa_for_exchanges_to_internal_wallets';
static const shouldRequireTOTP2FAForAddingContacts =
'should_require_totp_2fa_for_adding_contacts';
static const shouldRequireTOTP2FAForCreatingNewWallets =
'should_require_totp_2fa_for_creating_new_wallets';
static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
'should_require_totp_2fa_for_all_security_and_backup_settings';
static const selectedCake2FAPreset = 'selected_cake_2fa_preset';
}

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/utils/show_bar.dart';
@ -15,9 +16,10 @@ import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart
import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart';
class ContactListPage extends BasePage {
ContactListPage(this.contactListViewModel);
ContactListPage(this.contactListViewModel, this.authService);
final ContactListViewModel contactListViewModel;
final AuthService authService;
@override
String get title => S.current.address_book;
@ -26,95 +28,99 @@ class ContactListPage extends BasePage {
Widget? trailing(BuildContext context) {
return MergeSemantics(
child: Container(
width: 32.0,
height: 32.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context)
.accentTextTheme!
.bodySmall!
.color!),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Icon(Icons.add,
color: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
size: 22.0),
ButtonTheme(
minWidth: 32.0,
height: 32.0,
child: Semantics(
label: S.of(context).add,
child: TextButton(
// FIX-ME: Style
//shape: CircleBorder(),
onPressed: () async {
await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact);
},
child: Offstage()),
),
)
],
)),
width: 32.0,
height: 32.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).accentTextTheme!.bodySmall!.color!),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Icon(
Icons.add,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
size: 22.0,
),
ButtonTheme(
minWidth: 32.0,
height: 32.0,
child: TextButton(
// FIX-ME: Style
//shape: CircleBorder(),
onPressed: () async {
if (contactListViewModel
.shouldRequireTOTP2FAForAddingContacts) {
authService.authenticateAction(
context,
route: Routes.addressBookAddContact,
conditionToDetermineIfToUse2FA: contactListViewModel
.shouldRequireTOTP2FAForAddingContacts,
);
} else {
await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact);
}
},
child: Offstage()),
)
],
),
),
);
}
@override
Widget body(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 20.0, bottom: 20.0),
child: Observer(
builder: (_) {
child: Observer(builder: (_) {
final contacts = contactListViewModel.contactsToShow;
final walletContacts = contactListViewModel.walletContactsToShow;
return CollapsibleSectionList(
context: context,
sectionCount: 2,
themeColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
dividerThemeColor:
Theme.of(context).primaryTextTheme!.bodySmall!.decorationColor!,
sectionTitleBuilder: (_, int sectionIndex) {
var title = S.current.contact_list_contacts;
context: context,
sectionCount: 2,
themeColor: Theme.of(context).primaryTextTheme.titleLarge!.color!,
dividerThemeColor:
Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!,
sectionTitleBuilder: (_, int sectionIndex) {
var title = S.current.contact_list_contacts;
if (sectionIndex == 0) {
title = S.current.contact_list_wallets;
}
if (sectionIndex == 0) {
title = S.current.contact_list_wallets;
}
return Container(
padding: EdgeInsets.only(bottom: 10),
child: Text(title, style: TextStyle(fontSize: 36)));
},
itemCounter: (int sectionIndex) => sectionIndex == 0
? walletContacts.length
: contacts.length,
itemBuilder: (_, sectionIndex, index) {
if (sectionIndex == 0) {
final walletInfo = walletContacts[index];
return generateRaw(context, walletInfo);
}
return Container(
padding: EdgeInsets.only(bottom: 10),
child: Text(title, style: TextStyle(fontSize: 36)));
},
itemCounter: (int sectionIndex) =>
sectionIndex == 0 ? walletContacts.length : contacts.length,
itemBuilder: (_, sectionIndex, index) {
if (sectionIndex == 0) {
final walletInfo = walletContacts[index];
return generateRaw(context, walletInfo);
}
final contact = contacts[index];
final content = generateRaw(context, contact);
return contactListViewModel.isEditable
? Slidable(
key: Key('${contact.key}'),
endActionPane: _actionPane(context, contact),
child: content,
)
: content;
},
);})
);
final contact = contacts[index];
final content = generateRaw(context, contact);
return contactListViewModel.isEditable
? Slidable(
key: Key('${contact.key}'),
endActionPane: _actionPane(context, contact),
child: content,
)
: content;
},
);
}));
}
Widget generateRaw(BuildContext context, ContactBase contact) {
final image = contact.type.iconPath;
final currencyIcon = image != null ? Image.asset(image, height: 24, width: 24)
final currencyIcon = image != null
? Image.asset(image, height: 24, width: 24)
: const SizedBox(height: 24, width: 24);
return GestureDetector(
onTap: () async {
if (!contactListViewModel.isEditable) {
@ -128,30 +134,28 @@ class ContactListPage extends BasePage {
if (isCopied) {
await Clipboard.setData(ClipboardData(text: contact.address));
await showBar<void>(context, S.of(context).copied_to_clipboard);
}
},
child: Container(
color: Colors.transparent,
padding:
const EdgeInsets.only(top: 16, bottom: 16, right: 24),
padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
currencyIcon,
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 12),
child: Text(
contact.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).primaryTextTheme!.titleLarge!.color!),
child: Padding(
padding: EdgeInsets.only(left: 12),
child: Text(
contact.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
),
)
)
),
))
],
),
),
@ -160,60 +164,61 @@ class ContactListPage extends BasePage {
Future<bool> showAlertDialog(BuildContext context) async {
return await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).address_remove_contact,
alertContent: S.of(context).address_remove_content,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ?? false;
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).address_remove_contact,
alertContent: S.of(context).address_remove_content,
rightButtonText: S.of(context).remove,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ??
false;
}
Future<bool> showNameAndAddressDialog(
BuildContext context, String name, String address) async {
return await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: name,
alertContent: address,
rightButtonText: S.of(context).copy,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ?? false;
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: name,
alertContent: address,
rightButtonText: S.of(context).copy,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ??
false;
}
ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.4,
children: [
SlidableAction(
onPressed: (_) async => await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact,
arguments: contact),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.edit,
label: S.of(context).edit,
),
SlidableAction(
onPressed: (_) async {
final isDelete =
await showAlertDialog(context);
ActionPane _actionPane(BuildContext context, ContactRecord contact) =>
ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.4,
children: [
SlidableAction(
onPressed: (_) async => await Navigator.of(context)
.pushNamed(Routes.addressBookAddContact, arguments: contact),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.edit,
label: S.of(context).edit,
),
SlidableAction(
onPressed: (_) async {
final isDelete = await showAlertDialog(context);
if (isDelete) {
await contactListViewModel.delete(contact);
}
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
label: S.of(context).delete,
),
],
);
if (isDelete) {
await contactListViewModel.delete(contact);
}
},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
label: S.of(context).delete,
),
],
);
}

View file

@ -156,15 +156,29 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
} catch (e) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
});
},
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
);
}
void _navigateToCreateWallet() {
if (isSingleCoin) {
Navigator.of(context)
.pushNamed(Routes.newWallet, arguments: widget.walletListViewModel.currentWalletType);
widget._authService.authenticateAction(
context,
route: Routes.newWallet,
arguments: widget.walletListViewModel.currentWalletType,
conditionToDetermineIfToUse2FA: widget
.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
Navigator.of(context).pushNamed(Routes.newWalletType);
widget._authService.authenticateAction(
context,
route: Routes.newWalletType,
conditionToDetermineIfToUse2FA: widget
.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
}
}

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
@ -37,7 +38,7 @@ import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
class ExchangePage extends BasePage {
ExchangePage(this.exchangeViewModel) {
ExchangePage(this.exchangeViewModel, this.authService) {
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
: null;
@ -47,6 +48,7 @@ class ExchangePage extends BasePage {
}
final ExchangeViewModel exchangeViewModel;
final AuthService authService;
final depositKey = GlobalKey<ExchangeCardState>();
final receiveKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
@ -89,16 +91,17 @@ class ExchangePage extends BasePage {
@override
Widget middle(BuildContext context) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(right:6.0),
child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),)
),
PresentProviderPicker(exchangeViewModel: exchangeViewModel)
],
);
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(right: 6.0),
child: Observer(
builder: (_) =>
SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),
)),
PresentProviderPicker(exchangeViewModel: exchangeViewModel)
],
);
@override
Widget trailing(BuildContext context) => TrailButton(
@ -110,12 +113,13 @@ class ExchangePage extends BasePage {
@override
Widget? leading(BuildContext context) {
final _backButton = Icon(Icons.arrow_back_ios,
final _backButton = Icon(
Icons.arrow_back_ios,
color: titleColor,
size: 16,
);
final _closeButton = currentTheme.type == ThemeType.dark
? closeButtonImageDarkTheme : closeButtonImage;
final _closeButton =
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
bool isMobileView = ResponsiveLayoutUtil.instance.isMobile;
@ -126,13 +130,10 @@ class ExchangePage extends BasePage {
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: !isMobileView
? S.of(context).close
: S.of(context).seed_alert_back,
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith(
(states) => Colors.transparent),
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: !isMobileView ? _closeButton : _backButton,
@ -145,23 +146,19 @@ class ExchangePage extends BasePage {
@override
Widget body(BuildContext context) {
WidgetsBinding.instance
.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor:
Theme.of(context).accentTextTheme!.bodyLarge!.backgroundColor!,
keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _depositAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()]),
focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
KeyboardActionsItem(
focusNode: _receiveAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()])
focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
]),
child: Container(
color: Theme.of(context).colorScheme.background,
@ -169,30 +166,28 @@ class ExchangePage extends BasePage {
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Observer(builder: (_) => Column(
children: <Widget>[
_exchangeCardsSection(context),
Padding(
padding: EdgeInsets.only(top: 12, left: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
StandardCheckbox(
value: exchangeViewModel.isFixedRateMode,
caption: S.of(context).fixed_rate,
onChanged: (value) =>
exchangeViewModel.isFixedRateMode = value,
),
],
)
),
SizedBox(height: 30),
_buildTemplateSection(context)
content: Observer(
builder: (_) => Column(
children: <Widget>[
_exchangeCardsSection(context),
Padding(
padding: EdgeInsets.only(top: 12, left: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
StandardCheckbox(
value: exchangeViewModel.isFixedRateMode,
caption: S.of(context).fixed_rate,
onChanged: (value) => exchangeViewModel.isFixedRateMode = value,
),
],
)),
SizedBox(height: 30),
_buildTemplateSection(context)
],
),
),
bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 15),
@ -210,8 +205,7 @@ class ExchangePage extends BasePage {
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context)
.primaryTextTheme!
.displayLarge!
.primaryTextTheme.displayLarge!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 12),
@ -223,29 +217,34 @@ class ExchangePage extends BasePage {
builder: (_) => LoadingPrimaryButton(
text: S.of(context).exchange,
onPressed: () {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
if ((exchangeViewModel.depositCurrency ==
CryptoCurrency.xmr) &&
(!(exchangeViewModel.status
is SyncedSyncStatus))) {
if (_formKey.currentState != null &&
_formKey.currentState!.validate()) {
if ((exchangeViewModel.depositCurrency == CryptoCurrency.xmr) &&
(!(exchangeViewModel.status is SyncedSyncStatus))) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).exchange,
alertContent: S
.of(context)
.exchange_sync_alert_content,
alertContent: S.of(context).exchange_sync_alert_content,
buttonText: S.of(context).ok,
buttonAction: () =>
Navigator.of(context).pop());
buttonAction: () => Navigator.of(context).pop());
});
} else {
exchangeViewModel.createTrade();
final check = exchangeViewModel.shouldDisplayTOTP();
authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA: check,
onAuthSuccess: (value) {
if (value) {
exchangeViewModel.createTrade();
}
},
);
}
}
},
color: Theme.of(context).accentTextTheme!.bodyLarge!.color!,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
isDisabled: exchangeViewModel.selectedProviders.isEmpty,
isLoading: exchangeViewModel.tradeState is TradeIsCreating)),
@ -264,7 +263,7 @@ class ExchangePage extends BasePage {
child: Observer(
builder: (_) {
final templates = exchangeViewModel.templates;
return Row(
children: <Widget>[
AddTemplateButton(
@ -293,18 +292,15 @@ class ExchangePage extends BasePage {
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).template,
alertContent:
S.of(context).confirm_delete_template,
alertContent: S.of(context).confirm_delete_template,
rightButtonText: S.of(context).delete,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
Navigator.of(dialogContext).pop();
exchangeViewModel.removeTemplate(
template: template);
exchangeViewModel.removeTemplate(template: template);
exchangeViewModel.updateTemplate();
},
actionLeftButton: () =>
Navigator.of(dialogContext).pop());
actionLeftButton: () => Navigator.of(dialogContext).pop());
});
},
);
@ -318,8 +314,8 @@ class ExchangePage extends BasePage {
);
}
void applyTemplate(BuildContext context,
ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
void applyTemplate(
BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
exchangeViewModel.changeDepositCurrency(
currency: CryptoCurrency.fromString(template.depositCurrency));
exchangeViewModel.changeReceiveCurrency(
@ -333,22 +329,19 @@ class ExchangePage extends BasePage {
var domain = template.depositAddress;
var ticker = template.depositCurrency.toLowerCase();
exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, ticker);
exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker);
domain = template.receiveAddress;
ticker = template.receiveCurrency.toLowerCase();
exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, ticker);
exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker);
}
void _setReactions(
BuildContext context, ExchangeViewModel exchangeViewModel) {
void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
if (_isReactionsSet) {
return;
}
if (exchangeViewModel.isLowFee) {
if (exchangeViewModel.isLowFee) {
_showFeeAlert(context);
}
@ -359,42 +352,30 @@ class ExchangePage extends BasePage {
final limitsState = exchangeViewModel.limitsState;
if (limitsState is LimitsLoadedSuccessfully) {
final min = limitsState.limits.min != null
? limitsState.limits.min.toString()
: null;
final max = limitsState.limits.max != null
? limitsState.limits.max.toString()
: null;
final key = exchangeViewModel.isFixedRateMode
? receiveKey
: depositKey;
final min = limitsState.limits.min != null ? limitsState.limits.min.toString() : null;
final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null;
final key = exchangeViewModel.isFixedRateMode ? receiveKey : depositKey;
key.currentState!.changeLimits(min: min, max: max);
}
_onCurrencyChange(
exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
_onCurrencyChange(
exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
_onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
_onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
reaction(
(_) => exchangeViewModel.wallet.name,
(String _) => _onWalletNameChange(
exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
(String _) =>
_onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
reaction(
(_) => exchangeViewModel.wallet.name,
(String _) => _onWalletNameChange(
exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
(String _) =>
_onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
reaction(
(_) => exchangeViewModel.receiveCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, exchangeViewModel, receiveKey));
reaction((_) => exchangeViewModel.receiveCurrency,
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey));
reaction(
(_) => exchangeViewModel.depositCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, exchangeViewModel, depositKey));
reaction((_) => exchangeViewModel.depositCurrency,
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
reaction((_) => exchangeViewModel.depositAmount, (String amount) {
if (depositKey.currentState!.amountController.text != amount) {
@ -408,8 +389,7 @@ class ExchangePage extends BasePage {
}
});
reaction((_) => exchangeViewModel.isDepositAddressEnabled,
(bool isEnabled) {
reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {
depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
@ -425,13 +405,11 @@ class ExchangePage extends BasePage {
}
});
reaction((_) => exchangeViewModel.isReceiveAddressEnabled,
(bool isEnabled) {
reaction((_) => exchangeViewModel.isReceiveAddressEnabled, (bool isEnabled) {
receiveKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
reaction((_) => exchangeViewModel.isReceiveAmountEditable,
(bool isReceiveAmountEditable) {
reaction((_) => exchangeViewModel.isReceiveAmountEditable, (bool isReceiveAmountEditable) {
receiveKey.currentState!.isAmountEditable(isEditable: isReceiveAmountEditable);
});
@ -483,20 +461,20 @@ class ExchangePage extends BasePage {
}
});
depositAddressController.addListener(
() => exchangeViewModel.depositAddress = depositAddressController.text);
depositAddressController
.addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
depositAmountController.addListener(() {
if (depositAmountController.text != exchangeViewModel.depositAmount) {
_depositAmountDebounce.run(() {
_depositAmountDebounce.run(() {
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
exchangeViewModel.isReceiveAmountEntered = false;
});
}
});
receiveAddressController.addListener(
() => exchangeViewModel.receiveAddress = receiveAddressController.text);
receiveAddressController
.addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text);
receiveAmountController.addListener(() {
if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
@ -507,8 +485,7 @@ class ExchangePage extends BasePage {
}
});
reaction((_) => exchangeViewModel.wallet.walletAddresses.address,
(String address) {
reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
depositKey.currentState!.changeAddress(address: address);
}
@ -519,22 +496,18 @@ class ExchangePage extends BasePage {
});
_depositAddressFocus.addListener(() async {
if (!_depositAddressFocus.hasFocus &&
depositAddressController.text.isNotEmpty) {
if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
final domain = depositAddressController.text;
final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, ticker);
exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker);
}
});
_receiveAddressFocus.addListener(() async {
if (!_receiveAddressFocus.hasFocus &&
receiveAddressController.text.isNotEmpty) {
if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) {
final domain = receiveAddressController.text;
final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, ticker);
exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker);
}
});
@ -554,29 +527,26 @@ class ExchangePage extends BasePage {
_isReactionsSet = true;
}
void _onCurrencyChange(CryptoCurrency currency,
ExchangeViewModel exchangeViewModel, GlobalKey<ExchangeCardState> key) {
void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
key.currentState!.changeSelectedCurrency(currency);
key.currentState!.changeWalletName(
isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet
? exchangeViewModel.wallet.walletAddresses.address : '');
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(ExchangeViewModel exchangeViewModel,
CryptoCurrency currency, GlobalKey<ExchangeCardState> key) {
void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
key.currentState!.addressController.text =
exchangeViewModel.wallet.walletAddresses.address;
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
} else if (key.currentState!.addressController.text ==
exchangeViewModel.wallet.walletAddresses.address) {
key.currentState!.changeWalletName('');
@ -584,8 +554,7 @@ class ExchangePage extends BasePage {
}
}
Future<String> fetchParsedAddress(
BuildContext context, String domain, String ticker) async {
Future<String> fetchParsedAddress(BuildContext context, String domain, String ticker) async {
final parsedAddress = await getIt.get<AddressResolver>().resolve(domain, ticker);
final address = await extractAddressFromParsed(context, parsedAddress);
return address;
@ -594,16 +563,17 @@ class ExchangePage extends BasePage {
void _showFeeAlert(BuildContext context) async {
await Future<void>.delayed(Duration(seconds: 1));
final confirmed = await showPopUp<bool>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).low_fee,
alertContent: S.of(context).low_fee_alert,
leftButtonText: S.of(context).ignor,
rightButtonText: S.of(context).use_suggested,
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
actionRightButton: () => Navigator.of(dialogContext).pop(true));
}) ?? false;
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).low_fee,
alertContent: S.of(context).low_fee_alert,
leftButtonText: S.of(context).ignor,
rightButtonText: S.of(context).use_suggested,
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
actionRightButton: () => Navigator.of(dialogContext).pop(true));
}) ??
false;
if (confirmed) {
exchangeViewModel.setDefaultTransactionPriority();
}
@ -612,126 +582,122 @@ class ExchangePage extends BasePage {
void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel();
Widget _exchangeCardsSection(BuildContext context) {
final firstExchangeCard = Observer(builder: (_) => ExchangeCard(
onDispose: disposeBestRateSync,
hasAllAmount: exchangeViewModel.hasAllAmount,
allAmount: exchangeViewModel.hasAllAmount
? () => exchangeViewModel.calculateDepositAllAmount()
: null,
amountFocusNode: _depositAmountFocus,
addressFocusNode: _depositAddressFocus,
key: depositKey,
title: S.of(context).you_will_send,
initialCurrency: exchangeViewModel.depositCurrency,
initialWalletName: depositWalletName ?? '',
initialAddress:
exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
isAmountEstimated: false,
hasRefundAddress: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.depositCurrencies,
onCurrencySelected: (currency) {
// FIXME: need to move it into view model
if (currency == CryptoCurrency.xmr &&
exchangeViewModel.wallet.type != WalletType.monero) {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent:
S.of(context).exchange_incorrect_current_wallet_for_xmr,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(dialogContext).pop());
});
return;
}
final firstExchangeCard = Observer(
builder: (_) => ExchangeCard(
onDispose: disposeBestRateSync,
hasAllAmount: exchangeViewModel.hasAllAmount,
allAmount: exchangeViewModel.hasAllAmount
? () => exchangeViewModel.calculateDepositAllAmount()
: null,
amountFocusNode: _depositAmountFocus,
addressFocusNode: _depositAddressFocus,
key: depositKey,
title: S.of(context).you_will_send,
initialCurrency: exchangeViewModel.depositCurrency,
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
isAmountEstimated: false,
hasRefundAddress: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.depositCurrencies,
onCurrencySelected: (currency) {
// FIXME: need to move it into view model
if (currency == CryptoCurrency.xmr &&
exchangeViewModel.wallet.type != WalletType.monero) {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).exchange_incorrect_current_wallet_for_xmr,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(dialogContext).pop());
});
return;
}
exchangeViewModel.changeDepositCurrency(currency: currency);
},
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).focusColor!,
borderColor: Theme.of(context).primaryTextTheme!.bodyLarge!.color!,
currencyValueValidator: (value) {
return !exchangeViewModel.isFixedRateMode
? AmountValidator(
isAutovalidate: true,
currency: exchangeViewModel.depositCurrency,
minValue: exchangeViewModel.limits.min.toString(),
maxValue: exchangeViewModel.limits.max.toString(),
).call(value)
: null;
},
addressTextFieldValidator:
AddressValidator(type: exchangeViewModel.depositCurrency),
onPushPasteButton: (context) async {
final domain = exchangeViewModel.depositAddress;
final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, ticker);
},
onPushAddressBookButton: (context) async {
final domain = exchangeViewModel.depositAddress;
final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, ticker);
},
));
exchangeViewModel.changeDepositCurrency(currency: currency);
},
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).focusColor,
borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.color!,
currencyValueValidator: (value) {
return !exchangeViewModel.isFixedRateMode
? AmountValidator(
isAutovalidate: true,
currency: exchangeViewModel.depositCurrency,
minValue: exchangeViewModel.limits.min.toString(),
maxValue: exchangeViewModel.limits.max.toString(),
).call(value)
: null;
},
addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency),
onPushPasteButton: (context) async {
final domain = exchangeViewModel.depositAddress;
final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, ticker);
},
onPushAddressBookButton: (context) async {
final domain = exchangeViewModel.depositAddress;
final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, ticker);
},
));
final secondExchangeCard = Observer(builder: (_) => ExchangeCard(
onDispose: disposeBestRateSync,
amountFocusNode: _receiveAmountFocus,
addressFocusNode: _receiveAddressFocus,
key: receiveKey,
title: S.of(context).you_will_get,
initialCurrency: exchangeViewModel.receiveCurrency,
initialWalletName: receiveWalletName ?? '',
initialAddress:
exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled,
isAmountEstimated: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.receiveCurrencies,
onCurrencySelected: (currency) =>
exchangeViewModel.changeReceiveCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).focusColor!,
borderColor:
Theme.of(context).primaryTextTheme!.bodyLarge!.decorationColor!,
currencyValueValidator: (value) {
return exchangeViewModel.isFixedRateMode
? AmountValidator(
isAutovalidate: true,
currency: exchangeViewModel.receiveCurrency,
minValue: exchangeViewModel.limits.min.toString(),
maxValue: exchangeViewModel.limits.max.toString(),
).call(value)
: null;
},
addressTextFieldValidator:
AddressValidator(type: exchangeViewModel.receiveCurrency),
onPushPasteButton: (context) async {
final domain = exchangeViewModel.receiveAddress;
final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, ticker);
},
onPushAddressBookButton: (context) async {
final domain = exchangeViewModel.receiveAddress;
final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, ticker);
},
));
final secondExchangeCard = Observer(
builder: (_) => ExchangeCard(
onDispose: disposeBestRateSync,
amountFocusNode: _receiveAmountFocus,
addressFocusNode: _receiveAddressFocus,
key: receiveKey,
title: S.of(context).you_will_get,
initialCurrency: exchangeViewModel.receiveCurrency,
initialWalletName: receiveWalletName ?? '',
initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled,
isAmountEstimated: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.receiveCurrencies,
onCurrencySelected: (currency) =>
exchangeViewModel.changeReceiveCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).focusColor,
borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.decorationColor!,
currencyValueValidator: (value) {
return exchangeViewModel.isFixedRateMode
? AmountValidator(
isAutovalidate: true,
currency: exchangeViewModel.receiveCurrency,
minValue: exchangeViewModel.limits.min.toString(),
maxValue: exchangeViewModel.limits.max.toString(),
).call(value)
: null;
},
addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency),
onPushPasteButton: (context) async {
final domain = exchangeViewModel.receiveAddress;
final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, ticker);
},
onPushAddressBookButton: (context) async {
final domain = exchangeViewModel.receiveAddress;
final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, ticker);
},
));
if (ResponsiveLayoutUtil.instance.isMobile) {
return MobileExchangeCardsSection(

View file

@ -97,7 +97,8 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
}
if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
if (!_isInactive &&
widget.authenticationStore.state == AuthenticationState.allowed) {
setState(() => _setInactive(true));
}
@ -124,13 +125,16 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
} else {
final useTotp = widget.appStore.settingsStore.useTOTP2FA;
if (useTotp) {
final shouldUseTotp2FAToAccessWallets = widget.appStore
.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
if (useTotp && shouldUseTotp2FAToAccessWallets) {
_reset();
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
onTotpAuthenticationFinished:
(bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) {
(bool isAuthenticatedSuccessfully,
TotpAuthCodePageState totpAuth) {
if (!isAuthenticatedSuccessfully) {
return;
}
@ -151,15 +155,11 @@ class RootState extends State<Root> with WidgetsBindingObserver {
route: launchUri != null ? Routes.send : null,
arguments: PaymentRequest.fromUri(launchUri),
);
launchUri = null;
launchUri = null;
}
}
},
);
},
);
});
} else if (launchUri != null) {
widget.navigatorKey.currentState?.pushNamed(

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
@ -32,10 +33,12 @@ import 'package:cw_core/crypto_currency.dart';
class SendPage extends BasePage {
SendPage({
required this.sendViewModel,
required this.authService,
this.initialPaymentRequest,
}) : _formKey = GlobalKey<FormState>();
final SendViewModel sendViewModel;
final AuthService authService;
final GlobalKey<FormState> _formKey;
final controller = PageController(initialPage: 0);
final PaymentRequest? initialPaymentRequest;
@ -56,12 +59,14 @@ class SendPage extends BasePage {
@override
Widget? leading(BuildContext context) {
final _backButton = Icon(Icons.arrow_back_ios,
final _backButton = Icon(
Icons.arrow_back_ios,
color: titleColor,
size: 16,
);
final _closeButton = currentTheme.type == ThemeType.dark
? closeButtonImageDarkTheme : closeButtonImage;
? closeButtonImageDarkTheme
: closeButtonImage;
bool isMobileView = ResponsiveLayoutUtil.instance.isMobile;
@ -78,7 +83,7 @@ class SendPage extends BasePage {
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith(
(states) => Colors.transparent),
(states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: !isMobileView ? _closeButton : _backButton,
@ -114,11 +119,13 @@ class SendPage extends BasePage {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(right:8.0),
child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend),),
padding: const EdgeInsets.only(right: 8.0),
child: Observer(
builder: (_) =>
SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend),
),
),
if (supMiddle != null)
supMiddle
if (supMiddle != null) supMiddle
],
);
}
@ -200,12 +207,12 @@ class SendPage extends BasePage {
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context)
.primaryTextTheme
!.displaySmall!
.primaryTextTheme!
.displaySmall!
.backgroundColor!,
activeDotColor: Theme.of(context)
.primaryTextTheme
!.displayMedium!
.primaryTextTheme!
.displayMedium!
.backgroundColor!),
)
: Offstage();
@ -339,8 +346,8 @@ class SendPage extends BasePage {
'Change your asset (${sendViewModel.selectedCryptoCurrency})',
color: Colors.transparent,
textColor: Theme.of(context)
.accentTextTheme
!.displaySmall!
.accentTextTheme!
.displaySmall!
.decorationColor!,
))),
if (sendViewModel.hasMultiRecipient)
@ -357,13 +364,13 @@ class SendPage extends BasePage {
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor: Theme.of(context)
.accentTextTheme
!.displaySmall!
.accentTextTheme!
.displaySmall!
.decorationColor!,
isDottedBorder: true,
borderColor: Theme.of(context)
.primaryTextTheme
!.displaySmall!
.primaryTextTheme!
.displaySmall!
.decorationColor!,
)),
Observer(
@ -390,7 +397,16 @@ class SendPage extends BasePage {
return;
}
await sendViewModel.createTransaction();
final check = sendViewModel.shouldDisplayTotp();
authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA: check,
onAuthSuccess: (value) async {
if (value) {
await sendViewModel.createTransaction();
}
},
);
},
text: S.of(context).send,
color:

View file

@ -30,12 +30,22 @@ class SecurityBackupPage extends BasePage {
child: Column(mainAxisSize: MainAxisSize.min, children: [
SettingsCellWithArrow(
title: S.current.show_keys,
handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys),
handler: (_) => _authService.authenticateAction(
context,
route: Routes.showKeys,
conditionToDetermineIfToUse2FA: _securitySettingsViewModel
.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
title: S.current.create_backup,
handler: (_) => _authService.authenticateAction(context, route: Routes.backup),
handler: (_) => _authService.authenticateAction(
context,
route: Routes.backup,
conditionToDetermineIfToUse2FA: _securitySettingsViewModel
.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
@ -46,6 +56,8 @@ class SecurityBackupPage extends BasePage {
arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
setupPinContext.close();
},
conditionToDetermineIfToUse2FA: _securitySettingsViewModel
.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
@ -67,7 +79,10 @@ class SecurityBackupPage extends BasePage {
_securitySettingsViewModel
.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
}
});
},
conditionToDetermineIfToUse2FA: _securitySettingsViewModel
.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
);
} else {
_securitySettingsViewModel.setAllowBiometricalAuthentication(value);
}
@ -94,6 +109,8 @@ class SecurityBackupPage extends BasePage {
route: _securitySettingsViewModel.useTotp2FA
? Routes.modify2FAPage
: Routes.setup_2faPage,
conditionToDetermineIfToUse2FA: _securitySettingsViewModel
.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
),
);
},

View file

@ -22,7 +22,7 @@ class SettingsChoicesCell extends StatelessWidget {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
),
),
],
@ -34,10 +34,7 @@ class SettingsChoicesCell extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context)
.accentTextTheme!
.displaySmall!
.color!,
color: Theme.of(context).accentTextTheme.displaySmall!.color!,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@ -52,10 +49,7 @@ class SettingsChoicesCell extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: isSelected
? Theme.of(context)
.accentTextTheme!
.bodyLarge!
.color!
? Theme.of(context).accentTextTheme.bodyLarge!.color!
: null,
),
child: Text(
@ -63,10 +57,7 @@ class SettingsChoicesCell extends StatelessWidget {
style: TextStyle(
color: isSelected
? Colors.white
: Theme.of(context)
.primaryTextTheme!
.bodySmall!
.color!,
: Theme.of(context).primaryTextTheme.bodySmall!.color!,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal,
),
),

View file

@ -1,12 +1,16 @@
import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/view_model/settings/choices_list_item.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import '../../../routes.dart';
@ -21,35 +25,148 @@ class Modify2FAPage extends BasePage {
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsCellWithArrow(
title: S.current.disable_cake_2fa,
handler: (_) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.current.disable_cake_2fa,
alertContent: S.current.question_to_disable_2fa,
leftButtonText: S.current.cancel,
rightButtonText: S.current.disable,
actionLeftButton: () {
Navigator.of(context).pop();
},
actionRightButton: () {
setup2FAViewModel.setUseTOTP2FA(false);
Navigator.pushNamedAndRemoveUntil(
context, Routes.dashboard, (route) => false);
},
);
},
);
}),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
],
),
child: _2FAControlsWidget(setup2FAViewModel: setup2FAViewModel),
);
}
}
class _2FAControlsWidget extends StatelessWidget {
const _2FAControlsWidget({required this.setup2FAViewModel});
final Setup2FAViewModel setup2FAViewModel;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsCellWithArrow(
title: S.current.disable_cake_2fa,
handler: (_) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.current.disable_cake_2fa,
alertContent: S.current.question_to_disable_2fa,
leftButtonText: S.current.cancel,
rightButtonText: S.current.disable,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () {
setup2FAViewModel.setUseTOTP2FA(false);
Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false);
},
);
},
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsChoicesCell(
ChoicesListItem<Cake2FAPresetsOptions>(
title: S.current.cake_2fa_preset,
onItemSelected: setup2FAViewModel.selectCakePreset,
selectedItem: setup2FAViewModel.selectedCake2FAPreset,
items: [
Cake2FAPresetsOptions.narrow,
Cake2FAPresetsOptions.normal,
Cake2FAPresetsOptions.aggressive,
],
),
);
},
),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_assessing_wallet,
value: setup2FAViewModel.shouldRequireTOTP2FAForAccessingWallet,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForAccessingWallet(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_sends_to_non_contacts,
value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToNonContact,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToNonContact(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_sends_to_contacts,
value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToContact,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToContact(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_sends_to_internal_wallets,
value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToInternalWallets,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToInternalWallets(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_exchanges_to_internal_wallets,
value: setup2FAViewModel.shouldRequireTOTP2FAForExchangesToInternalWallets,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForExchangesToInternalWallets(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_adding_contacts,
value: setup2FAViewModel.shouldRequireTOTP2FAForAddingContacts,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForAddingContacts(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_creating_new_wallets,
value: setup2FAViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
onValueChange: (context, value) async =>
setup2FAViewModel.switchShouldRequireTOTP2FAForCreatingNewWallet(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(
builder: (context) {
return SettingsSwitcherCell(
title: S.current.require_for_all_security_and_backup_settings,
value: setup2FAViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
onValueChange: (context, value) async => setup2FAViewModel
.switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(value),
);
},
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
],
);
}
}

View file

@ -52,7 +52,8 @@ class Setup2FAPage extends BasePage {
SizedBox(height: 86),
SettingsCellWithArrow(
title: S.current.setup_totp_recommended,
handler: (_) => Navigator.of(context).pushNamed(Routes.setup_2faQRPage),
handler: (_) => Navigator.of(context)
.pushReplacementNamed(Routes.setup_2faQRPage),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
],

View file

@ -18,7 +18,8 @@ import 'package:mobx/mobx.dart';
import '../../../palette.dart';
import '../../../routes.dart';
typedef OnTotpAuthenticationFinished = void Function(bool, TotpAuthCodePageState);
typedef OnTotpAuthenticationFinished = void Function(
bool, TotpAuthCodePageState);
class TotpAuthCodePage extends StatefulWidget {
TotpAuthCodePage(
@ -43,8 +44,9 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
@override
void initState() {
if(widget.totpArguments.onTotpAuthenticationFinished != null) {
_reaction ??= reaction((_) => widget.setup2FAViewModel.state, (ExecutionState state) {
if (widget.totpArguments.onTotpAuthenticationFinished != null) {
_reaction ??= reaction((_) => widget.setup2FAViewModel.state,
(ExecutionState state) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (state is ExecutedSuccessfullyState) {
widget.totpArguments.onTotpAuthenticationFinished!(true, this);
@ -57,9 +59,9 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
if (state is AuthenticationBanned) {
widget.totpArguments.onTotpAuthenticationFinished!(false, this);
}
});
});
}
});
});
}
super.initState();
@ -73,7 +75,8 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
void changeProcessText(String text) {
dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!);
_progressBar = createBar<void>(text, duration: null)
..show(_key.currentContext!);
}
Future<void> close({String? route, dynamic arguments}) async {
@ -82,7 +85,8 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
}
await Future<void>.delayed(Duration(milliseconds: 50));
if (route != null) {
Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments);
Navigator.of(_key.currentContext!)
.pushReplacementNamed(route, arguments: arguments);
} else {
Navigator.of(_key.currentContext!).pop();
}
@ -120,7 +124,8 @@ class TOTPEnterCode extends BasePage {
}
@override
String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
String get title =>
isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
Widget? leading(BuildContext context) {
return isClosable ? super.leading(context) : null;
@ -166,21 +171,24 @@ class TOTPEnterCode extends BasePage {
return PrimaryButton(
isDisabled: setup2FAViewModel.enteredOTPCode.length != 8,
onPressed: () async {
final result =
await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup);
final bannedState = setup2FAViewModel.state is AuthenticationBanned;
final result = await setup2FAViewModel.totp2FAAuth(
totpController.text, isForSetup);
final bannedState =
setup2FAViewModel.state is AuthenticationBanned;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return PopUpCancellableAlertDialog(
contentText: _textDisplayedInPopupOnResult(result, bannedState, context),
contentText: _textDisplayedInPopupOnResult(
result, bannedState, context),
actionButtonText: S.of(context).ok,
buttonAction: () {
result ? setup2FAViewModel.success() : null;
if (isForSetup && result) {
Navigator.pushNamedAndRemoveUntil(
context, Routes.dashboard, (route) => false);
Navigator.pop(context);
// Navigator.of(context)
// .popAndPushNamed(Routes.modify2FAPage);
} else {
Navigator.of(context).pop(result);
}
@ -188,6 +196,11 @@ class TOTPEnterCode extends BasePage {
);
},
);
if (isForSetup && result) {
Navigator.pushReplacementNamed(
context, Routes.modify2FAPage);
}
},
text: S.of(context).continue_text,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
@ -201,10 +214,13 @@ class TOTPEnterCode extends BasePage {
);
}
String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) {
String _textDisplayedInPopupOnResult(
bool result, bool bannedState, BuildContext context) {
switch (result) {
case true:
return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success;
return isForSetup
? S.current.totp_2fa_success
: S.current.totp_verification_success;
case false:
if (bannedState) {
final state = setup2FAViewModel.state as AuthenticationBanned;

View file

@ -125,7 +125,9 @@ class WalletEditPage extends BasePage {
}
_onSuccessfulAuth(context);
});
},
conditionToDetermineIfToUse2FA: false,
);
}
void _onSuccessfulAuth(BuildContext context) async {

View file

@ -55,7 +55,7 @@ class WalletListBodyState extends State<WalletListBody> {
final newWalletImage =
Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white);
final restoreWalletImage = Image.asset('assets/images/restore_wallet.png',
height: 12, width: 12, color: Theme.of(context).primaryTextTheme!.titleLarge!.color!);
height: 12, width: 12, color: Theme.of(context).primaryTextTheme.titleLarge!.color!);
return Container(
padding: EdgeInsets.only(top: 16),
@ -72,7 +72,7 @@ class WalletListBodyState extends State<WalletListBody> {
itemBuilder: (__, index) {
final wallet = widget.walletListViewModel.wallets[index];
final currentColor = wallet.isCurrent
? Theme.of(context).accentTextTheme!.titleSmall!.decorationColor!
? Theme.of(context).accentTextTheme.titleSmall!.decorationColor!
: Theme.of(context).colorScheme.background;
final row = GestureDetector(
onTap: () => wallet.isCurrent ? null : _loadWallet(wallet),
@ -131,8 +131,7 @@ class WalletListBodyState extends State<WalletListBody> {
: Row(children: [
Expanded(child: row),
GestureDetector(
onTap: () => Navigator.of(context).pushNamed(
Routes.walletEdit,
onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit,
arguments: [widget.walletListViewModel, wallet]),
child: Container(
padding: EdgeInsets.only(right: 20),
@ -150,10 +149,7 @@ class WalletListBodyState extends State<WalletListBody> {
child: Icon(
Icons.edit,
size: 14,
color: Theme.of(context)
.textTheme
.headlineMedium!
.color!,
color: Theme.of(context).textTheme.headlineMedium!.color!,
),
),
),
@ -167,27 +163,59 @@ class WalletListBodyState extends State<WalletListBody> {
bottomSection: Column(children: <Widget>[
PrimaryImageButton(
onPressed: () {
//TODO(David): Find a way to optimize this
if (isSingleCoin) {
Navigator.of(context).pushNamed(Routes.newWallet,
arguments: widget.walletListViewModel.currentWalletType);
if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
widget.authService.authenticateAction(
context,
route: Routes.newWallet,
arguments: widget.walletListViewModel.currentWalletType,
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
Navigator.of(context).pushNamed(
Routes.newWallet,
arguments: widget.walletListViewModel.currentWalletType,
);
}
} else {
Navigator.of(context).pushNamed(Routes.newWalletType);
if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
widget.authService.authenticateAction(
context,
route: Routes.newWalletType,
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
Navigator.of(context).pushNamed(Routes.newWalletType);
}
}
},
image: newWalletImage,
text: S.of(context).wallet_list_create_new_wallet,
color: Theme.of(context).accentTextTheme!.bodyLarge!.color!,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
),
SizedBox(height: 10.0),
PrimaryImageButton(
onPressed: () {
Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
widget.authService.authenticateAction(
context,
route: Routes.restoreOptions,
arguments: false,
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
}
},
image: restoreWalletImage,
text: S.of(context).wallet_list_restore_wallet,
color: Theme.of(context).accentTextTheme!.bodySmall!.color!,
textColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!)
color: Theme.of(context).accentTextTheme.bodySmall!.color!,
textColor: Theme.of(context).primaryTextTheme.titleLarge!.color!)
])),
);
}
@ -208,27 +236,31 @@ class WalletListBodyState extends State<WalletListBody> {
}
Future<void> _loadWallet(WalletListItem wallet) async {
await widget.authService.authenticateAction(context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
try {
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
await hideProgressText();
// only pop the wallets route in mobile as it will go back to dashboard page
// in desktop platforms the navigation tree is different
if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop();
});
await widget.authService.authenticateAction(
context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
} catch (e) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
});
try {
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
await hideProgressText();
// only pop the wallets route in mobile as it will go back to dashboard page
// in desktop platforms the navigation tree is different
if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop();
});
}
} catch (e) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
},
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
);
}
void changeProcessText(String text) {

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
@ -56,6 +57,15 @@ abstract class SettingsStoreBase with Store {
required this.isBitcoinBuyEnabled,
required this.actionlistDisplayMode,
required this.pinTimeOutDuration,
required Cake2FAPresetsOptions initialCake2FAPresetOptions,
required bool initialShouldRequireTOTP2FAForAccessingWallet,
required bool initialShouldRequireTOTP2FAForSendsToContact,
required bool initialShouldRequireTOTP2FAForSendsToNonContact,
required bool initialShouldRequireTOTP2FAForSendsToInternalWallets,
required bool initialShouldRequireTOTP2FAForExchangesToInternalWallets,
required bool initialShouldRequireTOTP2FAForAddingContacts,
required bool initialShouldRequireTOTP2FAForCreatingNewWallets,
required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority,
TransactionPriority? initialHavenTransactionPriority,
@ -67,6 +77,7 @@ abstract class SettingsStoreBase with Store {
shouldSaveRecipientAddress = initialSaveRecipientAddress,
fiatApiMode = initialFiatMode,
allowBiometricalAuthentication = initialAllowBiometricalAuthentication,
selectedCake2FAPreset = initialCake2FAPresetOptions,
totpSecretKey = initialTotpSecretKey,
useTOTP2FA = initialUseTOTP2FA,
numberOfFailedTokenTrials = initialFailedTokenTrial,
@ -78,6 +89,18 @@ abstract class SettingsStoreBase with Store {
currentTheme = initialTheme,
pinCodeLength = initialPinLength,
languageCode = initialLanguageCode,
shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet,
shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact,
shouldRequireTOTP2FAForSendsToNonContact = initialShouldRequireTOTP2FAForSendsToNonContact,
shouldRequireTOTP2FAForSendsToInternalWallets =
initialShouldRequireTOTP2FAForSendsToInternalWallets,
shouldRequireTOTP2FAForExchangesToInternalWallets =
initialShouldRequireTOTP2FAForExchangesToInternalWallets,
shouldRequireTOTP2FAForAddingContacts = initialShouldRequireTOTP2FAForAddingContacts,
shouldRequireTOTP2FAForCreatingNewWallets =
initialShouldRequireTOTP2FAForCreatingNewWallets,
shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
priority = ObservableMap<WalletType, TransactionPriority>() {
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
@ -166,6 +189,57 @@ abstract class SettingsStoreBase with Store {
(bool biometricalAuthentication) => sharedPreferences.setBool(
PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication));
reaction(
(_) => selectedCake2FAPreset,
(Cake2FAPresetsOptions selectedCake2FAPreset) => sharedPreferences.setInt(
PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset.serialize()));
reaction(
(_) => shouldRequireTOTP2FAForAccessingWallet,
(bool requireTOTP2FAForAccessingWallet) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForAccessingWallet,
requireTOTP2FAForAccessingWallet));
reaction(
(_) => shouldRequireTOTP2FAForSendsToContact,
(bool requireTOTP2FAForSendsToContact) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForSendsToContact, requireTOTP2FAForSendsToContact));
reaction(
(_) => shouldRequireTOTP2FAForSendsToNonContact,
(bool requireTOTP2FAForSendsToNonContact) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact,
requireTOTP2FAForSendsToNonContact));
reaction(
(_) => shouldRequireTOTP2FAForSendsToInternalWallets,
(bool requireTOTP2FAForSendsToInternalWallets) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets,
requireTOTP2FAForSendsToInternalWallets));
reaction(
(_) => shouldRequireTOTP2FAForExchangesToInternalWallets,
(bool requireTOTP2FAForExchangesToInternalWallets) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets,
requireTOTP2FAForExchangesToInternalWallets));
reaction(
(_) => shouldRequireTOTP2FAForAddingContacts,
(bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForAddingContacts, requireTOTP2FAForAddingContacts));
reaction(
(_) => shouldRequireTOTP2FAForCreatingNewWallets,
(bool requireTOTP2FAForCreatingNewWallets) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets,
requireTOTP2FAForCreatingNewWallets));
reaction(
(_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
(bool requireTOTP2FAForAllSecurityAndBackupSettings) => sharedPreferences.setBool(
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
requireTOTP2FAForAllSecurityAndBackupSettings));
reaction(
(_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use));
@ -249,6 +323,33 @@ abstract class SettingsStoreBase with Store {
@observable
bool allowBiometricalAuthentication;
@observable
bool shouldRequireTOTP2FAForAccessingWallet;
@observable
bool shouldRequireTOTP2FAForSendsToContact;
@observable
bool shouldRequireTOTP2FAForSendsToNonContact;
@observable
bool shouldRequireTOTP2FAForSendsToInternalWallets;
@observable
bool shouldRequireTOTP2FAForExchangesToInternalWallets;
@observable
Cake2FAPresetsOptions selectedCake2FAPreset;
@observable
bool shouldRequireTOTP2FAForAddingContacts;
@observable
bool shouldRequireTOTP2FAForCreatingNewWallets;
@observable
bool shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
@observable
String totpSecretKey;
@ -356,6 +457,29 @@ abstract class SettingsStoreBase with Store {
FiatApiMode.enabled.raw);
final allowBiometricalAuthentication =
sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false;
final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
Cake2FAPresetsOptions.normal.raw);
final shouldRequireTOTP2FAForAccessingWallet =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false;
final shouldRequireTOTP2FAForSendsToContact =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false;
final shouldRequireTOTP2FAForSendsToNonContact =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false;
final shouldRequireTOTP2FAForSendsToInternalWallets =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ??
false;
final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ??
false;
final shouldRequireTOTP2FAForAddingContacts =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false;
final shouldRequireTOTP2FAForCreatingNewWallets =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ??
false;
final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ??
false;
final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? '';
final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false;
final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0;
@ -433,6 +557,7 @@ abstract class SettingsStoreBase with Store {
initialDisableSell: disableSell,
initialFiatMode: currentFiatApiMode,
initialAllowBiometricalAuthentication: allowBiometricalAuthentication,
initialCake2FAPresetOptions: selectedCake2FAPreset,
initialTotpSecretKey: totpSecretKey,
initialUseTOTP2FA: useTOTP2FA,
initialFailedTokenTrial: tokenTrialNumber,
@ -446,6 +571,17 @@ abstract class SettingsStoreBase with Store {
initialBitcoinTransactionPriority: bitcoinTransactionPriority,
initialHavenTransactionPriority: havenTransactionPriority,
initialLitecoinTransactionPriority: litecoinTransactionPriority,
initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet,
initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact,
initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact,
initialShouldRequireTOTP2FAForSendsToInternalWallets:
shouldRequireTOTP2FAForSendsToInternalWallets,
initialShouldRequireTOTP2FAForExchangesToInternalWallets:
shouldRequireTOTP2FAForExchangesToInternalWallets,
initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts,
initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets,
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings:
shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
shouldShowYatPopup: shouldShowYatPopup);
}
@ -480,6 +616,7 @@ abstract class SettingsStoreBase with Store {
shouldSaveRecipientAddress;
totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey;
useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA;
numberOfFailedTokenTrials =
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ??
@ -490,9 +627,35 @@ abstract class SettingsStoreBase with Store {
allowBiometricalAuthentication =
sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ??
allowBiometricalAuthentication;
selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
Cake2FAPresetsOptions.normal.raw);
shouldRequireTOTP2FAForAccessingWallet =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false;
shouldRequireTOTP2FAForSendsToContact =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false;
shouldRequireTOTP2FAForSendsToNonContact =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false;
shouldRequireTOTP2FAForSendsToInternalWallets =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ??
false;
shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ??
false;
shouldRequireTOTP2FAForAddingContacts =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false;
shouldRequireTOTP2FAForCreatingNewWallets =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ??
false;
shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ??
false;
shouldShowMarketPlaceInDashboard =
sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ??
shouldShowMarketPlaceInDashboard;
selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
Cake2FAPresetsOptions.narrow.raw);
exchangeStatus = ExchangeApiMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ??
ExchangeApiMode.enabled.raw);

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
@ -11,10 +12,12 @@ import 'package:cw_core/crypto_currency.dart';
part 'contact_list_view_model.g.dart';
class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel;
class ContactListViewModel = ContactListViewModelBase
with _$ContactListViewModel;
abstract class ContactListViewModelBase with Store {
ContactListViewModelBase(this.contactSource, this.walletInfoSource, this._currency)
ContactListViewModelBase(this.contactSource, this.walletInfoSource,
this._currency, this.settingsStore)
: contacts = ObservableList<ContactRecord>(),
walletContacts = [] {
walletInfoSource.values.forEach((info) {
@ -42,16 +45,23 @@ abstract class ContactListViewModelBase with Store {
final List<WalletContact> walletContacts;
final CryptoCurrency? _currency;
StreamSubscription<BoxEvent>? _subscription;
final SettingsStore settingsStore;
bool get isEditable => _currency == null;
@computed
bool get shouldRequireTOTP2FAForAddingContacts =>
settingsStore.shouldRequireTOTP2FAForAddingContacts;
Future<void> delete(ContactRecord contact) async => contact.original.delete();
@computed
List<ContactRecord> get contactsToShow =>
contacts.where((element) => _currency == null || element.type == _currency).toList();
List<ContactRecord> get contactsToShow => contacts
.where((element) => _currency == null || element.type == _currency)
.toList();
@computed
List<WalletContact> get walletContactsToShow =>
walletContacts.where((element) => _currency == null || element.type == _currency).toList();
List<WalletContact> get walletContactsToShow => walletContacts
.where((element) => _currency == null || element.type == _currency)
.toList();
}

View file

@ -4,12 +4,14 @@ import 'dart:convert';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart';
import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trocador/trocador_request.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
@ -44,42 +46,55 @@ part 'exchange_view_model.g.dart';
class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
abstract class ExchangeViewModelBase with Store {
ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
this.tradesStore, this._settingsStore, this.sharedPreferences)
: _cryptoNumberFormat = NumberFormat(),
isFixedRateMode = false,
isReceiveAmountEntered = false,
depositAmount = '',
receiveAmount = '',
receiveAddress = '',
depositAddress = '',
isDepositAddressEnabled = false,
isReceiveAddressEnabled = false,
isReceiveAmountEditable = false,
_useTorOnly = false,
receiveCurrencies = <CryptoCurrency>[],
depositCurrencies = <CryptoCurrency>[],
limits = Limits(min: 0, max: 0),
tradeState = ExchangeTradeStateInitial(),
limitsState = LimitsInitialState(),
receiveCurrency = wallet.currency,
depositCurrency = wallet.currency,
providerList = [],
selectedProviders = ObservableList<ExchangeProvider>() {
ExchangeViewModelBase(
this.wallet,
this.trades,
this._exchangeTemplateStore,
this.tradesStore,
this._settingsStore,
this.sharedPreferences,
this.contactListViewModel)
: _cryptoNumberFormat = NumberFormat(),
isFixedRateMode = false,
isReceiveAmountEntered = false,
depositAmount = '',
receiveAmount = '',
receiveAddress = '',
depositAddress = '',
isDepositAddressEnabled = false,
isReceiveAddressEnabled = false,
isReceiveAmountEditable = false,
_useTorOnly = false,
receiveCurrencies = <CryptoCurrency>[],
depositCurrencies = <CryptoCurrency>[],
limits = Limits(min: 0, max: 0),
tradeState = ExchangeTradeStateInitial(),
limitsState = LimitsInitialState(),
receiveCurrency = wallet.currency,
depositCurrency = wallet.currency,
providerList = [],
selectedProviders = ObservableList<ExchangeProvider>() {
_useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly;
_setProviders();
const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano];
const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp,
CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano];
const excludeReceiveCurrencies = [
CryptoCurrency.xlm,
CryptoCurrency.xrp,
CryptoCurrency.bnb,
CryptoCurrency.btt,
CryptoCurrency.nano
];
_initialPairBasedOnWallet();
final Map<String, dynamic> exchangeProvidersSelection = json
.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
final Map<String, dynamic> exchangeProvidersSelection = json.decode(
sharedPreferences
.getString(PreferencesKey.exchangeProvidersSelection) ??
"{}") as Map<String, dynamic>;
/// if the provider is not in the user settings (user's first time or newly added provider)
/// then use its default value decided by us
selectedProviders = ObservableList.of(providersForCurrentPair().where(
(element) => exchangeProvidersSelection[element.title] == null
selectedProviders = ObservableList.of(providersForCurrentPair()
.where((element) => exchangeProvidersSelection[element.title] == null
? element.isEnabled
: (exchangeProvidersSelection[element.title] as bool))
.toList());
@ -87,7 +102,8 @@ abstract class ExchangeViewModelBase with Store {
_setAvailableProviders();
_calculateBestRate();
bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate());
bestRateSync =
Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate());
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
@ -95,7 +111,8 @@ abstract class ExchangeViewModelBase with Store {
receiveAmount = '';
receiveAddress = '';
depositAddress = depositCurrency == wallet.currency
? wallet.walletAddresses.address : '';
? wallet.walletAddresses.address
: '';
provider = providersForCurrentPair().first;
final initialProvider = provider;
provider!.checkIsAvailable().then((bool isAvailable) {
@ -107,20 +124,20 @@ abstract class ExchangeViewModelBase with Store {
}
});
receiveCurrencies = CryptoCurrency.all
.where((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency))
.toList();
.where((cryptoCurrency) =>
!excludeReceiveCurrencies.contains(cryptoCurrency))
.toList();
depositCurrencies = CryptoCurrency.all
.where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency))
.toList();
.where((cryptoCurrency) =>
!excludeDepositCurrencies.contains(cryptoCurrency))
.toList();
_defineIsReceiveAmountEditable();
loadLimits();
reaction(
(_) => isFixedRateMode,
(Object _) {
loadLimits();
_bestRate = 0;
_calculateBestRate();
});
reaction((_) => isFixedRateMode, (Object _) {
loadLimits();
_bestRate = 0;
_calculateBestRate();
});
}
bool _useTorOnly;
final WalletBase wallet;
@ -148,7 +165,8 @@ abstract class ExchangeViewModelBase with Store {
/// initialize with descending comparator
/// since we want largest rate first
final SplayTreeMap<double, ExchangeProvider> _sortedAvailableProviders =
SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
SplayTreeMap<double, ExchangeProvider>(
(double a, double b) => b.compareTo(a));
final List<ExchangeProvider> _tradeAvailableProviders = [];
@ -207,6 +225,37 @@ abstract class ExchangeViewModelBase with Store {
ObservableList<ExchangeTemplate> get templates =>
_exchangeTemplateStore.templates;
@computed
List<WalletContact> get walletContactsToShow =>
contactListViewModel.walletContacts
.where((element) =>
receiveCurrency == null || element.type == receiveCurrency)
.toList();
@action
bool checkIfWalletIsAnInternalWallet(String address) {
final walletContactList = walletContactsToShow
.where((element) => element.address == address)
.toList();
return walletContactList.isNotEmpty;
}
@computed
bool get shouldDisplayTOTP2FAForExchangesToInternalWallet =>
_settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
//* Still open to further optimize these checks
//* It works but can be made better
@action
bool shouldDisplayTOTP() {
final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress);
if (isInternalWallet) {
return shouldDisplayTOTP2FAForExchangesToInternalWallet;
}
return false;
}
@computed
TransactionPriority get transactionPriority {
@ -219,21 +268,23 @@ abstract class ExchangeViewModelBase with Store {
return priority;
}
bool get hasAllAmount =>
(wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && depositCurrency == wallet.currency;
bool get isMoneroWallet => wallet.type == WalletType.monero;
bool get isMoneroWallet => wallet.type == WalletType.monero;
bool get isLowFee {
bool get isLowFee {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
return transactionPriority == monero!.getMoneroTransactionPrioritySlow();
return transactionPriority ==
monero!.getMoneroTransactionPrioritySlow();
case WalletType.bitcoin:
return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow();
return transactionPriority ==
bitcoin!.getBitcoinTransactionPrioritySlow();
case WalletType.litecoin:
return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow();
return transactionPriority ==
bitcoin!.getLitecoinTransactionPrioritySlow();
default:
return false;
}
@ -247,6 +298,8 @@ abstract class ExchangeViewModelBase with Store {
final SettingsStore _settingsStore;
final ContactListViewModel contactListViewModel;
double _bestRate = 0.0;
late Timer bestRateSync;
@ -337,23 +390,24 @@ abstract class ExchangeViewModelBase with Store {
}
Future<void> _calculateBestRate() async {
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1;
final amount =
double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1;
final _providers = _tradeAvailableProviders
.where((element) => !isFixedRateMode || element.supportsFixedRate).toList();
.where((element) => !isFixedRateMode || element.supportsFixedRate)
.toList();
final result = await Future.wait<double>(
_providers.map((element) => element.fetchRate(
from: depositCurrency,
to: receiveCurrency,
amount: amount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: isFixedRateMode))
);
final result = await Future.wait<double>(_providers.map((element) =>
element.fetchRate(
from: depositCurrency,
to: receiveCurrency,
amount: amount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: isFixedRateMode)));
_sortedAvailableProviders.clear();
for (int i=0;i<result.length;i++) {
for (int i = 0; i < result.length; i++) {
if (result[i] != 0) {
/// add this provider as its valid for this trade
try {
@ -377,12 +431,8 @@ abstract class ExchangeViewModelBase with Store {
limitsState = LimitsIsLoading();
final from = isFixedRateMode
? receiveCurrency
: depositCurrency;
final to = isFixedRateMode
? depositCurrency
: receiveCurrency;
final from = isFixedRateMode ? receiveCurrency : depositCurrency;
final to = isFixedRateMode ? depositCurrency : receiveCurrency;
double? lowestMin = double.maxFinite;
double? highestMax = 0.0;
@ -396,14 +446,13 @@ abstract class ExchangeViewModelBase with Store {
try {
final tempLimits = await provider.fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode);
from: from, to: to, isFixedRateMode: isFixedRateMode);
if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin) {
lowestMin = tempLimits.min;
}
if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax) {
if (highestMax != null &&
(tempLimits.max ?? double.maxFinite) > highestMax) {
highestMax = tempLimits.max;
}
} catch (e) {
@ -445,7 +494,7 @@ abstract class ExchangeViewModelBase with Store {
settleMethod: receiveCurrency,
depositAmount: isFixedRateMode
? receiveAmount.replaceAll(',', '.')
: depositAmount.replaceAll(',', '.'),
: depositAmount.replaceAll(',', '.'),
settleAddress: receiveAddress,
refundAddress: depositAddress,
);
@ -525,6 +574,7 @@ abstract class ExchangeViewModelBase with Store {
tradesStore.setTrade(trade);
await trades.add(trade);
tradeState = TradeIsCreatedSuccessfully(trade: trade);
/// return after the first successful trade
return;
} catch (e) {
@ -555,9 +605,11 @@ abstract class ExchangeViewModelBase with Store {
depositAmount = '';
receiveAmount = '';
depositAddress = depositCurrency == wallet.currency
? wallet.walletAddresses.address : '';
? wallet.walletAddresses.address
: '';
receiveAddress = receiveCurrency == wallet.currency
? wallet.walletAddresses.address : '';
? wallet.walletAddresses.address
: '';
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
isFixedRateMode = false;
@ -576,7 +628,8 @@ abstract class ExchangeViewModelBase with Store {
}
final amount = availableBalance - fee;
changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
changeDepositAmount(
amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
}
}
@ -612,8 +665,7 @@ abstract class ExchangeViewModelBase with Store {
{required CryptoCurrency from, required CryptoCurrency to}) {
final providers = providerList
.where((provider) => provider.pairList
.where((pair) =>
pair.from == from && pair.to == to)
.where((pair) => pair.from == from && pair.to == to)
.isNotEmpty)
.toList();
@ -690,11 +742,14 @@ abstract class ExchangeViewModelBase with Store {
_bestRate = 0;
_calculateBestRate();
final Map<String, dynamic> exchangeProvidersSelection = json
.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
final Map<String, dynamic> exchangeProvidersSelection = json.decode(
sharedPreferences
.getString(PreferencesKey.exchangeProvidersSelection) ??
"{}") as Map<String, dynamic>;
for (var provider in providerList) {
exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider);
exchangeProvidersSelection[provider.title] =
selectedProviders.contains(provider);
}
sharedPreferences.setString(
@ -705,15 +760,15 @@ abstract class ExchangeViewModelBase with Store {
bool get isAvailableInSelected {
final providersForPair = providersForCurrentPair();
return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element));
return selectedProviders.any(
(element) => element.isAvailable && providersForPair.contains(element));
}
void _setAvailableProviders() {
_tradeAvailableProviders.clear();
_tradeAvailableProviders.addAll(
selectedProviders
.where((provider) => providersForCurrentPair().contains(provider)));
_tradeAvailableProviders.addAll(selectedProviders
.where((provider) => providersForCurrentPair().contains(provider)));
}
@action
@ -721,22 +776,27 @@ abstract class ExchangeViewModelBase with Store {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
_settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic();
_settingsStore.priority[wallet.type] =
monero!.getMoneroTransactionPriorityAutomatic();
break;
case WalletType.bitcoin:
_settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium();
_settingsStore.priority[wallet.type] =
bitcoin!.getBitcoinTransactionPriorityMedium();
break;
case WalletType.litecoin:
_settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium();
_settingsStore.priority[wallet.type] =
bitcoin!.getLitecoinTransactionPriorityMedium();
break;
default:
break;
}
}
void _setProviders(){
void _setProviders() {
if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) {
providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList();
providerList = _allProviders
.where((provider) => provider.supportsOnionAddress)
.toList();
} else {
providerList = _allProviders;
}

View file

@ -1,6 +1,8 @@
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/view_model/send/output.dart';
@ -38,6 +40,7 @@ abstract class SendViewModelBase with Store {
this.sendTemplateViewModel,
this._fiatConversationStore,
this.balanceViewModel,
this.contactListViewModel,
this.transactionDescriptionBox)
: state = InitialExecutionState(),
currencies = _wallet.balance.keys.toList(),
@ -50,8 +53,9 @@ abstract class SendViewModelBase with Store {
if (!priorityForWalletType(_wallet.type).contains(priority)) {
_settingsStore.priority[_wallet.type] = priorities.first;
}
outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
outputs
.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
}
@observable
@ -61,7 +65,8 @@ abstract class SendViewModelBase with Store {
@action
void addOutput() {
outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
outputs
.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
}
@action
@ -148,13 +153,11 @@ abstract class SendViewModelBase with Store {
@computed
String get pendingTransactionFiatAmountFormatted =>
isFiatDisabled ? '' : pendingTransactionFiatAmount +
' ' + fiat.title;
isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title;
@computed
String get pendingTransactionFeeFiatAmountFormatted =>
isFiatDisabled ? '' : pendingTransactionFeeFiatAmount +
' ' + fiat.title;
isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title;
@computed
bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus;
@ -175,9 +178,8 @@ abstract class SendViewModelBase with Store {
bool get hasMultiRecipient => _wallet.type != WalletType.haven;
bool get hasYat => outputs.any((out) =>
out.isParsedAddress &&
out.parsedAddress.parseFrom == ParseFrom.yatRecord);
bool get hasYat => outputs
.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);
WalletType get walletType => _wallet.type;
@ -193,9 +195,73 @@ abstract class SendViewModelBase with Store {
final SettingsStore _settingsStore;
final SendTemplateViewModel sendTemplateViewModel;
final BalanceViewModel balanceViewModel;
final ContactListViewModel contactListViewModel;
final FiatConversionStore _fiatConversationStore;
final Box<TransactionDescription> transactionDescriptionBox;
@computed
List<ContactRecord> get contactsToShow => contactListViewModel.contacts
.where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency)
.toList();
@computed
List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
.where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency)
.toList();
@action
bool checkIfAddressIsAContact(String address) {
final contactList = contactsToShow.where((element) => element.address == address).toList();
return contactList.isNotEmpty;
}
@action
bool checkIfWalletIsAnInternalWallet(String address) {
final walletContactList =
walletContactsToShow.where((element) => element.address == address).toList();
return walletContactList.isNotEmpty;
}
@computed
bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact;
@computed
bool get shouldDisplayTOTP2FAForNonContact =>
_settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
@computed
bool get shouldDisplayTOTP2FAForSendsToInternalWallet =>
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
//* Still open to further optimize these checks
//* It works but can be made better
@action
bool checkThroughChecksToDisplayTOTP(String address) {
final isContact = checkIfAddressIsAContact(address);
final isInternalWallet = checkIfWalletIsAnInternalWallet(address);
if (isContact) {
return shouldDisplayTOTP2FAForContact;
} else if (isInternalWallet) {
return shouldDisplayTOTP2FAForSendsToInternalWallet;
} else {
return shouldDisplayTOTP2FAForNonContact;
}
}
bool shouldDisplayTotp() {
List<bool> conditionsList = [];
for (var output in outputs) {
final show = checkThroughChecksToDisplayTOTP(output.address);
conditionsList.add(show);
}
return conditionsList.contains(true);
}
@action
Future<void> createTransaction() async {
try {
@ -234,11 +300,9 @@ abstract class SendViewModelBase with Store {
if (pendingTransaction!.id.isNotEmpty) {
_settingsStore.shouldSaveRecipientAddress
? await transactionDescriptionBox.add(TransactionDescription(
id: pendingTransaction!.id,
recipientAddress: address,
transactionNote: note))
: await transactionDescriptionBox.add(TransactionDescription(
id: pendingTransaction!.id, transactionNote: note));
id: pendingTransaction!.id, recipientAddress: address, transactionNote: note))
: await transactionDescriptionBox
.add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note));
}
state = TransactionCommitted();
@ -276,15 +340,15 @@ abstract class SendViewModelBase with Store {
throw Exception('Priority is null for wallet type: ${_wallet.type}');
}
return monero!.createMoneroTransactionCreationCredentials(
outputs: outputs, priority: priority);
return monero!
.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority);
case WalletType.haven:
final priority = _settingsStore.priority[_wallet.type];
if (priority == null) {
throw Exception('Priority is null for wallet type: ${_wallet.type}');
}
return haven!.createHavenTransactionCreationCredentials(
outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title);
default:
@ -304,14 +368,12 @@ abstract class SendViewModelBase with Store {
return priority.toString();
}
bool _isEqualCurrency(String currency) =>
bool _isEqualCurrency(String currency) =>
currency.toLowerCase() == _wallet.currency.title.toLowerCase();
@action
void onClose() =>
_settingsStore.fiatCurrency = fiatFromSettings;
void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
@action
void setFiatCurrency(FiatCurrency fiat) =>
_settingsStore.fiatCurrency = fiat;
void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat;
}

View file

@ -1,5 +1,6 @@
// ignore_for_file: prefer_final_fields
import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/totp_utils.dart' as Utils;
import 'package:cake_wallet/view_model/auth_state.dart';
@ -23,8 +24,11 @@ abstract class Setup2FAViewModelBase with Store {
Setup2FAViewModelBase(this._settingsStore, this._sharedPreferences, this._authService)
: _failureCounter = 0,
enteredOTPCode = '',
unhighlightTabs = false,
selected2FASettings = ObservableList<VerboseControlSettings>(),
state = InitialExecutionState() {
_getRandomBase32SecretKey();
selectCakePreset(selectedCake2FAPreset);
reaction((_) => state, _saveLastAuthTime);
}
@ -48,6 +52,38 @@ abstract class Setup2FAViewModelBase with Store {
@computed
bool get useTOTP2FA => _settingsStore.useTOTP2FA;
@computed
bool get shouldRequireTOTP2FAForAccessingWallet =>
_settingsStore.shouldRequireTOTP2FAForAccessingWallet;
@computed
bool get shouldRequireTOTP2FAForSendsToContact =>
_settingsStore.shouldRequireTOTP2FAForSendsToContact;
@computed
bool get shouldRequireTOTP2FAForSendsToNonContact =>
_settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
@computed
bool get shouldRequireTOTP2FAForSendsToInternalWallets =>
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
@computed
bool get shouldRequireTOTP2FAForExchangesToInternalWallets =>
_settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
@computed
bool get shouldRequireTOTP2FAForAddingContacts =>
_settingsStore.shouldRequireTOTP2FAForAddingContacts;
@computed
bool get shouldRequireTOTP2FAForCreatingNewWallets =>
_settingsStore.shouldRequireTOTP2FAForCreatingNewWallets;
@computed
bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings =>
_settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
void _getRandomBase32SecretKey() {
final randomBase32Key = Utils.generateRandomBase32SecretKey(16);
_setBase32SecretKey(randomBase32Key);
@ -156,4 +192,230 @@ abstract class Setup2FAViewModelBase with Store {
_authService.saveLastAuthTime();
}
}
@computed
Cake2FAPresetsOptions get selectedCake2FAPreset => _settingsStore.selectedCake2FAPreset;
@observable
bool unhighlightTabs = false;
@observable
ObservableList<VerboseControlSettings> selected2FASettings;
//! The code here works, but can be improved
//! Still trying out various ways to improve it
@action
void selectCakePreset(Cake2FAPresetsOptions cake2FAPreset) {
// The tabs are ordered in the format [Narrow || Normal || Verbose]
// Where Narrow = 0, Normal = 1 and Verbose = 2
switch (cake2FAPreset) {
case Cake2FAPresetsOptions.narrow:
activateCake2FANarrowPreset();
break;
case Cake2FAPresetsOptions.normal:
activateCake2FANormalPreset();
break;
case Cake2FAPresetsOptions.aggressive:
activateCake2FAAggressivePreset();
break;
default:
activateCake2FANormalPreset();
}
}
@action
void checkIfTheCurrentSettingMatchesAnyOfThePresets() {
final hasNormalPreset = checkIfTheNormalPresetIsPresent();
final hasNarrowPreset = checkIfTheNarrowPresetIsPresent();
final hasVerbosePreset = checkIfTheVerbosePresetIsPresent();
if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) {
unhighlightTabs = false;
} else {
unhighlightTabs = true;
}
}
@action
bool checkIfTheNormalPresetIsPresent() {
final hasContacts = selected2FASettings.contains(VerboseControlSettings.sendsToContacts);
final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
final hasSecurityAndBackup =
selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
final hasSendToInternalWallet =
selected2FASettings.contains(VerboseControlSettings.sendsToInternalWallets);
final hasExchangesToInternalWallet =
selected2FASettings.contains(VerboseControlSettings.exchangesToInternalWallets);
bool isOnlyNormalPresetControlsPresent = selected2FASettings.length == 5;
return (hasContacts &&
hasNonContacts &&
hasSecurityAndBackup &&
hasSendToInternalWallet &&
hasExchangesToInternalWallet &&
isOnlyNormalPresetControlsPresent);
}
@action
bool checkIfTheVerbosePresetIsPresent() {
final hasAccessWallets = selected2FASettings.contains(VerboseControlSettings.accessWallet);
final hasSecurityAndBackup =
selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
bool isOnlyVerbosePresetControlsPresent = selected2FASettings.length == 2;
return (hasAccessWallets && hasSecurityAndBackup && isOnlyVerbosePresetControlsPresent);
}
@action
bool checkIfTheNarrowPresetIsPresent() {
final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
final hasAddContacts = selected2FASettings.contains(VerboseControlSettings.addingContacts);
final hasCreateNewWallet =
selected2FASettings.contains(VerboseControlSettings.creatingNewWallets);
final hasSecurityAndBackup =
selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
bool isOnlyNarrowPresetControlsPresent = selected2FASettings.length == 4;
return (hasNonContacts &&
hasAddContacts &&
hasCreateNewWallet &&
hasSecurityAndBackup &&
isOnlyNarrowPresetControlsPresent);
}
@action
void activateCake2FANormalPreset() {
_settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.normal;
setAllControlsToFalse();
switchShouldRequireTOTP2FAForSendsToNonContact(true);
switchShouldRequireTOTP2FAForSendsToContact(true);
switchShouldRequireTOTP2FAForSendsToInternalWallets(true);
switchShouldRequireTOTP2FAForExchangesToInternalWallets(true);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
}
@action
void activateCake2FANarrowPreset() {
_settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.narrow;
setAllControlsToFalse();
switchShouldRequireTOTP2FAForSendsToNonContact(true);
switchShouldRequireTOTP2FAForAddingContacts(true);
switchShouldRequireTOTP2FAForCreatingNewWallet(true);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
}
@action
void activateCake2FAAggressivePreset() {
_settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.aggressive;
setAllControlsToFalse();
switchShouldRequireTOTP2FAForAccessingWallet(true);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
}
@action
void setAllControlsToFalse() {
switchShouldRequireTOTP2FAForAccessingWallet(false);
switchShouldRequireTOTP2FAForSendsToContact(false);
switchShouldRequireTOTP2FAForSendsToNonContact(false);
switchShouldRequireTOTP2FAForAddingContacts(false);
switchShouldRequireTOTP2FAForCreatingNewWallet(false);
switchShouldRequireTOTP2FAForExchangesToInternalWallets(false);
switchShouldRequireTOTP2FAForSendsToInternalWallets(false);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false);
selected2FASettings.clear();
unhighlightTabs = false;
}
@action
void switchShouldRequireTOTP2FAForAccessingWallet(bool value) {
_settingsStore.shouldRequireTOTP2FAForAccessingWallet = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.accessWallet);
} else {
selected2FASettings.remove(VerboseControlSettings.accessWallet);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForSendsToContact(bool value) {
_settingsStore.shouldRequireTOTP2FAForSendsToContact = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.sendsToContacts);
} else {
selected2FASettings.remove(VerboseControlSettings.sendsToContacts);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) {
_settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.sendsToNonContacts);
} else {
selected2FASettings.remove(VerboseControlSettings.sendsToNonContacts);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) {
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.sendsToInternalWallets);
} else {
selected2FASettings.remove(VerboseControlSettings.sendsToInternalWallets);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) {
_settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.exchangesToInternalWallets);
} else {
selected2FASettings.remove(VerboseControlSettings.exchangesToInternalWallets);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForAddingContacts(bool value) {
_settingsStore.shouldRequireTOTP2FAForAddingContacts = value;
if (value)
selected2FASettings.add(VerboseControlSettings.addingContacts);
else {
selected2FASettings.remove(VerboseControlSettings.addingContacts);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) {
_settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.creatingNewWallets);
} else {
selected2FASettings.remove(VerboseControlSettings.creatingNewWallets);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) {
_settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value;
if (value)
selected2FASettings.add(VerboseControlSettings.securityAndBackupSettings);
else {
selected2FASettings.remove(VerboseControlSettings.securityAndBackupSettings);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
}

View file

@ -24,6 +24,10 @@ abstract class SecuritySettingsViewModelBase with Store {
@computed
bool get useTotp2FA => _settingsStore.useTOTP2FA;
@computed
bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings =>
_settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
@computed
PinCodeRequiredDuration get pinCodeRequiredDuration => _settingsStore.pinTimeOutDuration;

View file

@ -27,6 +27,14 @@ abstract class WalletListViewModelBase with Store {
@observable
ObservableList<WalletListItem> wallets;
@computed
bool get shouldRequireTOTP2FAForAccessingWallet =>
_appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
@computed
bool get shouldRequireTOTP2FAForCreatingNewWallets =>
_appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets;
final AppStore _appStore;
final Box<WalletInfo> _walletInfoSource;
final WalletLoadingService _walletLoadingService;
@ -38,7 +46,6 @@ abstract class WalletListViewModelBase with Store {
Future<void> loadWallet(WalletListItem walletItem) async {
final wallet =
await _walletLoadingService.load(walletItem.type, walletItem.name);
_appStore.changeCurrentWallet(wallet);
}

View file

@ -12,6 +12,7 @@ import devicelocale
import flutter_secure_storage_macos
import in_app_review
import package_info
import package_info_plus
import path_provider_foundation
import platform_device_id
import platform_device_id_macos
@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin"))
PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin"))

View file

@ -630,6 +630,18 @@
"setup_totp_recommended": "إعداد TOTP (موصى به)",
"disable_buy": "تعطيل إجراء الشراء",
"disable_sell": "قم بتعطيل إجراء البيع",
"cake_2fa_preset" : " كعكة 2FA مسبقا",
"narrow": "ضيق",
"normal": "طبيعي",
"aggressive": "عنيف",
"require_for_assessing_wallet": "تتطلب الوصول إلى المحفظة",
"require_for_sends_to_non_contacts" : "تتطلب لارسال لغير جهات الاتصال",
"require_for_sends_to_contacts" : "تتطلب لارسال جهات الاتصال",
"require_for_sends_to_internal_wallets" : "تتطلب عمليات الإرسال إلى المحافظ الداخلية",
"require_for_exchanges_to_internal_wallets" : "تتطلب عمليات التبادل إلى المحافظ الداخلية",
"require_for_adding_contacts" : "تتطلب إضافة جهات اتصال",
"require_for_creating_new_wallets" : "تتطلب إنشاء محافظ جديدة",
"require_for_all_security_and_backup_settings" : "مطلوب لجميع إعدادات الأمان والنسخ الاحتياطي",
"available_balance_description": "الرصيد المتاح هو الرصيد الذي يمكنك إنفاقه أو تحويله إلى محفظة أخرى. يتم تجميد الرصيد المتاح للمعاملات الصادرة والمعاملات الواردة غير المؤكدة.",
"syncing_wallet_alert_title": "محفظتك تتم مزامنتها",
"syncing_wallet_alert_content": "قد لا يكتمل رصيدك وقائمة المعاملات الخاصة بك حتى تظهر عبارة “SYNCHRONIZED“ في الأعلى. انقر / اضغط لمعرفة المزيد.",

View file

@ -626,6 +626,18 @@
"setup_totp_recommended": "Настройка на TOTP (препоръчително)",
"disable_buy": "Деактивирайте действието за покупка",
"disable_sell": "Деактивирайте действието за продажба",
"cake_2fa_preset" : "Торта 2FA Preset",
"narrow": "Тесен",
"normal": "нормално",
"aggressive": "Прекалено усърден",
"require_for_assessing_wallet": "Изискване за достъп до портфейла",
"require_for_sends_to_non_contacts" : "Изискване за изпращане до лица без контакт",
"require_for_sends_to_contacts" : "Изискване за изпращане до контакти",
"require_for_sends_to_internal_wallets" : "Изискване за изпращане до вътрешни портфейли",
"require_for_exchanges_to_internal_wallets" : "Изискване за обмен към вътрешни портфейли",
"require_for_adding_contacts" : "Изисква се за добавяне на контакти",
"require_for_creating_new_wallets" : "Изискване за създаване на нови портфейли",
"require_for_all_security_and_backup_settings" : "Изисква се за всички настройки за сигурност и архивиране",
"available_balance_description": "Това е балансът, който можете да използвате за покупка на криптовалути. Това не включва замразените средства.",
"syncing_wallet_alert_title": "Вашият портфейл се синхронизира",
"syncing_wallet_alert_content": "Списъкът ви с баланс и транзакции може да не е пълен, докато в горната част не пише „СИНХРОНИЗИРАН“. Кликнете/докоснете, за да научите повече.",

View file

@ -626,6 +626,18 @@
"setup_totp_recommended": "Nastavit TOTP (doporučeno)",
"disable_buy": "Zakázat akci nákupu",
"disable_sell": "Zakázat akci prodeje",
"cake_2fa_preset" : "Předvolba Cake 2FA",
"narrow": "Úzký",
"normal": "Normální",
"aggressive": "Agresivní",
"require_for_assessing_wallet": "Vyžadovat pro přístup k peněžence",
"require_for_sends_to_non_contacts" : "Vyžadovat pro odesílání nekontaktním osobám",
"require_for_sends_to_contacts" : "Vyžadovat pro odeslání kontaktům",
"require_for_sends_to_internal_wallets" : "Vyžadovat pro odesílání do interních peněženek",
"require_for_exchanges_to_internal_wallets" : "Vyžadovat pro výměny do interních peněženek",
"require_for_adding_contacts" : "Vyžadovat pro přidání kontaktů",
"require_for_creating_new_wallets" : "Vyžadovat pro vytváření nových peněženek",
"require_for_all_security_and_backup_settings" : "Vyžadovat všechna nastavení zabezpečení a zálohování",
"available_balance_description": "Dostupná částka je částka, kterou můžete okamžitě utratit. Zmrazená částka je částka, která ještě není k dispozici, protože ještě nebyla potvrzena síťovým protokolem.",
"syncing_wallet_alert_title": "Vaše peněženka se synchronizuje",
"syncing_wallet_alert_content": "Váš seznam zůstatků a transakcí nemusí být úplný, dokud nebude nahoře uvedeno „SYNCHRONIZOVANÉ“. Kliknutím/klepnutím se dozvíte více.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "TOTP einrichten (empfohlen)",
"disable_buy": "Kaufaktion deaktivieren",
"disable_sell": "Verkaufsaktion deaktivieren",
"cake_2fa_preset" : "Kuchen 2FA-Voreinstellung",
"narrow": "Eng",
"normal": "Normal",
"aggressive": "Übereifrig",
"require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich",
"require_for_sends_to_non_contacts" : "Erforderlich für Versendungen an Nichtkontakte",
"require_for_sends_to_contacts" : "Erforderlich für Versendungen an Kontakte",
"require_for_sends_to_internal_wallets" : "Erforderlich für Sendungen an interne Wallets",
"require_for_exchanges_to_internal_wallets" : "Erforderlich für den Umtausch in interne Wallets",
"require_for_adding_contacts" : "Erforderlich zum Hinzufügen von Kontakten",
"require_for_creating_new_wallets" : "Erforderlich zum Erstellen neuer Wallets",
"require_for_all_security_and_backup_settings" : "Für alle Sicherheits- und Sicherungseinstellungen erforderlich",
"available_balance_description": "Verfügbarer Saldo ist der Betrag, den Sie sofort ausgeben können. Dieser Betrag kann sich ändern, wenn Sie eine Transaktion senden oder empfangen.",
"syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert",
"syncing_wallet_alert_content": "Ihr Kontostand und Ihre Transaktionsliste sind möglicherweise erst vollständig, wenn oben „SYNCHRONISIERT“ steht. Klicken/tippen Sie, um mehr zu erfahren.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Set up TOTP (Recommended)",
"disable_buy": "Disable buy action",
"disable_sell": "Disable sell action",
"cake_2fa_preset" : "Cake 2FA Preset",
"narrow": "Narrow",
"normal": "Normal",
"aggressive": "Aggressive",
"require_for_assessing_wallet": "Require for accessing wallet",
"require_for_sends_to_non_contacts" : "Require for sends to non-contacts",
"require_for_sends_to_contacts" : "Require for sends to contacts",
"require_for_sends_to_internal_wallets" : "Require for sends to internal wallets",
"require_for_exchanges_to_internal_wallets" : "Require for exchanges to internal wallets",
"require_for_adding_contacts" : "Require for adding contacts",
"require_for_creating_new_wallets" : "Require for creating new wallets",
"require_for_all_security_and_backup_settings" : "Require for all security and backup settings",
"available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.",
"syncing_wallet_alert_title": "Your wallet is syncing",
"syncing_wallet_alert_content": "Your balance and transaction list may not be complete until it says “SYNCHRONIZED” at the top. Click/tap to learn more.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Configurar TOTP (Recomendado)",
"disable_buy": "Desactivar acción de compra",
"disable_sell": "Desactivar acción de venta",
"cake_2fa_preset" : "Pastel 2FA preestablecido",
"narrow": "Angosto",
"normal": "Normal",
"aggressive": "Demasiado entusiasta",
"require_for_assessing_wallet": "Requerido para acceder a la billetera",
"require_for_sends_to_non_contacts" : "Requerido para envíos a no contactos",
"require_for_sends_to_contacts" : "Requerir para envíos a contactos",
"require_for_sends_to_internal_wallets" : "Requerido para envíos a billeteras internas",
"require_for_exchanges_to_internal_wallets" : "Requerido para intercambios a billeteras internas",
"require_for_adding_contacts" : "Requerido para agregar contactos",
"require_for_creating_new_wallets" : "Requerido para crear nuevas billeteras",
"require_for_all_security_and_backup_settings" : "Requerido para todas las configuraciones de seguridad y copia de seguridad",
"available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.",
"syncing_wallet_alert_title": "Tu billetera se está sincronizando",
"syncing_wallet_alert_content": "Es posible que su lista de saldo y transacciones no esté completa hasta que diga \"SINCRONIZADO\" en la parte superior. Haga clic/toque para obtener más información.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Configurer TOTP (recommandé)",
"disable_buy": "Désactiver l'action d'achat",
"disable_sell": "Désactiver l'action de vente",
"cake_2fa_preset" : "Gâteau 2FA prédéfini",
"narrow": "Étroit",
"normal": "Normal",
"aggressive": "Trop zélé",
"require_for_assessing_wallet": "Nécessaire pour accéder au portefeuille",
"require_for_sends_to_non_contacts" : "Exiger pour les envois à des non-contacts",
"require_for_sends_to_contacts" : "Exiger pour les envois aux contacts",
"require_for_sends_to_internal_wallets" : "Exiger pour les envois vers des portefeuilles internes",
"require_for_exchanges_to_internal_wallets" : "Exiger pour les échanges vers des portefeuilles internes",
"require_for_adding_contacts" : "Requis pour ajouter des contacts",
"require_for_creating_new_wallets" : "Nécessaire pour créer de nouveaux portefeuilles",
"require_for_all_security_and_backup_settings" : "Exiger pour tous les paramètres de sécurité et de sauvegarde",
"available_balance_description": "Le solde disponible est le montant que vous pouvez dépenser immédiatement. Il est calculé en soustrayant le solde gelé du solde total.",
"syncing_wallet_alert_title": "Votre portefeuille est en cours de synchronisation",
"syncing_wallet_alert_content": "Votre solde et votre liste de transactions peuvent ne pas être complets tant qu'il n'y a pas « SYNCHRONISÉ » en haut. Cliquez/appuyez pour en savoir plus.",

View file

@ -612,6 +612,18 @@
"prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi",
"disable_buy": "Kashe alama",
"disable_sell": "Kashe karbuwa",
"cake_2fa_preset" : "Cake 2FA saiti",
"narrow": "kunkuntar",
"normal": "Na al'ada",
"aggressive": "Mai tsananin kishi",
"require_for_assessing_wallet": "Bukatar samun damar walat",
"require_for_sends_to_non_contacts" : "Bukatar aika zuwa waɗanda ba lambobin sadarwa ba",
"require_for_sends_to_contacts" : "Bukatar aika zuwa lambobin sadarwa",
"require_for_sends_to_internal_wallets" : "Bukatar aika zuwa wallet na ciki",
"require_for_exchanges_to_internal_wallets" : "Bukatar musanya zuwa wallet na ciki",
"require_for_adding_contacts" : "Bukatar ƙara lambobin sadarwa",
"require_for_creating_new_wallets" : "Bukatar ƙirƙirar sabbin wallet",
"require_for_all_security_and_backup_settings" : "Bukatar duk tsaro da saitunan wariyar ajiya",
"available_balance_description": "Ma'auni mai samuwa” ko ”,Tabbataccen Ma'auni”, kudade ne da za a iya kashewa nan da nan. Idan kudade sun bayyana a cikin ƙananan ma'auni amma ba babban ma'auni ba, to dole ne ku jira 'yan mintoci kaɗan don kudaden shiga don samun ƙarin tabbaci na hanyar sadarwa. Bayan sun sami ƙarin tabbaci, za a kashe su.",
"syncing_wallet_alert_title": "Walat ɗin ku yana aiki tare",
"syncing_wallet_alert_content": "Ma'aunin ku da lissafin ma'amala bazai cika ba har sai an ce \"SYNCHRONIZED\" a saman. Danna/matsa don ƙarin koyo.",
@ -621,4 +633,3 @@
"slidable": "Mai iya zamewa",
"template_name": "Sunan Samfura"
}

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "टीओटीपी सेट अप करें (अनुशंसित)",
"disable_buy": "खरीद कार्रवाई अक्षम करें",
"disable_sell": "बेचने की कार्रवाई अक्षम करें",
"cake_2fa_preset" : "केक 2एफए प्रीसेट",
"narrow": "सँकरा",
"normal": "सामान्य",
"aggressive": "ज्यादा",
"require_for_assessing_wallet": "वॉलेट तक पहुँचने के लिए आवश्यकता है",
"require_for_sends_to_non_contacts" : "गैर-संपर्कों को भेजने की आवश्यकता",
"require_for_sends_to_contacts" : "संपर्कों को भेजने के लिए आवश्यक है",
"require_for_sends_to_internal_wallets" : "आंतरिक वॉलेट में भेजने की आवश्यकता है",
"require_for_exchanges_to_internal_wallets" : "आंतरिक वॉलेट में आदान-प्रदान की आवश्यकता है",
"require_for_adding_contacts" : "संपर्क जोड़ने के लिए आवश्यकता है",
"require_for_creating_new_wallets" : "नए वॉलेट बनाने की आवश्यकता है",
"require_for_all_security_and_backup_settings" : "सभी सुरक्षा और बैकअप सेटिंग्स की आवश्यकता है",
"available_balance_description": "उपलब्ध शेष या ”पुष्टिकृत शेष”, वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड निचले बैलेंस में दिखाई देते हैं, लेकिन शीर्ष बैलेंस में नहीं, तो आपको आने वाले फंड के लिए अधिक नेटवर्क पुष्टिकरण प्राप्त करने के लिए कुछ मिनट इंतजार करना होगा। अधिक पुष्टि मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।",
"syncing_wallet_alert_title": "आपका वॉलेट सिंक हो रहा है",
"syncing_wallet_alert_content": "आपकी शेष राशि और लेनदेन सूची तब तक पूरी नहीं हो सकती जब तक कि शीर्ष पर \"सिंक्रनाइज़्ड\" न लिखा हो। अधिक जानने के लिए क्लिक/टैप करें।",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Postavite TOTP (preporučeno)",
"disable_buy": "Onemogući kupnju",
"disable_sell": "Onemogući akciju prodaje",
"cake_2fa_preset" : "Cake 2FA Preset",
"narrow": "Usko",
"normal": "Normalno",
"aggressive": "Preterano",
"require_for_assessing_wallet": "Potreban za pristup novčaniku",
"require_for_sends_to_non_contacts" : "Zahtijeva za slanje nekontaktima",
"require_for_sends_to_contacts" : "Zahtijeva za slanje kontaktima",
"require_for_sends_to_internal_wallets" : "Zahtijeva za slanje u interne novčanike",
"require_for_exchanges_to_internal_wallets" : "Potreban za razmjenu na interne novčanike",
"require_for_adding_contacts" : "Zahtijeva za dodavanje kontakata",
"require_for_creating_new_wallets" : "Potreban za kreiranje novih novčanika",
"require_for_all_security_and_backup_settings" : "Zahtijeva za sve postavke sigurnosti i sigurnosne kopije",
"available_balance_description": "Dostupno stanje je iznos koji možete potrošiti. To je vaš saldo minus bilo kakve transakcije koje su još uvijek u tijeku.",
"syncing_wallet_alert_title": "Vaš novčanik se sinkronizira",
"syncing_wallet_alert_content": "Vaš saldo i popis transakcija možda neće biti potpuni sve dok na vrhu ne piše \"SINKRONIZIRANO\". Kliknite/dodirnite da biste saznali više.",

View file

@ -622,6 +622,18 @@
"setup_totp_recommended": "Siapkan TOTP (Disarankan)",
"disable_buy": "Nonaktifkan tindakan beli",
"disable_sell": "Nonaktifkan aksi jual",
"cake_2fa_preset" : "Preset Kue 2FA",
"narrow": "Sempit",
"normal": "Normal",
"aggressive": "Terlalu bersemangat",
"require_for_assessing_wallet": "Diperlukan untuk mengakses dompet",
"require_for_sends_to_non_contacts" : "Wajibkan untuk mengirim ke non-kontak",
"require_for_sends_to_contacts" : "Membutuhkan untuk mengirim ke kontak",
"require_for_sends_to_internal_wallets" : "Diperlukan untuk mengirim ke dompet internal",
"require_for_exchanges_to_internal_wallets" : "Diperlukan untuk pertukaran ke dompet internal",
"require_for_adding_contacts" : "Membutuhkan untuk menambahkan kontak",
"require_for_creating_new_wallets" : "Diperlukan untuk membuat dompet baru",
"require_for_all_security_and_backup_settings" : "Memerlukan untuk semua pengaturan keamanan dan pencadangan",
"available_balance_description": "“Saldo yang Tersedia” atau “Saldo yang Dikonfirmasi” adalah dana yang dapat langsung dibelanjakan. Jika dana muncul di saldo bawah tetapi tidak di saldo atas, maka Anda harus menunggu beberapa menit agar dana masuk mendapatkan konfirmasi jaringan lainnya. Setelah mereka mendapatkan lebih banyak konfirmasi, mereka akan dapat dibelanjakan.",
"syncing_wallet_alert_title": "Dompet Anda sedang disinkronkan",
"syncing_wallet_alert_content": "Saldo dan daftar transaksi Anda mungkin belum lengkap sampai tertulis “SYNCHRONIZED” di bagian atas. Klik/ketuk untuk mempelajari lebih lanjut.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Imposta TOTP (consigliato)",
"disable_buy": "Disabilita l'azione di acquisto",
"disable_sell": "Disabilita l'azione di vendita",
"cake_2fa_preset" : "Torta 2FA Preset",
"narrow": "Stretto",
"normal": "Normale",
"aggressive": "Fervente",
"require_for_assessing_wallet": "Richiesto per l'accesso al portafoglio",
"require_for_sends_to_non_contacts" : "Richiesto per invii a non contatti",
"require_for_sends_to_contacts" : "Richiedi per gli invii ai contatti",
"require_for_sends_to_internal_wallets" : "Richiesto per invii a portafogli interni",
"require_for_exchanges_to_internal_wallets" : "Richiedi per gli scambi ai portafogli interni",
"require_for_adding_contacts" : "Richiesto per l'aggiunta di contatti",
"require_for_creating_new_wallets" : "Richiesto per la creazione di nuovi portafogli",
"require_for_all_security_and_backup_settings" : "Richiedi per tutte le impostazioni di sicurezza e backup",
"available_balance_description": "Il saldo disponibile è il saldo totale meno i fondi congelati. I fondi congelati sono fondi che sono stati inviati ma non sono ancora stati confermati.",
"syncing_wallet_alert_title": "Il tuo portafoglio si sta sincronizzando",
"syncing_wallet_alert_content": "Il saldo e l'elenco delle transazioni potrebbero non essere completi fino a quando non viene visualizzato \"SYNCHRONIZED\" in alto. Clicca/tocca per saperne di più.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "TOTP を設定する (推奨)",
"disable_buy": "購入アクションを無効にする",
"disable_sell": "販売アクションを無効にする",
"cake_2fa_preset" : "ケーキ 2FA プリセット",
"narrow": "狭い",
"normal": "普通",
"aggressive": "熱心すぎる",
"require_for_assessing_wallet": "ウォレットにアクセスするために必要です",
"require_for_sends_to_non_contacts" : "非連絡先への送信に必須",
"require_for_sends_to_contacts" : "連絡先に送信する場合に必須",
"require_for_sends_to_internal_wallets" : "内部ウォレットへの送信に必須",
"require_for_exchanges_to_internal_wallets" : "内部ウォレットへの交換に必要",
"require_for_adding_contacts" : "連絡先の追加に必要",
"require_for_creating_new_wallets" : "新しいウォレットを作成するために必要です",
"require_for_all_security_and_backup_settings" : "すべてのセキュリティおよびバックアップ設定に必須",
"available_balance_description": "利用可能な残高は、ウォレットの残高から冷凍残高を差し引いたものです。",
"syncing_wallet_alert_title": "ウォレットは同期中です",
"syncing_wallet_alert_content": "上部に「同期済み」と表示されるまで、残高と取引リストが完了していない可能性があります。詳細については、クリック/タップしてください。",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "TOTP 설정(권장)",
"disable_buy": "구매 행동 비활성화",
"disable_sell": "판매 조치 비활성화",
"cake_2fa_preset" : "케이크 2FA 프리셋",
"narrow": "좁은",
"normal": "정상",
"aggressive": "지나치게 열심인",
"require_for_assessing_wallet": "지갑 접근을 위해 필요",
"require_for_sends_to_non_contacts" : "비접촉자에게 보내는 데 필요",
"require_for_sends_to_contacts" : "연락처로 보내기에 필요",
"require_for_sends_to_internal_wallets" : "내부 지갑으로 보내는 데 필요",
"require_for_exchanges_to_internal_wallets" : "내부 지갑으로의 교환에 필요",
"require_for_adding_contacts" : "연락처 추가에 필요",
"require_for_creating_new_wallets" : "새 지갑 생성에 필요",
"require_for_all_security_and_backup_settings" : "모든 보안 및 백업 설정에 필요",
"available_balance_description": "이 지갑에서 사용할 수 있는 잔액입니다. 이 잔액은 블록체인에서 가져온 것이며, Cake Wallet이 사용할 수 없습니다.",
"syncing_wallet_alert_title": "지갑 동기화 중",
"syncing_wallet_alert_content": "상단에 \"동기화됨\"이라고 표시될 때까지 잔액 및 거래 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "TOTP ကို ​​စနစ်ထည့်သွင်းပါ (အကြံပြုထားသည်)",
"disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။",
"disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။",
"cake_2fa_preset" : "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်",
"narrow": "ကျဉ်းသော",
"normal": "ပုံမှန်",
"aggressive": "စိတ်အားထက်သန်ခြင်း။",
"require_for_assessing_wallet": "ပိုက်ဆံအိတ်ကို ဝင်သုံးရန် လိုအပ်သည်။",
"require_for_sends_to_non_contacts" : "အဆက်အသွယ်မရှိသူများထံ ပေးပို့ရန် လိုအပ်သည်။",
"require_for_sends_to_contacts" : "အဆက်အသွယ်များထံ ပေးပို့ရန် လိုအပ်သည်။",
"require_for_sends_to_internal_wallets" : "အတွင်းပိုင်း ပိုက်ဆံအိတ်များသို့ ပေးပို့ရန် လိုအပ်သည်။",
"require_for_exchanges_to_internal_wallets" : "အတွင်းပိုင်းပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။",
"require_for_adding_contacts" : "အဆက်အသွယ်များထည့်ရန် လိုအပ်သည်။",
"require_for_creating_new_wallets" : "ပိုက်ဆံအိတ်အသစ်များ ဖန်တီးရန် လိုအပ်သည်။",
"require_for_all_security_and_backup_settings" : "လုံခြုံရေးနှင့် အရန်ဆက်တင်များအားလုံးအတွက် လိုအပ်ပါသည်။",
"available_balance_description": "သင့်ရဲ့ အကောင့်တွင် ရရှိနိုင်သော ငွေကျန်ငွေကို ပြန်လည်ပေးသွင်းပါ။",
"syncing_wallet_alert_title": "သင့်ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်နေပါသည်။",
"syncing_wallet_alert_content": "သင်၏လက်ကျန်နှင့် ငွေပေးငွေယူစာရင်းသည် ထိပ်တွင် \"Synchronizeed\" ဟုပြောသည်အထိ မပြီးမြောက်နိုင်ပါ။ ပိုမိုလေ့လာရန် နှိပ်/နှိပ်ပါ။",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "TOTP instellen (aanbevolen)",
"disable_buy": "Koopactie uitschakelen",
"disable_sell": "Verkoopactie uitschakelen",
"cake_2fa_preset" : "Taart 2FA Voorinstelling",
"narrow": "Smal",
"normal": "Normaal",
"aggressive": "Overijverig",
"require_for_assessing_wallet": "Vereist voor toegang tot portemonnee",
"require_for_sends_to_non_contacts" : "Vereist voor verzendingen naar niet-contacten",
"require_for_sends_to_contacts" : "Vereist voor verzending naar contacten",
"require_for_sends_to_internal_wallets" : "Vereist voor verzendingen naar interne portefeuilles",
"require_for_exchanges_to_internal_wallets" : "Vereist voor uitwisselingen naar interne portefeuilles",
"require_for_adding_contacts" : "Vereist voor het toevoegen van contacten",
"require_for_creating_new_wallets" : "Vereist voor het maken van nieuwe portefeuilles",
"require_for_all_security_and_backup_settings" : "Vereist voor alle beveiligings- en back-upinstellingen",
"available_balance_description": "Beschikbaar saldo is het saldo dat u kunt uitgeven. Het kan lager zijn dan uw totale saldo als u onlangs geld hebt verzonden.",
"syncing_wallet_alert_title": "Uw portemonnee wordt gesynchroniseerd",
"syncing_wallet_alert_content": "Uw saldo- en transactielijst is mogelijk pas compleet als er bovenaan 'GESYNCHRONISEERD' staat. Klik/tik voor meer informatie.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Skonfiguruj TOTP (zalecane)",
"disable_buy": "Wyłącz akcję kupna",
"disable_sell": "Wyłącz akcję sprzedaży",
"cake_2fa_preset" : "Ciasto 2FA Preset",
"narrow": "Wąski",
"normal": "Normalna",
"aggressive": "Nadgorliwy",
"require_for_assessing_wallet": "Wymagaj dostępu do portfela",
"require_for_sends_to_non_contacts" : "Wymagaj wysyłania do osób niekontaktowych",
"require_for_sends_to_contacts" : "Wymagaj wysyłania do kontaktów",
"require_for_sends_to_internal_wallets" : "Wymagaj wysyłania do portfeli wewnętrznych",
"require_for_exchanges_to_internal_wallets" : "Wymagaj wymiany do portfeli wewnętrznych",
"require_for_adding_contacts" : "Wymagane do dodania kontaktów",
"require_for_creating_new_wallets" : "Wymagane do tworzenia nowych portfeli",
"require_for_all_security_and_backup_settings" : "Wymagaj dla wszystkich ustawień zabezpieczeń i kopii zapasowych",
"available_balance_description": "Dostępne saldo jest równoważne z saldem portfela minus zamrożone saldo.",
"syncing_wallet_alert_title": "Twój portfel się synchronizuje",
"syncing_wallet_alert_content": "Twoje saldo i lista transakcji mogą nie być kompletne, dopóki u góry nie pojawi się napis „SYNCHRONIZOWANY”. Kliknij/stuknij, aby dowiedzieć się więcej.",

View file

@ -631,6 +631,18 @@
"setup_totp_recommended": "Configurar TOTP (recomendado)",
"disable_buy": "Desativar ação de compra",
"disable_sell": "Desativar ação de venda",
"cake_2fa_preset" : "Predefinição de bolo 2FA",
"narrow": "Estreito",
"normal": "Normal",
"aggressive": "excessivamente zeloso",
"require_for_assessing_wallet": "Requer para acessar a carteira",
"require_for_sends_to_non_contacts" : "Exigir para envios para não-contatos",
"require_for_sends_to_contacts" : "Exigir para envios para contatos",
"require_for_sends_to_internal_wallets" : "Exigir envios para carteiras internas",
"require_for_exchanges_to_internal_wallets" : "Requer trocas para carteiras internas",
"require_for_adding_contacts" : "Requer para adicionar contatos",
"require_for_creating_new_wallets" : "Requer para criar novas carteiras",
"require_for_all_security_and_backup_settings" : "Exigir todas as configurações de segurança e backup",
"available_balance_description": "Seu saldo disponível é o saldo total menos o saldo congelado. O saldo congelado é o saldo que você não pode gastar, mas que ainda não foi confirmado na blockchain. O saldo congelado é geralmente o resultado de transações recentes.",
"syncing_wallet_alert_title": "Sua carteira está sincronizando",
"syncing_wallet_alert_content": "Seu saldo e lista de transações podem não estar completos até que diga “SYNCHRONIZED” no topo. Clique/toque para saber mais.",

View file

@ -633,6 +633,18 @@
"setup_totp_recommended": "Настроить TOTP (рекомендуется)",
"disable_buy": "Отключить действие покупки",
"disable_sell": "Отключить действие продажи",
"cake_2fa_preset" : "Торт 2FA Preset",
"narrow": "Узкий",
"normal": "Нормальный",
"aggressive": "чрезмерно усердный",
"require_for_assessing_wallet": "Требовать для доступа к кошельку",
"require_for_sends_to_non_contacts" : "Требовать для отправки не контактам",
"require_for_sends_to_contacts" : "Требовать для отправки контактам",
"require_for_sends_to_internal_wallets" : "Требовать отправки на внутренние кошельки",
"require_for_exchanges_to_internal_wallets" : "Требовать для обмена на внутренние кошельки",
"require_for_adding_contacts" : "Требовать добавления контактов",
"require_for_creating_new_wallets" : "Требовать для создания новых кошельков",
"require_for_all_security_and_backup_settings" : "Требовать все настройки безопасности и резервного копирования",
"available_balance_description": "Доступный баланс - это средства, которые вы можете использовать для покупки или продажи криптовалюты.",
"syncing_wallet_alert_title": "Ваш кошелек синхронизируется",
"syncing_wallet_alert_content": "Ваш баланс и список транзакций могут быть неполными, пока вверху не будет написано «СИНХРОНИЗИРОВАНО». Щелкните/коснитесь, чтобы узнать больше.",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "ตั้งค่า TOTP (แนะนำ)",
"disable_buy": "ปิดการใช้งานการซื้อ",
"disable_sell": "ปิดการใช้งานการขาย",
"cake_2fa_preset" : "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า",
"narrow": "แคบ",
"normal": "ปกติ",
"aggressive": "กระตือรือร้นมากเกินไป",
"require_for_assessing_wallet": "จำเป็นสำหรับการเข้าถึงกระเป๋าเงิน",
"require_for_sends_to_non_contacts" : "จำเป็นต้องส่งไปยังผู้ที่ไม่ได้ติดต่อ",
"require_for_sends_to_contacts" : "จำเป็นต้องส่งไปยังผู้ติดต่อ",
"require_for_sends_to_internal_wallets" : "จำเป็นต้องส่งไปยังกระเป๋าเงินภายใน",
"require_for_exchanges_to_internal_wallets" : "ต้องการการแลกเปลี่ยนไปยังกระเป๋าเงินภายใน",
"require_for_adding_contacts" : "ต้องการสำหรับการเพิ่มผู้ติดต่อ",
"require_for_creating_new_wallets" : "จำเป็นสำหรับการสร้างกระเป๋าเงินใหม่",
"require_for_all_security_and_backup_settings" : "จำเป็นสำหรับการตั้งค่าความปลอดภัยและการสำรองข้อมูลทั้งหมด",
"available_balance_description": "จำนวนเงินที่คุณสามารถใช้ได้ในการซื้อหรือขาย",
"syncing_wallet_alert_title": "กระเป๋าสตางค์ของคุณกำลังซิงค์",
"syncing_wallet_alert_content": "รายการยอดเงินและธุรกรรมของคุณอาจไม่สมบูรณ์จนกว่าจะมีข้อความว่า “ซิงโครไนซ์” ที่ด้านบน คลิก/แตะเพื่อเรียนรู้เพิ่มเติม่",

View file

@ -631,6 +631,19 @@
"setup_2fa_text": "Cake 2FA, soğuk hava deposu kadar güvenli DEĞİLDİR. 2FA, siz uyurken arkadaşınızın parmak izinizi sağlaması gibi temel saldırı türlerine karşı koruma sağlar.\n\n Cake 2FA, gelişmiş bir saldırgan tarafından güvenliği ihlal edilmiş bir cihaza karşı koruma SAĞLAMAZ.\n\n 2FA kodlarınıza erişimi kaybederseniz , BU CÜZDANA ERİŞİMİNİZİ KAYBEDECEKSİNİZ. Mnemonic seed'den cüzdanınızı geri yüklemeniz gerekecek. BU NEDENLE HATIRLAYICI TOHUMLARINIZI YEDEKLEMELİSİNİZ! Ayrıca anımsatıcı tohumlarınıza erişimi olan biri, Cake 2FA'yı atlayarak paranızı çalabilir.\n\n Cake, anımsatıcı tohumlarınıza erişimi kaybederseniz size yardımcı olamaz, çünkü Cake bir saklama dışı cüzdan.",
"setup_totp_recommended": "TOTP'yi kurun (Önerilir)",
"disable_buy": "Satın alma işlemini devre dışı bırak",
"disable_sell": "Satış işlemini devre dışı bırak",
"cake_2fa_preset" : "Kek 2FA Ön Ayarı",
"narrow": "Dar",
"normal": "Normal",
"aggressive": "Aşırı duyarlı",
"require_for_assessing_wallet": "Cüzdana erişmek için gerekli",
"require_for_sends_to_non_contacts" : "Kişi olmayan kişilere göndermeler için gerekli kıl",
"require_for_sends_to_contacts" : "Kişilere göndermeler için gerekli kıl",
"require_for_sends_to_internal_wallets" : "Dahili cüzdanlara yapılan gönderimler için gereklilik",
"require_for_exchanges_to_internal_wallets" : "Dahili cüzdanlara değişim gerektir",
"require_for_adding_contacts" : "Kişi eklemek için gerekli",
"require_for_creating_new_wallets" : "Yeni cüzdan oluşturmak için gerekli",
"require_for_all_security_and_backup_settings" : "Tüm güvenlik ve yedekleme ayarları için iste",
"disable_sell": "Satış işlemini devre dışı bırak",
"available_balance_description": "Bu, cüzdanınızda harcayabileceğiniz miktar. Bu miktar, cüzdanınızdan çekilebilecek toplam bakiyeden daha düşük olabilir, çünkü bazı fonlar henüz kullanılamaz durumda olabilir.",
"syncing_wallet_alert_title": "Cüzdanınız senkronize ediliyor",

View file

@ -632,6 +632,18 @@
"setup_totp_recommended": "Налаштувати TOTP (рекомендовано)",
"disable_buy": "Вимкнути дію покупки",
"disable_sell": "Вимкнути дію продажу",
"cake_2fa_preset" : "Торт 2FA Preset",
"narrow": "вузькі",
"normal": "нормальний",
"aggressive": "Надто старанний",
"require_for_assessing_wallet": "Потрібен доступ до гаманця",
"require_for_sends_to_non_contacts" : "Вимагати для надсилання неконтактним особам",
"require_for_sends_to_contacts" : "Вимагати для надсилання контактам",
"require_for_sends_to_internal_wallets" : "Вимагати надсилання на внутрішні гаманці",
"require_for_exchanges_to_internal_wallets" : "Вимагати обміну на внутрішні гаманці",
"require_for_adding_contacts" : "Потрібен для додавання контактів",
"require_for_creating_new_wallets" : "Потрібно для створення нових гаманців",
"require_for_all_security_and_backup_settings" : "Вимагати всіх налаштувань безпеки та резервного копіювання",
"available_balance_description": "Це сума, яку ви можете витратити, не включаючи невизначені кошти. Це може бути менше, ніж загальний баланс, якщо ви витратили кошти, які ще не підтверджені.",
"syncing_wallet_alert_title": "Ваш гаманець синхронізується",
"syncing_wallet_alert_content": "Ваш баланс та список транзакцій може бути неповним, доки вгорі не буде написано «СИНХРОНІЗОВАНО». Натисніть/торкніться, щоб дізнатися більше.",

View file

@ -626,6 +626,18 @@
"setup_totp_recommended": "TOTP ترتیب دیں (تجویز کردہ)",
"disable_buy": "خرید ایکشن کو غیر فعال کریں۔",
"disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔",
"cake_2fa_preset" : "کیک 2FA پیش سیٹ",
"narrow": "تنگ",
"normal": "نارمل",
"aggressive": "حد سے زیادہ پرجوش",
"require_for_assessing_wallet": "بٹوے تک رسائی کے لیے درکار ہے۔",
"require_for_sends_to_non_contacts" : "غیر رابطوں کو بھیجنے کی ضرورت ہے۔",
"require_for_sends_to_contacts" : "رابطوں کو بھیجنے کی ضرورت ہے۔",
"require_for_sends_to_internal_wallets" : "اندرونی بٹوے پر بھیجنے کے لیے درکار ہے۔",
"require_for_exchanges_to_internal_wallets" : "اندرونی بٹوے میں تبادلے کی ضرورت ہے۔",
"require_for_adding_contacts" : "رابطوں کو شامل کرنے کی ضرورت ہے۔",
"require_for_creating_new_wallets" : "نئے بٹوے بنانے کی ضرورت ہے۔",
"require_for_all_security_and_backup_settings" : "تمام سیکورٹی اور بیک اپ کی ترتیبات کے لیے درکار ہے۔",
"available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جو فوری طور پر خرچ کیے جا سکتے ہیں۔ اگر فنڈز کم بیلنس میں ظاہر ہوتے ہیں لیکن اوپر کے بیلنس میں نہیں، تو آپ کو مزید نیٹ ورک کی تصدیقات حاصل کرنے کے لیے آنے والے فنڈز کے لیے چند منٹ انتظار کرنا چاہیے۔ مزید تصدیق حاصل کرنے کے بعد، وہ قابل خرچ ہوں گے۔",
"syncing_wallet_alert_title": "آپ کا بٹوہ مطابقت پذیر ہو رہا ہے۔",
"syncing_wallet_alert_content": "آپ کے بیلنس اور لین دین کی فہرست اس وقت تک مکمل نہیں ہو سکتی جب تک کہ یہ سب سے اوپر \"SYNCRONIZED\" نہ کہے۔ مزید جاننے کے لیے کلک/تھپتھپائیں۔",

View file

@ -628,6 +628,18 @@
"setup_totp_recommended": "Sọ TOTP (Kẹṣọdọ)",
"disable_buy": "Ko iṣọrọ ọja",
"disable_sell": "Ko iṣọrọ iṣọrọ",
"cake_2fa_preset" : "Cake 2FA Tito",
"narrow": "Taara",
"normal": "Deede",
"aggressive": "Onítara",
"require_for_assessing_wallet": "Beere fun wiwọle si apamọwọ",
"require_for_sends_to_non_contacts" : "Beere fun fifiranṣẹ si awọn ti kii ṣe awọn olubasọrọ",
"require_for_sends_to_contacts" : "Beere fun fifiranṣẹ si awọn olubasọrọ",
"require_for_sends_to_internal_wallets" : "Beere fun fifiranṣẹ si awọn apamọwọ inu",
"require_for_exchanges_to_internal_wallets" : "Beere fun awọn paṣipaarọ si awọn apamọwọ inu",
"require_for_adding_contacts" : "Beere fun fifi awọn olubasọrọ kun",
"require_for_creating_new_wallets" : "Beere fun ṣiṣẹda titun Woleti",
"require_for_all_security_and_backup_settings" : "Beere fun gbogbo aabo ati awọn eto afẹyinti",
"available_balance_description": "“Iwọntunwọnsi Wa” tabi “Iwọntunwọnsi Ijẹrisi” jẹ awọn owo ti o le ṣee lo lẹsẹkẹsẹ. Ti awọn owo ba han ni iwọntunwọnsi kekere ṣugbọn kii ṣe iwọntunwọnsi oke, lẹhinna o gbọdọ duro iṣẹju diẹ fun awọn owo ti nwọle lati gba awọn ijẹrisi nẹtiwọọki diẹ sii. Lẹhin ti wọn gba awọn ijẹrisi diẹ sii, wọn yoo jẹ inawo.",
"syncing_wallet_alert_title": "Apamọwọ rẹ n muṣiṣẹpọ",
"syncing_wallet_alert_content": "Iwontunws.funfun rẹ ati atokọ idunadura le ma pari titi ti yoo fi sọ “SYNCHRONIZED” ni oke. Tẹ/tẹ ni kia kia lati ni imọ siwaju sii.",

View file

@ -631,6 +631,18 @@
"setup_totp_recommended": "设置 TOTP推荐",
"disable_buy": "禁用购买操作",
"disable_sell": "禁用卖出操作",
"cake_2fa_preset" : "蛋糕 2FA 预设",
"narrow": "狭窄的",
"normal": "普通的",
"aggressive": "过分热心",
"require_for_assessing_wallet": "需要访问钱包",
"require_for_sends_to_non_contacts" : "需要发送给非联系人",
"require_for_sends_to_contacts" : "需要发送给联系人",
"require_for_sends_to_internal_wallets" : "需要发送到内部钱包",
"require_for_exchanges_to_internal_wallets" : "需要兑换到内部钱包",
"require_for_adding_contacts" : "需要添加联系人",
"require_for_creating_new_wallets" : "创建新钱包的要求",
"require_for_all_security_and_backup_settings" : "需要所有安全和备份设置",
"available_balance_description": "可用余额是您可以使用的金额。冻结余额是您当前正在等待确认的金额。",
"syncing_wallet_alert_title": "您的钱包正在同步",
"syncing_wallet_alert_content": "您的余额和交易列表可能不完整,直到顶部显示“已同步”。单击/点击以了解更多信息。",