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>

Adegoke David committed Aug 4, 2023 at 14:49 UTC 412039412131fb92e9e141550f1bd5f519e4291b
52 files changed +1980 -838
.gitignore
+1
@@ -8,6 +8,7 @@
8 .buildlog/
9 .history
10 .svn/
11 +.fvm/
12
13 # IntelliJ related
14 *.iml
android/gradle.properties
+1 -1
@@ -1,4 +1,4 @@
1 org.gradle.jvmargs=-Xmx1536M
2 android.enableR8=true
3 android.useAndroidX=true
4 -android.enableJetifier=true
4 +android.enableJetifier=true
\ No newline at end of file
lib/core/auth_service.dart
+21 -12
@@ -25,6 +25,10 @@ class AuthService with Store {
25 Routes.setupPin,
26 Routes.setup_2faPage,
27 Routes.modify2FAPage,
28 + Routes.newWallet,
29 + Routes.newWalletType,
30 + Routes.addressBookAddContact,
31 + Routes.restoreOptions,
32 ];
33
34 final FlutterSecureStorage secureStorage;
@@ -81,21 +85,26 @@ class AuthService with Store {
85 }
86
87 Future<void> authenticateAction(BuildContext context,
84 - {Function(bool)? onAuthSuccess, String? route, Object? arguments}) async {
88 + {Function(bool)? onAuthSuccess,
89 + String? route,
90 + Object? arguments,
91 + required bool conditionToDetermineIfToUse2FA}) async {
92 assert(route != null || onAuthSuccess != null,
93 'Either route or onAuthSuccess param must be passed.');
94
88 - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) {
89 - if (onAuthSuccess != null) {
90 - onAuthSuccess(true);
91 - } else {
92 - Navigator.of(context).pushNamed(
93 - route ?? '',
94 - arguments: arguments,
95 - );
95 + if (!conditionToDetermineIfToUse2FA) {
96 + if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) {
97 + if (onAuthSuccess != null) {
98 + onAuthSuccess(true);
99 + } else {
100 + Navigator.of(context).pushNamed(
101 + route ?? '',
102 + arguments: arguments,
103 + );
104 + }
105 + return;
106 }
97 - return;
98 - }
107 +}
108
109
110 Navigator.of(context).pushNamed(Routes.auth,
@@ -104,7 +113,7 @@ class AuthService with Store {
113 onAuthSuccess?.call(false);
114 return;
115 } else {
107 - if (settingsStore.useTOTP2FA) {
116 + if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) {
117 auth.close(
118 route: Routes.totpAuthCodePage,
119 arguments: TotpAuthArgumentsModel(
lib/core/backup_service.dart
+172 -148
@@ -1,6 +1,7 @@
1 import 'dart:convert';
2 import 'dart:io';
3 import 'dart:typed_data';
4 +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
5 import 'package:cw_core/wallet_type.dart';
6 import 'package:flutter/foundation.dart';
7 import 'package:hive/hive.dart';
@@ -19,8 +20,8 @@ import 'package:cake_wallet/wallet_types.g.dart';
20 import 'package:cake_backup/backup.dart' as cake_backup;
21
22 class BackupService {
22 - BackupService(this._flutterSecureStorage, this._walletInfoSource,
23 - this._keyService, this._sharedPreferences)
23 + BackupService(
24 + this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences)
25 : _cipher = Cryptography.instance.chacha20Poly1305Aead(),
26 _correctWallets = <WalletInfo>[];
27
@@ -67,9 +68,8 @@ class BackupService {
68 }
69
70 @Deprecated('Use v2 instead')
70 - Future<Uint8List> _exportBackupV1(String password,
71 - {String nonce = secrets.backupSalt}) async
72 - => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.');
71 + Future<Uint8List> _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async =>
72 + throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.');
73
74 Future<Uint8List> _exportBackupV2(String password) async {
75 final zipEncoder = ZipFileEncoder();
@@ -112,8 +112,7 @@ class BackupService {
112 return await _encryptV2(content, password);
113 }
114
115 - Future<void> _importBackupV1(Uint8List data, String password,
116 - {required String nonce}) async {
115 + Future<void> _importBackupV1(Uint8List data, String password, {required String nonce}) async {
116 final appDir = await getApplicationDocumentsDirectory();
117 final decryptedData = await _decryptV1(data, password, nonce);
118 final zip = ZipDecoder().decodeBytes(decryptedData);
@@ -161,10 +160,8 @@ class BackupService {
160
161 Future<void> _verifyWallets() async {
162 final walletInfoSource = await _reloadHiveWalletInfoBox();
164 - _correctWallets = walletInfoSource
165 - .values
166 - .where((info) => availableWalletTypes.contains(info.type))
167 - .toList();
163 + _correctWallets =
164 + walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList();
165
166 if (_correctWallets.isEmpty) {
167 throw Exception('Correct wallets not detected');
@@ -191,14 +188,12 @@ class BackupService {
188 return;
189 }
190
194 - final data =
195 - json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
191 + final data = json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
192 String currentWalletName = data[PreferencesKey.currentWalletName] as String;
193 int currentWalletType = data[PreferencesKey.currentWalletType] as int;
194
195 final isCorrentCurrentWallet = _correctWallets
200 - .any((info) => info.name == currentWalletName &&
201 - info.type.index == currentWalletType);
196 + .any((info) => info.name == currentWalletName && info.type.index == currentWalletType);
197
198 if (!isCorrentCurrentWallet) {
199 currentWalletName = _correctWallets.first.name;
@@ -212,138 +207,173 @@ class BackupService {
207 final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
208 final disableBuy = data[PreferencesKey.disableBuyKey] as bool?;
209 final disableSell = data[PreferencesKey.disableSellKey] as bool?;
215 - final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
216 - final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?;
217 - final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?;
210 + final currentTransactionPriorityKeyLegacy =
211 + data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
212 + final allowBiometricalAuthentication =
213 + data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?;
214 + final currentBitcoinElectrumSererId =
215 + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?;
216 final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?;
217 final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?;
218 final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?;
219 final currentPinLength = data[PreferencesKey.currentPinLength] as int?;
220 final currentTheme = data[PreferencesKey.currentTheme] as int?;
221 final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?;
224 - final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?;
222 + final currentDefaultSettingsMigrationVersion =
223 + data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?;
224 final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?;
225 final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?;
227 -
228 - await _sharedPreferences.setString(PreferencesKey.currentWalletName,
229 - currentWalletName);
226 + final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?;
227 + final shouldRequireTOTP2FAForAccessingWallet =
228 + data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?;
229 + final shouldRequireTOTP2FAForSendsToContact =
230 + data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?;
231 + final shouldRequireTOTP2FAForSendsToNonContact =
232 + data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?;
233 + final shouldRequireTOTP2FAForSendsToInternalWallets =
234 + data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?;
235 + final shouldRequireTOTP2FAForExchangesToInternalWallets =
236 + data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?;
237 + final shouldRequireTOTP2FAForAddingContacts =
238 + data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?;
239 + final shouldRequireTOTP2FAForCreatingNewWallets =
240 + data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?;
241 + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
242 + data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?;
243 +
244 + await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
245
246 if (currentNodeId != null)
232 - await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey,
233 - currentNodeId);
247 + await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId);
248
249 if (currentBalanceDisplayMode != null)
236 - await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey,
237 - currentBalanceDisplayMode);
250 + await _sharedPreferences.setInt(
251 + PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode);
252
239 - await _sharedPreferences.setInt(PreferencesKey.currentWalletType,
240 - currentWalletType);
253 + await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType);
254
255 if (currentFiatCurrency != null)
243 - await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey,
244 - currentFiatCurrency);
256 + await _sharedPreferences.setString(
257 + PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency);
258
259 if (shouldSaveRecipientAddress != null)
260 await _sharedPreferences.setBool(
248 - PreferencesKey.shouldSaveRecipientAddressKey,
249 - shouldSaveRecipientAddress);
261 + PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress);
262
263 if (isAppSecure != null)
252 - await _sharedPreferences.setBool(
253 - PreferencesKey.isAppSecureKey,
254 - isAppSecure);
264 + await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
265
266 if (disableBuy != null)
257 - await _sharedPreferences.setBool(
258 - PreferencesKey.disableBuyKey,
259 - disableBuy);
267 + await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy);
268
269 if (disableSell != null)
262 - await _sharedPreferences.setBool(
263 - PreferencesKey.disableSellKey,
264 - disableSell);
270 + await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
271
272 if (currentTransactionPriorityKeyLegacy != null)
273 await _sharedPreferences.setInt(
268 - PreferencesKey.currentTransactionPriorityKeyLegacy,
269 - currentTransactionPriorityKeyLegacy);
274 + PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy);
275
276 if (allowBiometricalAuthentication != null)
277 await _sharedPreferences.setBool(
273 - PreferencesKey.allowBiometricalAuthenticationKey,
274 - allowBiometricalAuthentication);
278 + PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication);
279
280 if (currentBitcoinElectrumSererId != null)
281 await _sharedPreferences.setInt(
278 - PreferencesKey.currentBitcoinElectrumSererIdKey,
279 - currentBitcoinElectrumSererId);
282 + PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId);
283
284 if (currentLanguageCode != null)
282 - await _sharedPreferences.setString(PreferencesKey.currentLanguageCode,
283 - currentLanguageCode);
285 + await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode);
286
287 if (displayActionListMode != null)
286 - await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey,
287 - displayActionListMode);
288 + await _sharedPreferences.setInt(
289 + PreferencesKey.displayActionListModeKey, displayActionListMode);
290
291 if (fiatApiMode != null)
290 - await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey,
291 - fiatApiMode);
292 + await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
293
294 if (currentPinLength != null)
294 - await _sharedPreferences.setInt(PreferencesKey.currentPinLength,
295 - currentPinLength);
295 + await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength);
296
297 if (currentTheme != null)
298 - await _sharedPreferences.setInt(
299 - PreferencesKey.currentTheme, currentTheme);
298 + await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme);
299
300 if (exchangeStatus != null)
302 - await _sharedPreferences.setInt(
303 - PreferencesKey.exchangeStatusKey, exchangeStatus);
301 + await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus);
302
303 if (currentDefaultSettingsMigrationVersion != null)
306 - await _sharedPreferences.setInt(
307 - PreferencesKey.currentDefaultSettingsMigrationVersion,
308 - currentDefaultSettingsMigrationVersion);
304 + await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion,
305 + currentDefaultSettingsMigrationVersion);
306
307 if (moneroTransactionPriority != null)
311 - await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority,
312 - moneroTransactionPriority);
308 + await _sharedPreferences.setInt(
309 + PreferencesKey.moneroTransactionPriority, moneroTransactionPriority);
310
311 if (bitcoinTransactionPriority != null)
315 - await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority,
316 - bitcoinTransactionPriority);
312 + await _sharedPreferences.setInt(
313 + PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority);
314 +
315 + if (selectedCake2FAPreset != null)
316 + await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset);
317 +
318 + if (shouldRequireTOTP2FAForAccessingWallet != null)
319 + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet,
320 + shouldRequireTOTP2FAForAccessingWallet);
321 +
322 + if (shouldRequireTOTP2FAForSendsToContact != null)
323 + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact,
324 + shouldRequireTOTP2FAForSendsToContact);
325 +
326 + if (shouldRequireTOTP2FAForSendsToNonContact != null)
327 + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact,
328 + shouldRequireTOTP2FAForSendsToNonContact);
329 +
330 + if (shouldRequireTOTP2FAForSendsToInternalWallets != null)
331 + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets,
332 + shouldRequireTOTP2FAForSendsToInternalWallets);
333 +
334 + if (shouldRequireTOTP2FAForExchangesToInternalWallets != null)
335 + await _sharedPreferences.setBool(
336 + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets,
337 + shouldRequireTOTP2FAForExchangesToInternalWallets);
338 +
339 + if (shouldRequireTOTP2FAForAddingContacts != null)
340 + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts,
341 + shouldRequireTOTP2FAForAddingContacts);
342 +
343 + if (shouldRequireTOTP2FAForCreatingNewWallets != null)
344 + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets,
345 + shouldRequireTOTP2FAForCreatingNewWallets);
346 +
347 + if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null)
348 + await _sharedPreferences.setBool(
349 + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
350 + shouldRequireTOTP2FAForAllSecurityAndBackupSettings);
351
352 await preferencesFile.delete();
353 }
354
355 Future<void> _importKeychainDumpV1(String password,
322 - {required String nonce,
323 - String keychainSalt = secrets.backupKeychainSalt}) async {
356 + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async {
357 final appDir = await getApplicationDocumentsDirectory();
358 final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
326 - final decryptedKeychainDumpFileData = await _decryptV1(
327 - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
328 - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData))
329 - as Map<String, dynamic>;
359 + final decryptedKeychainDumpFileData =
360 + await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
361 + final keychainJSON =
362 + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
363 final keychainWalletsInfo = keychainJSON['wallets'] as List;
364 final decodedPin = keychainJSON['pin'] as String;
365 final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
333 - final backupPasswordKey =
334 - generateStoreKeyFor(key: SecretStoreKey.backupPassword);
366 + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
367 final backupPassword = keychainJSON[backupPasswordKey] as String;
368
337 - await _flutterSecureStorage.write(
338 - key: backupPasswordKey, value: backupPassword);
369 + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword);
370
371 keychainWalletsInfo.forEach((dynamic rawInfo) async {
372 final info = rawInfo as Map<String, dynamic>;
373 await importWalletKeychainInfo(info);
374 });
375
345 - await _flutterSecureStorage.write(
346 - key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
376 + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
377
378 keychainDumpFile.deleteSync();
379 }
@@ -352,27 +382,24 @@ class BackupService {
382 {String keychainSalt = secrets.backupKeychainSalt}) async {
383 final appDir = await getApplicationDocumentsDirectory();
384 final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
355 - final decryptedKeychainDumpFileData = await _decryptV2(
356 - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
357 - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData))
358 - as Map<String, dynamic>;
385 + final decryptedKeychainDumpFileData =
386 + await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
387 + final keychainJSON =
388 + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
389 final keychainWalletsInfo = keychainJSON['wallets'] as List;
390 final decodedPin = keychainJSON['pin'] as String;
391 final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
362 - final backupPasswordKey =
363 - generateStoreKeyFor(key: SecretStoreKey.backupPassword);
392 + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
393 final backupPassword = keychainJSON[backupPasswordKey] as String;
394
366 - await _flutterSecureStorage.write(
367 - key: backupPasswordKey, value: backupPassword);
395 + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword);
396
397 keychainWalletsInfo.forEach((dynamic rawInfo) async {
398 final info = rawInfo as Map<String, dynamic>;
399 await importWalletKeychainInfo(info);
400 });
401
374 - await _flutterSecureStorage.write(
375 - key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
402 + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
403
404 keychainDumpFile.deleteSync();
405 }
@@ -386,35 +413,26 @@ class BackupService {
413
414 @Deprecated('Use v2 instead')
415 Future<Uint8List> _exportKeychainDumpV1(String password,
389 - {required String nonce,
390 - String keychainSalt = secrets.backupKeychainSalt}) async
391 - => throw Exception('Deprecated');
416 + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async =>
417 + throw Exception('Deprecated');
418
419 Future<Uint8List> _exportKeychainDumpV2(String password,
420 {String keychainSalt = secrets.backupKeychainSalt}) async {
421 final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
422 final encodedPin = await _flutterSecureStorage.read(key: key);
423 final decodedPin = decodedPinCode(pin: encodedPin!);
398 - final wallets =
399 - await Future.wait(_walletInfoSource.values.map((walletInfo) async {
424 + final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async {
425 return {
426 'name': walletInfo.name,
427 'type': walletInfo.type.toString(),
403 - 'password':
404 - await _keyService.getWalletPassword(walletName: walletInfo.name)
428 + 'password': await _keyService.getWalletPassword(walletName: walletInfo.name)
429 };
430 }));
407 - final backupPasswordKey =
408 - generateStoreKeyFor(key: SecretStoreKey.backupPassword);
409 - final backupPassword =
410 - await _flutterSecureStorage.read(key: backupPasswordKey);
411 - final data = utf8.encode(json.encode({
412 - 'pin': decodedPin,
413 - 'wallets': wallets,
414 - backupPasswordKey: backupPassword
415 - }));
416 - final encrypted = await _encryptV2(
417 - Uint8List.fromList(data), '$keychainSalt$password');
431 + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
432 + final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey);
433 + final data = utf8.encode(
434 + json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword}));
435 + final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password');
436
437 return encrypted;
438 }
@@ -423,46 +441,57 @@ class BackupService {
441 final preferences = <String, dynamic>{
442 PreferencesKey.currentWalletName:
443 _sharedPreferences.getString(PreferencesKey.currentWalletName),
426 - PreferencesKey.currentNodeIdKey:
427 - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
428 - PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences
429 - .getInt(PreferencesKey.currentBalanceDisplayModeKey),
430 - PreferencesKey.currentWalletType:
431 - _sharedPreferences.getInt(PreferencesKey.currentWalletType),
444 + PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
445 + PreferencesKey.currentBalanceDisplayModeKey:
446 + _sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey),
447 + PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType),
448 PreferencesKey.currentFiatCurrencyKey:
449 _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
434 - PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences
435 - .getBool(PreferencesKey.shouldSaveRecipientAddressKey),
436 - PreferencesKey.disableBuyKey: _sharedPreferences
437 - .getBool(PreferencesKey.disableBuyKey),
438 - PreferencesKey.disableSellKey: _sharedPreferences
439 - .getBool(PreferencesKey.disableSellKey),
450 + PreferencesKey.shouldSaveRecipientAddressKey:
451 + _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
452 + PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey),
453 + PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
454 PreferencesKey.isDarkThemeLegacy:
455 _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy),
442 - PreferencesKey.currentPinLength:
443 - _sharedPreferences.getInt(PreferencesKey.currentPinLength),
444 - PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences
445 - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
446 - PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences
447 - .getBool(PreferencesKey.allowBiometricalAuthenticationKey),
448 - PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences
449 - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
456 + PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
457 + PreferencesKey.currentTransactionPriorityKeyLegacy:
458 + _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
459 + PreferencesKey.allowBiometricalAuthenticationKey:
460 + _sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey),
461 + PreferencesKey.currentBitcoinElectrumSererIdKey:
462 + _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
463 PreferencesKey.currentLanguageCode:
464 _sharedPreferences.getString(PreferencesKey.currentLanguageCode),
465 PreferencesKey.displayActionListModeKey:
466 _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey),
454 - PreferencesKey.currentTheme:
455 - _sharedPreferences.getInt(PreferencesKey.currentTheme),
456 - PreferencesKey.exchangeStatusKey:
457 - _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
458 - PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences
459 - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
467 + PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme),
468 + PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
469 + PreferencesKey.currentDefaultSettingsMigrationVersion:
470 + _sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
471 PreferencesKey.bitcoinTransactionPriority:
472 _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority),
473 PreferencesKey.moneroTransactionPriority:
474 _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority),
475 PreferencesKey.currentFiatApiModeKey:
465 - _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
476 + _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
477 + PreferencesKey.selectedCake2FAPreset:
478 + _sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset),
479 + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet:
480 + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet),
481 + PreferencesKey.shouldRequireTOTP2FAForSendsToContact:
482 + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact),
483 + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact:
484 + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact),
485 + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets:
486 + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets),
487 + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences
488 + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets),
489 + PreferencesKey.shouldRequireTOTP2FAForAddingContacts:
490 + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts),
491 + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets:
492 + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets),
493 + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences
494 + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
495 };
496
497 return json.encode(preferences);
@@ -476,28 +505,23 @@ class BackupService {
505 }
506
507 @Deprecated('Use v2 instead')
479 - Future<Uint8List> _encryptV1(
480 - Uint8List data, String secretKeySource, String nonceBase64) async
481 - => throw Exception('Deprecated');
508 + Future<Uint8List> _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async =>
509 + throw Exception('Deprecated');
510
483 - Future<Uint8List> _decryptV1(
484 - Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async {
511 + Future<Uint8List> _decryptV1(Uint8List data, String secretKeySource, String nonceBase64,
512 + {int macLength = 16}) async {
513 final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));
514 final secretKey = SecretKey(secretKeyHash.bytes);
515 final nonce = base64.decode(nonceBase64).toList();
488 - final box = SecretBox(
489 - Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
490 - nonce: nonce,
491 - mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
516 + final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
517 + nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
518 final plainData = await _cipher.decrypt(box, secretKey: secretKey);
519 return Uint8List.fromList(plainData);
520 }
521
496 - Future<Uint8List> _encryptV2(
497 - Uint8List data, String passphrase) async
498 - => cake_backup.encrypt(passphrase, data, version: _v2);
522 + Future<Uint8List> _encryptV2(Uint8List data, String passphrase) async =>
523 + cake_backup.encrypt(passphrase, data, version: _v2);
524
500 - Future<Uint8List> _decryptV2(
501 - Uint8List data, String passphrase) async
502 - => cake_backup.decrypt(passphrase, data);
525 + Future<Uint8List> _decryptV2(Uint8List data, String passphrase) async =>
526 + cake_backup.decrypt(passphrase, data);
527 }
lib/di.dart
+29 -19
@@ -247,7 +247,9 @@ Future setup({
247 nodeSource: _nodeSource,
248 isBitcoinBuyEnabled: isBitcoinBuyEnabled,
249 // Enforce darkTheme on platforms other than mobile till the design for other themes is completed
250 - initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme,
250 + initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile
251 + ? null
252 + : ThemeList.darkTheme,
253 );
254
255 if (_isSetupFinished) {
@@ -389,7 +391,9 @@ Future setup({
391 final authStore = getIt.get<AuthenticationStore>();
392 final appStore = getIt.get<AppStore>();
393 final useTotp = appStore.settingsStore.useTOTP2FA;
392 - if (useTotp) {
394 + final shouldUseTotp2FAToAccessWallets =
395 + appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
396 + if (useTotp && shouldUseTotp2FAToAccessWallets) {
397 authPageState.close(
398 route: Routes.totpAuthCodePage,
399 arguments: TotpAuthArgumentsModel(
@@ -525,17 +529,22 @@ Future setup({
529 getIt.get<SendTemplateStore>(),
530 getIt.get<FiatConversionStore>()));
531
528 - getIt.registerFactory<SendViewModel>(() => SendViewModel(
532 + getIt.registerFactory<SendViewModel>(
533 + () => SendViewModel(
534 getIt.get<AppStore>().wallet!,
535 getIt.get<AppStore>().settingsStore,
536 getIt.get<SendTemplateViewModel>(),
537 getIt.get<FiatConversionStore>(),
538 getIt.get<BalanceViewModel>(),
534 - _transactionDescriptionBox));
539 + getIt.get<ContactListViewModel>(),
540 + _transactionDescriptionBox,
541 + ),
542 + );
543
544 getIt.registerFactoryParam<SendPage, PaymentRequest?, void>(
545 (PaymentRequest? initialPaymentRequest, _) => SendPage(
546 sendViewModel: getIt.get<SendViewModel>(),
547 + authService: getIt.get<AuthService>(),
548 initialPaymentRequest: initialPaymentRequest,
549 ));
550
@@ -570,8 +579,8 @@ Future setup({
579 ));
580
581 getIt.registerFactoryParam<WalletEditViewModel, WalletListViewModel, void>(
573 - (WalletListViewModel walletListViewModel, _) => WalletEditViewModel(
574 - walletListViewModel, getIt.get<WalletLoadingService>()));
582 + (WalletListViewModel walletListViewModel, _) =>
583 + WalletEditViewModel(walletListViewModel, getIt.get<WalletLoadingService>()));
584
585 getIt.registerFactoryParam<WalletEditPage, List<dynamic>, void>((args, _) {
586 final walletListViewModel = args.first as WalletListViewModel;
@@ -583,7 +592,6 @@ Future setup({
592 editingWallet: editingWallet);
593 });
594
586 -
595 getIt.registerFactory(() {
596 final wallet = getIt.get<AppStore>().wallet!;
597
@@ -654,10 +662,11 @@ Future setup({
662 (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
663
664 getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
657 - (CryptoCurrency? cur, _) => ContactListViewModel(_contactSource, _walletInfoSource, cur));
665 + (CryptoCurrency? cur, _) =>
666 + ContactListViewModel(_contactSource, _walletInfoSource, cur, getIt.get<SettingsStore>()));
667
659 - getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>(
660 - (CryptoCurrency? cur, _) => ContactListPage(getIt.get<ContactListViewModel>(param1: cur)));
668 + getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) =>
669 + ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>()));
670
671 getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
672 (ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
@@ -702,13 +711,13 @@ Future setup({
711 ));
712
713 getIt.registerFactory(() => ExchangeViewModel(
705 - getIt.get<AppStore>().wallet!,
706 - _tradesSource,
707 - getIt.get<ExchangeTemplateStore>(),
708 - getIt.get<TradesStore>(),
709 - getIt.get<AppStore>().settingsStore,
710 - getIt.get<SharedPreferences>(),
711 - ));
714 + getIt.get<AppStore>().wallet!,
715 + _tradesSource,
716 + getIt.get<ExchangeTemplateStore>(),
717 + getIt.get<TradesStore>(),
718 + getIt.get<AppStore>().settingsStore,
719 + getIt.get<SharedPreferences>(),
720 + getIt.get<ContactListViewModel>()));
721
722 getIt.registerFactory(() => ExchangeTradeViewModel(
723 wallet: getIt.get<AppStore>().wallet!,
@@ -716,7 +725,8 @@ Future setup({
725 tradesStore: getIt.get<TradesStore>(),
726 sendViewModel: getIt.get<SendViewModel>()));
727
719 - getIt.registerFactory(() => ExchangePage(getIt.get<ExchangeViewModel>()));
728 + getIt.registerFactory(
729 + () => ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>()));
730
731 getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get<TradesStore>()));
732
@@ -890,7 +900,7 @@ Future setup({
900
901 getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>()));
902
893 - getIt.registerFactory(()=> MarketPlaceViewModel(getIt.get<IoniaService>()));
903 + getIt.registerFactory(() => MarketPlaceViewModel(getIt.get<IoniaService>()));
904
905 getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>()));
906
lib/entities/cake_2fa_preset_options.dart new
+35
@@ -0,0 +1,35 @@
1 +import 'package:cw_core/enumerable_item.dart';
2 +
3 +class Cake2FAPresetsOptions extends EnumerableItem<int> with Serializable<int> {
4 + const Cake2FAPresetsOptions({required String super.title, required int super.raw});
5 +
6 + static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0);
7 + static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1);
8 + static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2);
9 +
10 + static Cake2FAPresetsOptions deserialize({required int raw}) {
11 + switch (raw) {
12 + case 0:
13 + return Cake2FAPresetsOptions.narrow;
14 + case 1:
15 + return Cake2FAPresetsOptions.normal;
16 + case 2:
17 + return Cake2FAPresetsOptions.aggressive;
18 + default:
19 + throw Exception(
20 + 'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize',
21 + );
22 + }
23 + }
24 +}
25 +
26 +enum VerboseControlSettings {
27 + accessWallet,
28 + addingContacts,
29 + sendsToContacts,
30 + sendsToNonContacts,
31 + sendsToInternalWallets,
32 + exchangesToInternalWallets,
33 + securityAndBackupSettings,
34 + creatingNewWallets,
35 +}
lib/entities/preferences_key.dart
+22 -6
@@ -39,15 +39,31 @@ class PreferencesKey {
39 static const lastPopupDate = 'last_popup_date';
40 static const lastAppReviewDate = 'last_app_review_date';
41
42 -
43 -
44 - static String moneroWalletUpdateV1Key(String name)
45 - => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
42 + static String moneroWalletUpdateV1Key(String name) =>
43 + '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
44
45 static const exchangeProvidersSelection = 'exchange-providers-selection';
48 - static const clearnetDonationLink = 'clearnet_donation_link';
46 + static const clearnetDonationLink = 'clearnet_donation_link';
47 static const onionDonationLink = 'onion_donation_link';
48 static const lastSeenAppVersion = 'last_seen_app_version';
51 - static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
49 + static const shouldShowMarketPlaceInDashboard =
50 + 'should_show_marketplace_in_dashboard';
51 static const isNewInstall = 'is_new_install';
52 + static const shouldRequireTOTP2FAForAccessingWallet =
53 + 'should_require_totp_2fa_for_accessing_wallets';
54 + static const shouldRequireTOTP2FAForSendsToContact =
55 + 'should_require_totp_2fa_for_sends_to_contact';
56 + static const shouldRequireTOTP2FAForSendsToNonContact =
57 + 'should_require_totp_2fa_for_sends_to_non_contact';
58 + static const shouldRequireTOTP2FAForSendsToInternalWallets =
59 + 'should_require_totp_2fa_for_sends_to_internal_wallets';
60 + static const shouldRequireTOTP2FAForExchangesToInternalWallets =
61 + 'should_require_totp_2fa_for_exchanges_to_internal_wallets';
62 + static const shouldRequireTOTP2FAForAddingContacts =
63 + 'should_require_totp_2fa_for_adding_contacts';
64 + static const shouldRequireTOTP2FAForCreatingNewWallets =
65 + 'should_require_totp_2fa_for_creating_new_wallets';
66 + static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
67 + 'should_require_totp_2fa_for_all_security_and_backup_settings';
68 + static const selectedCake2FAPreset = 'selected_cake_2fa_preset';
69 }
lib/src/screens/contact/contact_list_page.dart
+141 -136
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 import 'package:cake_wallet/entities/contact_base.dart';
3 import 'package:cake_wallet/entities/contact_record.dart';
4 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
16 import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart';
17
18 class ContactListPage extends BasePage {
18 - ContactListPage(this.contactListViewModel);
19 + ContactListPage(this.contactListViewModel, this.authService);
20
21 final ContactListViewModel contactListViewModel;
22 + final AuthService authService;
23
24 @override
25 String get title => S.current.address_book;
@@ -26,95 +28,99 @@ class ContactListPage extends BasePage {
28 Widget? trailing(BuildContext context) {
29 return MergeSemantics(
30 child: Container(
29 - width: 32.0,
30 - height: 32.0,
31 - decoration: BoxDecoration(
32 - shape: BoxShape.circle,
33 - color: Theme.of(context)
34 - .accentTextTheme!
35 - .bodySmall!
36 - .color!),
37 - child: Stack(
38 - alignment: Alignment.center,
39 - children: <Widget>[
40 - Icon(Icons.add,
41 - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
42 - size: 22.0),
43 - ButtonTheme(
44 - minWidth: 32.0,
45 - height: 32.0,
46 - child: Semantics(
47 - label: S.of(context).add,
48 - child: TextButton(
49 - // FIX-ME: Style
50 - //shape: CircleBorder(),
51 - onPressed: () async {
52 - await Navigator.of(context)
53 - .pushNamed(Routes.addressBookAddContact);
54 - },
55 - child: Offstage()),
56 - ),
57 - )
58 - ],
59 - )),
31 + width: 32.0,
32 + height: 32.0,
33 + decoration: BoxDecoration(
34 + shape: BoxShape.circle,
35 + color: Theme.of(context).accentTextTheme!.bodySmall!.color!),
36 + child: Stack(
37 + alignment: Alignment.center,
38 + children: <Widget>[
39 + Icon(
40 + Icons.add,
41 + color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
42 + size: 22.0,
43 + ),
44 + ButtonTheme(
45 + minWidth: 32.0,
46 + height: 32.0,
47 + child: TextButton(
48 + // FIX-ME: Style
49 + //shape: CircleBorder(),
50 + onPressed: () async {
51 + if (contactListViewModel
52 + .shouldRequireTOTP2FAForAddingContacts) {
53 + authService.authenticateAction(
54 + context,
55 + route: Routes.addressBookAddContact,
56 + conditionToDetermineIfToUse2FA: contactListViewModel
57 + .shouldRequireTOTP2FAForAddingContacts,
58 + );
59 + } else {
60 + await Navigator.of(context)
61 + .pushNamed(Routes.addressBookAddContact);
62 + }
63 + },
64 + child: Offstage()),
65 + )
66 + ],
67 + ),
68 + ),
69 );
70 }
71
72 @override
73 Widget body(BuildContext context) {
65 -
74 return Container(
75 padding: EdgeInsets.only(top: 20.0, bottom: 20.0),
68 - child: Observer(
69 - builder: (_) {
76 + child: Observer(builder: (_) {
77 final contacts = contactListViewModel.contactsToShow;
78 final walletContacts = contactListViewModel.walletContactsToShow;
79 return CollapsibleSectionList(
73 - context: context,
74 - sectionCount: 2,
75 - themeColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
76 - dividerThemeColor:
77 - Theme.of(context).primaryTextTheme!.bodySmall!.decorationColor!,
78 - sectionTitleBuilder: (_, int sectionIndex) {
79 - var title = S.current.contact_list_contacts;
80 -
81 - if (sectionIndex == 0) {
82 - title = S.current.contact_list_wallets;
83 - }
84 -
85 - return Container(
86 - padding: EdgeInsets.only(bottom: 10),
87 - child: Text(title, style: TextStyle(fontSize: 36)));
88 - },
89 - itemCounter: (int sectionIndex) => sectionIndex == 0
90 - ? walletContacts.length
91 - : contacts.length,
92 - itemBuilder: (_, sectionIndex, index) {
93 - if (sectionIndex == 0) {
94 - final walletInfo = walletContacts[index];
95 - return generateRaw(context, walletInfo);
96 - }
97 -
98 - final contact = contacts[index];
99 - final content = generateRaw(context, contact);
100 - return contactListViewModel.isEditable
101 - ? Slidable(
102 - key: Key('${contact.key}'),
103 - endActionPane: _actionPane(context, contact),
104 - child: content,
105 - )
106 - : content;
107 - },
108 - );})
109 - );
80 + context: context,
81 + sectionCount: 2,
82 + themeColor: Theme.of(context).primaryTextTheme.titleLarge!.color!,
83 + dividerThemeColor:
84 + Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!,
85 + sectionTitleBuilder: (_, int sectionIndex) {
86 + var title = S.current.contact_list_contacts;
87 +
88 + if (sectionIndex == 0) {
89 + title = S.current.contact_list_wallets;
90 + }
91 +
92 + return Container(
93 + padding: EdgeInsets.only(bottom: 10),
94 + child: Text(title, style: TextStyle(fontSize: 36)));
95 + },
96 + itemCounter: (int sectionIndex) =>
97 + sectionIndex == 0 ? walletContacts.length : contacts.length,
98 + itemBuilder: (_, sectionIndex, index) {
99 + if (sectionIndex == 0) {
100 + final walletInfo = walletContacts[index];
101 + return generateRaw(context, walletInfo);
102 + }
103 +
104 + final contact = contacts[index];
105 + final content = generateRaw(context, contact);
106 + return contactListViewModel.isEditable
107 + ? Slidable(
108 + key: Key('${contact.key}'),
109 + endActionPane: _actionPane(context, contact),
110 + child: content,
111 + )
112 + : content;
113 + },
114 + );
115 + }));
116 }
117
118 Widget generateRaw(BuildContext context, ContactBase contact) {
119 final image = contact.type.iconPath;
114 - final currencyIcon = image != null ? Image.asset(image, height: 24, width: 24)
120 + final currencyIcon = image != null
121 + ? Image.asset(image, height: 24, width: 24)
122 : const SizedBox(height: 24, width: 24);
123
117 -
124 return GestureDetector(
125 onTap: () async {
126 if (!contactListViewModel.isEditable) {
@@ -128,30 +134,28 @@ class ContactListPage extends BasePage {
134 if (isCopied) {
135 await Clipboard.setData(ClipboardData(text: contact.address));
136 await showBar<void>(context, S.of(context).copied_to_clipboard);
131 -
137 }
138 },
139 child: Container(
140 color: Colors.transparent,
136 - padding:
137 - const EdgeInsets.only(top: 16, bottom: 16, right: 24),
141 + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24),
142 child: Row(
143 mainAxisSize: MainAxisSize.min,
144 mainAxisAlignment: MainAxisAlignment.start,
145 children: <Widget>[
146 currencyIcon,
147 Expanded(
144 - child: Padding(
145 - padding: EdgeInsets.only(left: 12),
146 - child: Text(
147 - contact.name,
148 - style: TextStyle(
149 - fontSize: 14,
150 - fontWeight: FontWeight.normal,
151 - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!),
148 + child: Padding(
149 + padding: EdgeInsets.only(left: 12),
150 + child: Text(
151 + contact.name,
152 + style: TextStyle(
153 + fontSize: 14,
154 + fontWeight: FontWeight.normal,
155 + color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
156 ),
153 - )
154 - )
157 + ),
158 + ))
159 ],
160 ),
161 ),
@@ -160,60 +164,61 @@ class ContactListPage extends BasePage {
164
165 Future<bool> showAlertDialog(BuildContext context) async {
166 return await showPopUp<bool>(
163 - context: context,
164 - builder: (BuildContext context) {
165 - return AlertWithTwoActions(
166 - alertTitle: S.of(context).address_remove_contact,
167 - alertContent: S.of(context).address_remove_content,
168 - rightButtonText: S.of(context).remove,
169 - leftButtonText: S.of(context).cancel,
170 - actionRightButton: () => Navigator.of(context).pop(true),
171 - actionLeftButton: () => Navigator.of(context).pop(false));
172 - }) ?? false;
167 + context: context,
168 + builder: (BuildContext context) {
169 + return AlertWithTwoActions(
170 + alertTitle: S.of(context).address_remove_contact,
171 + alertContent: S.of(context).address_remove_content,
172 + rightButtonText: S.of(context).remove,
173 + leftButtonText: S.of(context).cancel,
174 + actionRightButton: () => Navigator.of(context).pop(true),
175 + actionLeftButton: () => Navigator.of(context).pop(false));
176 + }) ??
177 + false;
178 }
179
180 Future<bool> showNameAndAddressDialog(
181 BuildContext context, String name, String address) async {
182 return await showPopUp<bool>(
178 - context: context,
179 - builder: (BuildContext context) {
180 - return AlertWithTwoActions(
181 - alertTitle: name,
182 - alertContent: address,
183 - rightButtonText: S.of(context).copy,
184 - leftButtonText: S.of(context).cancel,
185 - actionRightButton: () => Navigator.of(context).pop(true),
186 - actionLeftButton: () => Navigator.of(context).pop(false));
187 - }) ?? false;
183 + context: context,
184 + builder: (BuildContext context) {
185 + return AlertWithTwoActions(
186 + alertTitle: name,
187 + alertContent: address,
188 + rightButtonText: S.of(context).copy,
189 + leftButtonText: S.of(context).cancel,
190 + actionRightButton: () => Navigator.of(context).pop(true),
191 + actionLeftButton: () => Navigator.of(context).pop(false));
192 + }) ??
193 + false;
194 }
195
190 - ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane(
191 - motion: const ScrollMotion(),
192 - extentRatio: 0.4,
193 - children: [
194 - SlidableAction(
195 - onPressed: (_) async => await Navigator.of(context)
196 - .pushNamed(Routes.addressBookAddContact,
197 - arguments: contact),
198 - backgroundColor: Colors.blue,
199 - foregroundColor: Colors.white,
200 - icon: Icons.edit,
201 - label: S.of(context).edit,
202 - ),
203 - SlidableAction(
204 - onPressed: (_) async {
205 - final isDelete =
206 - await showAlertDialog(context);
207 -
208 - if (isDelete) {
209 - await contactListViewModel.delete(contact);
210 - }
211 - },
212 - backgroundColor: Colors.red,
213 - foregroundColor: Colors.white,
214 - icon: CupertinoIcons.delete,
215 - label: S.of(context).delete,
216 - ),
217 - ],
218 - );
196 + ActionPane _actionPane(BuildContext context, ContactRecord contact) =>
197 + ActionPane(
198 + motion: const ScrollMotion(),
199 + extentRatio: 0.4,
200 + children: [
201 + SlidableAction(
202 + onPressed: (_) async => await Navigator.of(context)
203 + .pushNamed(Routes.addressBookAddContact, arguments: contact),
204 + backgroundColor: Colors.blue,
205 + foregroundColor: Colors.white,
206 + icon: Icons.edit,
207 + label: S.of(context).edit,
208 + ),
209 + SlidableAction(
210 + onPressed: (_) async {
211 + final isDelete = await showAlertDialog(context);
212 +
213 + if (isDelete) {
214 + await contactListViewModel.delete(contact);
215 + }
216 + },
217 + backgroundColor: Colors.red,
218 + foregroundColor: Colors.white,
219 + icon: CupertinoIcons.delete,
220 + label: S.of(context).delete,
221 + ),
222 + ],
223 + );
224 }
lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
+18 -4
@@ -156,15 +156,29 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
156 } catch (e) {
157 changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
158 }
159 - });
159 + },
160 + conditionToDetermineIfToUse2FA:
161 + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
162 + );
163 }
164
165 void _navigateToCreateWallet() {
166 if (isSingleCoin) {
164 - Navigator.of(context)
165 - .pushNamed(Routes.newWallet, arguments: widget.walletListViewModel.currentWalletType);
167 + widget._authService.authenticateAction(
168 + context,
169 + route: Routes.newWallet,
170 + arguments: widget.walletListViewModel.currentWalletType,
171 + conditionToDetermineIfToUse2FA: widget
172 + .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
173 + );
174 } else {
167 - Navigator.of(context).pushNamed(Routes.newWalletType);
175 + widget._authService.authenticateAction(
176 + context,
177 + route: Routes.newWalletType,
178 + conditionToDetermineIfToUse2FA: widget
179 + .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
180 + );
181 +
182 }
183 }
184
lib/src/screens/exchange/exchange_page.dart
+232 -266
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 import 'package:cake_wallet/di.dart';
3 import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
4 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
38 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
39
40 class ExchangePage extends BasePage {
40 - ExchangePage(this.exchangeViewModel) {
41 + ExchangePage(this.exchangeViewModel, this.authService) {
42 depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
43 ? exchangeViewModel.wallet.name
44 : null;
@@ -47,6 +48,7 @@ class ExchangePage extends BasePage {
48 }
49
50 final ExchangeViewModel exchangeViewModel;
51 + final AuthService authService;
52 final depositKey = GlobalKey<ExchangeCardState>();
53 final receiveKey = GlobalKey<ExchangeCardState>();
54 final _formKey = GlobalKey<FormState>();
@@ -89,16 +91,17 @@ class ExchangePage extends BasePage {
91
92 @override
93 Widget middle(BuildContext context) => Row(
92 - mainAxisAlignment: MainAxisAlignment.center,
93 - children: [
94 - Padding(
95 - padding: const EdgeInsets.only(right:6.0),
96 - child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),)
97 - ),
98 - PresentProviderPicker(exchangeViewModel: exchangeViewModel)
99 - ],
100 - );
101 -
94 + mainAxisAlignment: MainAxisAlignment.center,
95 + children: [
96 + Padding(
97 + padding: const EdgeInsets.only(right: 6.0),
98 + child: Observer(
99 + builder: (_) =>
100 + SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),
101 + )),
102 + PresentProviderPicker(exchangeViewModel: exchangeViewModel)
103 + ],
104 + );
105
106 @override
107 Widget trailing(BuildContext context) => TrailButton(
@@ -110,12 +113,13 @@ class ExchangePage extends BasePage {
113
114 @override
115 Widget? leading(BuildContext context) {
113 - final _backButton = Icon(Icons.arrow_back_ios,
116 + final _backButton = Icon(
117 + Icons.arrow_back_ios,
118 color: titleColor,
119 size: 16,
120 );
117 - final _closeButton = currentTheme.type == ThemeType.dark
118 - ? closeButtonImageDarkTheme : closeButtonImage;
121 + final _closeButton =
122 + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
123
124 bool isMobileView = ResponsiveLayoutUtil.instance.isMobile;
125
@@ -126,13 +130,10 @@ class ExchangePage extends BasePage {
130 child: ButtonTheme(
131 minWidth: double.minPositive,
132 child: Semantics(
129 - label: !isMobileView
130 - ? S.of(context).close
131 - : S.of(context).seed_alert_back,
133 + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
134 child: TextButton(
135 style: ButtonStyle(
134 - overlayColor: MaterialStateColor.resolveWith(
135 - (states) => Colors.transparent),
136 + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
137 ),
138 onPressed: () => onClose(context),
139 child: !isMobileView ? _closeButton : _backButton,
@@ -145,23 +146,19 @@ class ExchangePage extends BasePage {
146
147 @override
148 Widget body(BuildContext context) {
148 - WidgetsBinding.instance
149 - .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
149 + WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
150
151 return KeyboardActions(
152 disableScroll: true,
153 config: KeyboardActionsConfig(
154 keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
155 - keyboardBarColor:
156 - Theme.of(context).accentTextTheme!.bodyLarge!.backgroundColor!,
155 + keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
156 nextFocus: false,
157 actions: [
158 KeyboardActionsItem(
160 - focusNode: _depositAmountFocus,
161 - toolbarButtons: [(_) => KeyboardDoneButton()]),
159 + focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
160 KeyboardActionsItem(
163 - focusNode: _receiveAmountFocus,
164 - toolbarButtons: [(_) => KeyboardDoneButton()])
161 + focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
162 ]),
163 child: Container(
164 color: Theme.of(context).colorScheme.background,
@@ -169,30 +166,28 @@ class ExchangePage extends BasePage {
166 key: _formKey,
167 child: ScrollableWithBottomSection(
168 contentPadding: EdgeInsets.only(bottom: 24),
172 - content: Observer(builder: (_) => Column(
173 - children: <Widget>[
174 - _exchangeCardsSection(context),
175 - Padding(
176 - padding: EdgeInsets.only(top: 12, left: 24),
177 - child: Row(
178 - mainAxisAlignment: MainAxisAlignment.start,
179 - children: [
180 - StandardCheckbox(
181 - value: exchangeViewModel.isFixedRateMode,
182 - caption: S.of(context).fixed_rate,
183 - onChanged: (value) =>
184 - exchangeViewModel.isFixedRateMode = value,
185 - ),
186 - ],
187 - )
188 - ),
189 - SizedBox(height: 30),
190 - _buildTemplateSection(context)
169 + content: Observer(
170 + builder: (_) => Column(
171 + children: <Widget>[
172 + _exchangeCardsSection(context),
173 + Padding(
174 + padding: EdgeInsets.only(top: 12, left: 24),
175 + child: Row(
176 + mainAxisAlignment: MainAxisAlignment.start,
177 + children: [
178 + StandardCheckbox(
179 + value: exchangeViewModel.isFixedRateMode,
180 + caption: S.of(context).fixed_rate,
181 + onChanged: (value) => exchangeViewModel.isFixedRateMode = value,
182 + ),
183 + ],
184 + )),
185 + SizedBox(height: 30),
186 + _buildTemplateSection(context)
187 ],
188 ),
189 ),
194 - bottomSectionPadding:
195 - EdgeInsets.only(left: 24, right: 24, bottom: 24),
190 + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
191 bottomSection: Column(children: <Widget>[
192 Padding(
193 padding: EdgeInsets.only(bottom: 15),
@@ -210,8 +205,7 @@ class ExchangePage extends BasePage {
205 textAlign: TextAlign.center,
206 style: TextStyle(
207 color: Theme.of(context)
213 - .primaryTextTheme!
214 - .displayLarge!
208 + .primaryTextTheme.displayLarge!
209 .decorationColor!,
210 fontWeight: FontWeight.w500,
211 fontSize: 12),
@@ -223,29 +217,34 @@ class ExchangePage extends BasePage {
217 builder: (_) => LoadingPrimaryButton(
218 text: S.of(context).exchange,
219 onPressed: () {
226 - if (_formKey.currentState != null && _formKey.currentState!.validate()) {
227 - if ((exchangeViewModel.depositCurrency ==
228 - CryptoCurrency.xmr) &&
229 - (!(exchangeViewModel.status
230 - is SyncedSyncStatus))) {
220 + if (_formKey.currentState != null &&
221 + _formKey.currentState!.validate()) {
222 + if ((exchangeViewModel.depositCurrency == CryptoCurrency.xmr) &&
223 + (!(exchangeViewModel.status is SyncedSyncStatus))) {
224 showPopUp<void>(
225 context: context,
226 builder: (BuildContext context) {
227 return AlertWithOneAction(
228 alertTitle: S.of(context).exchange,
236 - alertContent: S
237 - .of(context)
238 - .exchange_sync_alert_content,
229 + alertContent: S.of(context).exchange_sync_alert_content,
230 buttonText: S.of(context).ok,
240 - buttonAction: () =>
241 - Navigator.of(context).pop());
231 + buttonAction: () => Navigator.of(context).pop());
232 });
233 } else {
244 - exchangeViewModel.createTrade();
234 + final check = exchangeViewModel.shouldDisplayTOTP();
235 + authService.authenticateAction(
236 + context,
237 + conditionToDetermineIfToUse2FA: check,
238 + onAuthSuccess: (value) {
239 + if (value) {
240 + exchangeViewModel.createTrade();
241 + }
242 + },
243 + );
244 }
245 }
246 },
248 - color: Theme.of(context).accentTextTheme!.bodyLarge!.color!,
247 + color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
248 textColor: Colors.white,
249 isDisabled: exchangeViewModel.selectedProviders.isEmpty,
250 isLoading: exchangeViewModel.tradeState is TradeIsCreating)),
@@ -264,7 +263,7 @@ class ExchangePage extends BasePage {
263 child: Observer(
264 builder: (_) {
265 final templates = exchangeViewModel.templates;
267 -
266 +
267 return Row(
268 children: <Widget>[
269 AddTemplateButton(
@@ -293,18 +292,15 @@ class ExchangePage extends BasePage {
292 builder: (dialogContext) {
293 return AlertWithTwoActions(
294 alertTitle: S.of(context).template,
296 - alertContent:
297 - S.of(context).confirm_delete_template,
295 + alertContent: S.of(context).confirm_delete_template,
296 rightButtonText: S.of(context).delete,
297 leftButtonText: S.of(context).cancel,
298 actionRightButton: () {
299 Navigator.of(dialogContext).pop();
302 - exchangeViewModel.removeTemplate(
303 - template: template);
300 + exchangeViewModel.removeTemplate(template: template);
301 exchangeViewModel.updateTemplate();
302 },
306 - actionLeftButton: () =>
307 - Navigator.of(dialogContext).pop());
303 + actionLeftButton: () => Navigator.of(dialogContext).pop());
304 });
305 },
306 );
@@ -318,8 +314,8 @@ class ExchangePage extends BasePage {
314 );
315 }
316
321 - void applyTemplate(BuildContext context,
322 - ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
317 + void applyTemplate(
318 + BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
319 exchangeViewModel.changeDepositCurrency(
320 currency: CryptoCurrency.fromString(template.depositCurrency));
321 exchangeViewModel.changeReceiveCurrency(
@@ -333,22 +329,19 @@ class ExchangePage extends BasePage {
329
330 var domain = template.depositAddress;
331 var ticker = template.depositCurrency.toLowerCase();
336 - exchangeViewModel.depositAddress =
337 - await fetchParsedAddress(context, domain, ticker);
332 + exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker);
333
334 domain = template.receiveAddress;
335 ticker = template.receiveCurrency.toLowerCase();
341 - exchangeViewModel.receiveAddress =
342 - await fetchParsedAddress(context, domain, ticker);
336 + exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker);
337 }
338
345 - void _setReactions(
346 - BuildContext context, ExchangeViewModel exchangeViewModel) {
339 + void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
340 if (_isReactionsSet) {
341 return;
342 }
343
351 - if (exchangeViewModel.isLowFee) {
344 + if (exchangeViewModel.isLowFee) {
345 _showFeeAlert(context);
346 }
347
@@ -359,42 +352,30 @@ class ExchangePage extends BasePage {
352 final limitsState = exchangeViewModel.limitsState;
353
354 if (limitsState is LimitsLoadedSuccessfully) {
362 - final min = limitsState.limits.min != null
363 - ? limitsState.limits.min.toString()
364 - : null;
365 - final max = limitsState.limits.max != null
366 - ? limitsState.limits.max.toString()
367 - : null;
368 - final key = exchangeViewModel.isFixedRateMode
369 - ? receiveKey
370 - : depositKey;
355 + final min = limitsState.limits.min != null ? limitsState.limits.min.toString() : null;
356 + final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null;
357 + final key = exchangeViewModel.isFixedRateMode ? receiveKey : depositKey;
358 key.currentState!.changeLimits(min: min, max: max);
359 }
360
374 - _onCurrencyChange(
375 - exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
376 - _onCurrencyChange(
377 - exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
361 + _onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
362 + _onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
363
364 reaction(
365 (_) => exchangeViewModel.wallet.name,
381 - (String _) => _onWalletNameChange(
382 - exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
366 + (String _) =>
367 + _onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
368
369 reaction(
370 (_) => exchangeViewModel.wallet.name,
386 - (String _) => _onWalletNameChange(
387 - exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
371 + (String _) =>
372 + _onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
373
389 - reaction(
390 - (_) => exchangeViewModel.receiveCurrency,
391 - (CryptoCurrency currency) =>
392 - _onCurrencyChange(currency, exchangeViewModel, receiveKey));
374 + reaction((_) => exchangeViewModel.receiveCurrency,
375 + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey));
376
394 - reaction(
395 - (_) => exchangeViewModel.depositCurrency,
396 - (CryptoCurrency currency) =>
397 - _onCurrencyChange(currency, exchangeViewModel, depositKey));
377 + reaction((_) => exchangeViewModel.depositCurrency,
378 + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
379
380 reaction((_) => exchangeViewModel.depositAmount, (String amount) {
381 if (depositKey.currentState!.amountController.text != amount) {
@@ -408,8 +389,7 @@ class ExchangePage extends BasePage {
389 }
390 });
391
411 - reaction((_) => exchangeViewModel.isDepositAddressEnabled,
412 - (bool isEnabled) {
392 + reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {
393 depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
394 });
395
@@ -425,13 +405,11 @@ class ExchangePage extends BasePage {
405 }
406 });
407
428 - reaction((_) => exchangeViewModel.isReceiveAddressEnabled,
429 - (bool isEnabled) {
408 + reaction((_) => exchangeViewModel.isReceiveAddressEnabled, (bool isEnabled) {
409 receiveKey.currentState!.isAddressEditable(isEditable: isEnabled);
410 });
411
433 - reaction((_) => exchangeViewModel.isReceiveAmountEditable,
434 - (bool isReceiveAmountEditable) {
412 + reaction((_) => exchangeViewModel.isReceiveAmountEditable, (bool isReceiveAmountEditable) {
413 receiveKey.currentState!.isAmountEditable(isEditable: isReceiveAmountEditable);
414 });
415
@@ -483,20 +461,20 @@ class ExchangePage extends BasePage {
461 }
462 });
463
486 - depositAddressController.addListener(
487 - () => exchangeViewModel.depositAddress = depositAddressController.text);
464 + depositAddressController
465 + .addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
466
467 depositAmountController.addListener(() {
468 if (depositAmountController.text != exchangeViewModel.depositAmount) {
491 - _depositAmountDebounce.run(() {
469 + _depositAmountDebounce.run(() {
470 exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
471 exchangeViewModel.isReceiveAmountEntered = false;
472 });
473 }
474 });
475
498 - receiveAddressController.addListener(
499 - () => exchangeViewModel.receiveAddress = receiveAddressController.text);
476 + receiveAddressController
477 + .addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text);
478
479 receiveAmountController.addListener(() {
480 if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
@@ -507,8 +485,7 @@ class ExchangePage extends BasePage {
485 }
486 });
487
510 - reaction((_) => exchangeViewModel.wallet.walletAddresses.address,
511 - (String address) {
488 + reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
489 if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
490 depositKey.currentState!.changeAddress(address: address);
491 }
@@ -519,22 +496,18 @@ class ExchangePage extends BasePage {
496 });
497
498 _depositAddressFocus.addListener(() async {
522 - if (!_depositAddressFocus.hasFocus &&
523 - depositAddressController.text.isNotEmpty) {
499 + if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
500 final domain = depositAddressController.text;
501 final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
526 - exchangeViewModel.depositAddress =
527 - await fetchParsedAddress(context, domain, ticker);
502 + exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker);
503 }
504 });
505
506 _receiveAddressFocus.addListener(() async {
532 - if (!_receiveAddressFocus.hasFocus &&
533 - receiveAddressController.text.isNotEmpty) {
507 + if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) {
508 final domain = receiveAddressController.text;
509 final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
536 - exchangeViewModel.receiveAddress =
537 - await fetchParsedAddress(context, domain, ticker);
510 + exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker);
511 }
512 });
513
@@ -554,29 +527,26 @@ class ExchangePage extends BasePage {
527 _isReactionsSet = true;
528 }
529
557 - void _onCurrencyChange(CryptoCurrency currency,
558 - ExchangeViewModel exchangeViewModel, GlobalKey<ExchangeCardState> key) {
530 + void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
531 + GlobalKey<ExchangeCardState> key) {
532 final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
533
534 key.currentState!.changeSelectedCurrency(currency);
562 - key.currentState!.changeWalletName(
563 - isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
535 + key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
536
537 key.currentState!.changeAddress(
566 - address: isCurrentTypeWallet
567 - ? exchangeViewModel.wallet.walletAddresses.address : '');
538 + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
539
540 key.currentState!.changeAmount(amount: '');
541 }
542
572 - void _onWalletNameChange(ExchangeViewModel exchangeViewModel,
573 - CryptoCurrency currency, GlobalKey<ExchangeCardState> key) {
543 + void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
544 + GlobalKey<ExchangeCardState> key) {
545 final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
546
547 if (isCurrentTypeWallet) {
548 key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
578 - key.currentState!.addressController.text =
579 - exchangeViewModel.wallet.walletAddresses.address;
549 + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
550 } else if (key.currentState!.addressController.text ==
551 exchangeViewModel.wallet.walletAddresses.address) {
552 key.currentState!.changeWalletName('');
@@ -584,8 +554,7 @@ class ExchangePage extends BasePage {
554 }
555 }
556
587 - Future<String> fetchParsedAddress(
588 - BuildContext context, String domain, String ticker) async {
557 + Future<String> fetchParsedAddress(BuildContext context, String domain, String ticker) async {
558 final parsedAddress = await getIt.get<AddressResolver>().resolve(domain, ticker);
559 final address = await extractAddressFromParsed(context, parsedAddress);
560 return address;
@@ -594,16 +563,17 @@ class ExchangePage extends BasePage {
563 void _showFeeAlert(BuildContext context) async {
564 await Future<void>.delayed(Duration(seconds: 1));
565 final confirmed = await showPopUp<bool>(
597 - context: context,
598 - builder: (dialogContext) {
599 - return AlertWithTwoActions(
600 - alertTitle: S.of(context).low_fee,
601 - alertContent: S.of(context).low_fee_alert,
602 - leftButtonText: S.of(context).ignor,
603 - rightButtonText: S.of(context).use_suggested,
604 - actionLeftButton: () => Navigator.of(dialogContext).pop(false),
605 - actionRightButton: () => Navigator.of(dialogContext).pop(true));
606 - }) ?? false;
566 + context: context,
567 + builder: (dialogContext) {
568 + return AlertWithTwoActions(
569 + alertTitle: S.of(context).low_fee,
570 + alertContent: S.of(context).low_fee_alert,
571 + leftButtonText: S.of(context).ignor,
572 + rightButtonText: S.of(context).use_suggested,
573 + actionLeftButton: () => Navigator.of(dialogContext).pop(false),
574 + actionRightButton: () => Navigator.of(dialogContext).pop(true));
575 + }) ??
576 + false;
577 if (confirmed) {
578 exchangeViewModel.setDefaultTransactionPriority();
579 }
@@ -612,126 +582,122 @@ class ExchangePage extends BasePage {
582 void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel();
583
584 Widget _exchangeCardsSection(BuildContext context) {
615 - final firstExchangeCard = Observer(builder: (_) => ExchangeCard(
616 - onDispose: disposeBestRateSync,
617 - hasAllAmount: exchangeViewModel.hasAllAmount,
618 - allAmount: exchangeViewModel.hasAllAmount
619 - ? () => exchangeViewModel.calculateDepositAllAmount()
620 - : null,
621 - amountFocusNode: _depositAmountFocus,
622 - addressFocusNode: _depositAddressFocus,
623 - key: depositKey,
624 - title: S.of(context).you_will_send,
625 - initialCurrency: exchangeViewModel.depositCurrency,
626 - initialWalletName: depositWalletName ?? '',
627 - initialAddress:
628 - exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
629 - ? exchangeViewModel.wallet.walletAddresses.address
630 - : exchangeViewModel.depositAddress,
631 - initialIsAmountEditable: true,
632 - initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
633 - isAmountEstimated: false,
634 - hasRefundAddress: true,
635 - isMoneroWallet: exchangeViewModel.isMoneroWallet,
636 - currencies: exchangeViewModel.depositCurrencies,
637 - onCurrencySelected: (currency) {
638 - // FIXME: need to move it into view model
639 - if (currency == CryptoCurrency.xmr &&
640 - exchangeViewModel.wallet.type != WalletType.monero) {
641 - showPopUp<void>(
642 - context: context,
643 - builder: (dialogContext) {
644 - return AlertWithOneAction(
645 - alertTitle: S.of(context).error,
646 - alertContent:
647 - S.of(context).exchange_incorrect_current_wallet_for_xmr,
648 - buttonText: S.of(context).ok,
649 - buttonAction: () => Navigator.of(dialogContext).pop());
650 - });
651 - return;
652 - }
653 -
654 - exchangeViewModel.changeDepositCurrency(currency: currency);
655 - },
656 - imageArrow: arrowBottomPurple,
657 - currencyButtonColor: Colors.transparent,
658 - addressButtonsColor: Theme.of(context).focusColor!,
659 - borderColor: Theme.of(context).primaryTextTheme!.bodyLarge!.color!,
660 - currencyValueValidator: (value) {
661 - return !exchangeViewModel.isFixedRateMode
662 - ? AmountValidator(
663 - isAutovalidate: true,
664 - currency: exchangeViewModel.depositCurrency,
665 - minValue: exchangeViewModel.limits.min.toString(),
666 - maxValue: exchangeViewModel.limits.max.toString(),
667 - ).call(value)
668 - : null;
669 - },
670 - addressTextFieldValidator:
671 - AddressValidator(type: exchangeViewModel.depositCurrency),
672 - onPushPasteButton: (context) async {
673 - final domain = exchangeViewModel.depositAddress;
674 - final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
675 - exchangeViewModel.depositAddress =
676 - await fetchParsedAddress(context, domain, ticker);
677 - },
678 - onPushAddressBookButton: (context) async {
679 - final domain = exchangeViewModel.depositAddress;
680 - final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
681 - exchangeViewModel.depositAddress =
682 - await fetchParsedAddress(context, domain, ticker);
683 - },
684 - ));
685 -
686 - final secondExchangeCard = Observer(builder: (_) => ExchangeCard(
687 - onDispose: disposeBestRateSync,
688 - amountFocusNode: _receiveAmountFocus,
689 - addressFocusNode: _receiveAddressFocus,
690 - key: receiveKey,
691 - title: S.of(context).you_will_get,
692 - initialCurrency: exchangeViewModel.receiveCurrency,
693 - initialWalletName: receiveWalletName ?? '',
694 - initialAddress:
695 - exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
696 - ? exchangeViewModel.wallet.walletAddresses.address
697 - : exchangeViewModel.receiveAddress,
698 - initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
699 - initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled,
700 - isAmountEstimated: true,
701 - isMoneroWallet: exchangeViewModel.isMoneroWallet,
702 - currencies: exchangeViewModel.receiveCurrencies,
703 - onCurrencySelected: (currency) =>
704 - exchangeViewModel.changeReceiveCurrency(currency: currency),
705 - imageArrow: arrowBottomCakeGreen,
706 - currencyButtonColor: Colors.transparent,
707 - addressButtonsColor: Theme.of(context).focusColor!,
708 - borderColor:
709 - Theme.of(context).primaryTextTheme!.bodyLarge!.decorationColor!,
710 - currencyValueValidator: (value) {
711 - return exchangeViewModel.isFixedRateMode
712 - ? AmountValidator(
713 - isAutovalidate: true,
714 - currency: exchangeViewModel.receiveCurrency,
715 - minValue: exchangeViewModel.limits.min.toString(),
716 - maxValue: exchangeViewModel.limits.max.toString(),
717 - ).call(value)
718 - : null;
719 - },
720 - addressTextFieldValidator:
721 - AddressValidator(type: exchangeViewModel.receiveCurrency),
722 - onPushPasteButton: (context) async {
723 - final domain = exchangeViewModel.receiveAddress;
724 - final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
725 - exchangeViewModel.receiveAddress =
726 - await fetchParsedAddress(context, domain, ticker);
727 - },
728 - onPushAddressBookButton: (context) async {
729 - final domain = exchangeViewModel.receiveAddress;
730 - final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
731 - exchangeViewModel.receiveAddress =
732 - await fetchParsedAddress(context, domain, ticker);
733 - },
734 - ));
585 + final firstExchangeCard = Observer(
586 + builder: (_) => ExchangeCard(
587 + onDispose: disposeBestRateSync,
588 + hasAllAmount: exchangeViewModel.hasAllAmount,
589 + allAmount: exchangeViewModel.hasAllAmount
590 + ? () => exchangeViewModel.calculateDepositAllAmount()
591 + : null,
592 + amountFocusNode: _depositAmountFocus,
593 + addressFocusNode: _depositAddressFocus,
594 + key: depositKey,
595 + title: S.of(context).you_will_send,
596 + initialCurrency: exchangeViewModel.depositCurrency,
597 + initialWalletName: depositWalletName ?? '',
598 + initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
599 + ? exchangeViewModel.wallet.walletAddresses.address
600 + : exchangeViewModel.depositAddress,
601 + initialIsAmountEditable: true,
602 + initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
603 + isAmountEstimated: false,
604 + hasRefundAddress: true,
605 + isMoneroWallet: exchangeViewModel.isMoneroWallet,
606 + currencies: exchangeViewModel.depositCurrencies,
607 + onCurrencySelected: (currency) {
608 + // FIXME: need to move it into view model
609 + if (currency == CryptoCurrency.xmr &&
610 + exchangeViewModel.wallet.type != WalletType.monero) {
611 + showPopUp<void>(
612 + context: context,
613 + builder: (dialogContext) {
614 + return AlertWithOneAction(
615 + alertTitle: S.of(context).error,
616 + alertContent: S.of(context).exchange_incorrect_current_wallet_for_xmr,
617 + buttonText: S.of(context).ok,
618 + buttonAction: () => Navigator.of(dialogContext).pop());
619 + });
620 + return;
621 + }
622 +
623 + exchangeViewModel.changeDepositCurrency(currency: currency);
624 + },
625 + imageArrow: arrowBottomPurple,
626 + currencyButtonColor: Colors.transparent,
627 + addressButtonsColor: Theme.of(context).focusColor,
628 + borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.color!,
629 + currencyValueValidator: (value) {
630 + return !exchangeViewModel.isFixedRateMode
631 + ? AmountValidator(
632 + isAutovalidate: true,
633 + currency: exchangeViewModel.depositCurrency,
634 + minValue: exchangeViewModel.limits.min.toString(),
635 + maxValue: exchangeViewModel.limits.max.toString(),
636 + ).call(value)
637 + : null;
638 + },
639 + addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency),
640 + onPushPasteButton: (context) async {
641 + final domain = exchangeViewModel.depositAddress;
642 + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
643 + exchangeViewModel.depositAddress =
644 + await fetchParsedAddress(context, domain, ticker);
645 + },
646 + onPushAddressBookButton: (context) async {
647 + final domain = exchangeViewModel.depositAddress;
648 + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase();
649 + exchangeViewModel.depositAddress =
650 + await fetchParsedAddress(context, domain, ticker);
651 + },
652 + ));
653 +
654 + final secondExchangeCard = Observer(
655 + builder: (_) => ExchangeCard(
656 + onDispose: disposeBestRateSync,
657 + amountFocusNode: _receiveAmountFocus,
658 + addressFocusNode: _receiveAddressFocus,
659 + key: receiveKey,
660 + title: S.of(context).you_will_get,
661 + initialCurrency: exchangeViewModel.receiveCurrency,
662 + initialWalletName: receiveWalletName ?? '',
663 + initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
664 + ? exchangeViewModel.wallet.walletAddresses.address
665 + : exchangeViewModel.receiveAddress,
666 + initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
667 + initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled,
668 + isAmountEstimated: true,
669 + isMoneroWallet: exchangeViewModel.isMoneroWallet,
670 + currencies: exchangeViewModel.receiveCurrencies,
671 + onCurrencySelected: (currency) =>
672 + exchangeViewModel.changeReceiveCurrency(currency: currency),
673 + imageArrow: arrowBottomCakeGreen,
674 + currencyButtonColor: Colors.transparent,
675 + addressButtonsColor: Theme.of(context).focusColor,
676 + borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.decorationColor!,
677 + currencyValueValidator: (value) {
678 + return exchangeViewModel.isFixedRateMode
679 + ? AmountValidator(
680 + isAutovalidate: true,
681 + currency: exchangeViewModel.receiveCurrency,
682 + minValue: exchangeViewModel.limits.min.toString(),
683 + maxValue: exchangeViewModel.limits.max.toString(),
684 + ).call(value)
685 + : null;
686 + },
687 + addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency),
688 + onPushPasteButton: (context) async {
689 + final domain = exchangeViewModel.receiveAddress;
690 + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
691 + exchangeViewModel.receiveAddress =
692 + await fetchParsedAddress(context, domain, ticker);
693 + },
694 + onPushAddressBookButton: (context) async {
695 + final domain = exchangeViewModel.receiveAddress;
696 + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase();
697 + exchangeViewModel.receiveAddress =
698 + await fetchParsedAddress(context, domain, ticker);
699 + },
700 + ));
701
702 if (ResponsiveLayoutUtil.instance.isMobile) {
703 return MobileExchangeCardsSection(
lib/src/screens/root/root.dart
+10 -10
@@ -97,7 +97,8 @@ class RootState extends State<Root> with WidgetsBindingObserver {
97 return;
98 }
99
100 - if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
100 + if (!_isInactive &&
101 + widget.authenticationStore.state == AuthenticationState.allowed) {
102 setState(() => _setInactive(true));
103 }
104
@@ -124,13 +125,16 @@ class RootState extends State<Root> with WidgetsBindingObserver {
125 return;
126 } else {
127 final useTotp = widget.appStore.settingsStore.useTOTP2FA;
127 - if (useTotp) {
128 + final shouldUseTotp2FAToAccessWallets = widget.appStore
129 + .settingsStore.shouldRequireTOTP2FAForAccessingWallet;
130 + if (useTotp && shouldUseTotp2FAToAccessWallets) {
131 _reset();
132 auth.close(
133 route: Routes.totpAuthCodePage,
134 arguments: TotpAuthArgumentsModel(
135 onTotpAuthenticationFinished:
133 - (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) {
136 + (bool isAuthenticatedSuccessfully,
137 + TotpAuthCodePageState totpAuth) {
138 if (!isAuthenticatedSuccessfully) {
139 return;
140 }
@@ -151,15 +155,11 @@ class RootState extends State<Root> with WidgetsBindingObserver {
155 route: launchUri != null ? Routes.send : null,
156 arguments: PaymentRequest.fromUri(launchUri),
157 );
154 - launchUri = null;
158 + launchUri = null;
159 }
160 }
157 -
158 -
159 - },
160 - );
161 -
162 -
161 + },
162 + );
163 });
164 } else if (launchUri != null) {
165 widget.navigatorKey.currentState?.pushNamed(
lib/src/screens/send/send_page.dart
+34 -18
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 import 'package:cake_wallet/entities/fiat_currency.dart';
3 import 'package:cake_wallet/entities/template.dart';
4 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
@@ -32,10 +33,12 @@ import 'package:cw_core/crypto_currency.dart';
33 class SendPage extends BasePage {
34 SendPage({
35 required this.sendViewModel,
36 + required this.authService,
37 this.initialPaymentRequest,
38 }) : _formKey = GlobalKey<FormState>();
39
40 final SendViewModel sendViewModel;
41 + final AuthService authService;
42 final GlobalKey<FormState> _formKey;
43 final controller = PageController(initialPage: 0);
44 final PaymentRequest? initialPaymentRequest;
@@ -56,12 +59,14 @@ class SendPage extends BasePage {
59
60 @override
61 Widget? leading(BuildContext context) {
59 - final _backButton = Icon(Icons.arrow_back_ios,
62 + final _backButton = Icon(
63 + Icons.arrow_back_ios,
64 color: titleColor,
65 size: 16,
66 );
67 final _closeButton = currentTheme.type == ThemeType.dark
64 - ? closeButtonImageDarkTheme : closeButtonImage;
68 + ? closeButtonImageDarkTheme
69 + : closeButtonImage;
70
71 bool isMobileView = ResponsiveLayoutUtil.instance.isMobile;
72
@@ -78,7 +83,7 @@ class SendPage extends BasePage {
83 child: TextButton(
84 style: ButtonStyle(
85 overlayColor: MaterialStateColor.resolveWith(
81 - (states) => Colors.transparent),
86 + (states) => Colors.transparent),
87 ),
88 onPressed: () => onClose(context),
89 child: !isMobileView ? _closeButton : _backButton,
@@ -114,11 +119,13 @@ class SendPage extends BasePage {
119 mainAxisAlignment: MainAxisAlignment.center,
120 children: [
121 Padding(
117 - padding: const EdgeInsets.only(right:8.0),
118 - child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend),),
122 + padding: const EdgeInsets.only(right: 8.0),
123 + child: Observer(
124 + builder: (_) =>
125 + SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend),
126 + ),
127 ),
120 - if (supMiddle != null)
121 - supMiddle
128 + if (supMiddle != null) supMiddle
129 ],
130 );
131 }
@@ -200,12 +207,12 @@ class SendPage extends BasePage {
207 dotWidth: 6.0,
208 dotHeight: 6.0,
209 dotColor: Theme.of(context)
203 - .primaryTextTheme
204 - !.displaySmall!
210 + .primaryTextTheme!
211 + .displaySmall!
212 .backgroundColor!,
213 activeDotColor: Theme.of(context)
207 - .primaryTextTheme
208 - !.displayMedium!
214 + .primaryTextTheme!
215 + .displayMedium!
216 .backgroundColor!),
217 )
218 : Offstage();
@@ -339,8 +346,8 @@ class SendPage extends BasePage {
346 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
347 color: Colors.transparent,
348 textColor: Theme.of(context)
342 - .accentTextTheme
343 - !.displaySmall!
349 + .accentTextTheme!
350 + .displaySmall!
351 .decorationColor!,
352 ))),
353 if (sendViewModel.hasMultiRecipient)
@@ -357,13 +364,13 @@ class SendPage extends BasePage {
364 text: S.of(context).add_receiver,
365 color: Colors.transparent,
366 textColor: Theme.of(context)
360 - .accentTextTheme
361 - !.displaySmall!
367 + .accentTextTheme!
368 + .displaySmall!
369 .decorationColor!,
370 isDottedBorder: true,
371 borderColor: Theme.of(context)
365 - .primaryTextTheme
366 - !.displaySmall!
372 + .primaryTextTheme!
373 + .displaySmall!
374 .decorationColor!,
375 )),
376 Observer(
@@ -390,7 +397,16 @@ class SendPage extends BasePage {
397 return;
398 }
399
393 - await sendViewModel.createTransaction();
400 + final check = sendViewModel.shouldDisplayTotp();
401 + authService.authenticateAction(
402 + context,
403 + conditionToDetermineIfToUse2FA: check,
404 + onAuthSuccess: (value) async {
405 + if (value) {
406 + await sendViewModel.createTransaction();
407 + }
408 + },
409 + );
410 },
411 text: S.of(context).send,
412 color:
lib/src/screens/settings/security_backup_page.dart
+20 -3
@@ -30,12 +30,22 @@ class SecurityBackupPage extends BasePage {
30 child: Column(mainAxisSize: MainAxisSize.min, children: [
31 SettingsCellWithArrow(
32 title: S.current.show_keys,
33 - handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys),
33 + handler: (_) => _authService.authenticateAction(
34 + context,
35 + route: Routes.showKeys,
36 + conditionToDetermineIfToUse2FA: _securitySettingsViewModel
37 + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
38 + ),
39 ),
40 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
41 SettingsCellWithArrow(
42 title: S.current.create_backup,
38 - handler: (_) => _authService.authenticateAction(context, route: Routes.backup),
43 + handler: (_) => _authService.authenticateAction(
44 + context,
45 + route: Routes.backup,
46 + conditionToDetermineIfToUse2FA: _securitySettingsViewModel
47 + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
48 + ),
49 ),
50 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
51 SettingsCellWithArrow(
@@ -46,6 +56,8 @@ class SecurityBackupPage extends BasePage {
56 arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
57 setupPinContext.close();
58 },
59 + conditionToDetermineIfToUse2FA: _securitySettingsViewModel
60 + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
61 ),
62 ),
63 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
@@ -67,7 +79,10 @@ class SecurityBackupPage extends BasePage {
79 _securitySettingsViewModel
80 .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
81 }
70 - });
82 + },
83 + conditionToDetermineIfToUse2FA: _securitySettingsViewModel
84 + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
85 + );
86 } else {
87 _securitySettingsViewModel.setAllowBiometricalAuthentication(value);
88 }
@@ -94,6 +109,8 @@ class SecurityBackupPage extends BasePage {
109 route: _securitySettingsViewModel.useTotp2FA
110 ? Routes.modify2FAPage
111 : Routes.setup_2faPage,
112 + conditionToDetermineIfToUse2FA: _securitySettingsViewModel
113 + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
114 ),
115 );
116 },
lib/src/screens/settings/widgets/settings_choices_cell.dart
+4 -13
@@ -22,7 +22,7 @@ class SettingsChoicesCell extends StatelessWidget {
22 style: TextStyle(
23 fontSize: 14,
24 fontWeight: FontWeight.normal,
25 - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
25 + color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
26 ),
27 ),
28 ],
@@ -34,10 +34,7 @@ class SettingsChoicesCell extends StatelessWidget {
34 child: Container(
35 decoration: BoxDecoration(
36 borderRadius: BorderRadius.circular(30),
37 - color: Theme.of(context)
38 - .accentTextTheme!
39 - .displaySmall!
40 - .color!,
37 + color: Theme.of(context).accentTextTheme.displaySmall!.color!,
38 ),
39 child: Row(
40 mainAxisAlignment: MainAxisAlignment.center,
@@ -52,10 +49,7 @@ class SettingsChoicesCell extends StatelessWidget {
49 decoration: BoxDecoration(
50 borderRadius: BorderRadius.circular(30),
51 color: isSelected
55 - ? Theme.of(context)
56 - .accentTextTheme!
57 - .bodyLarge!
58 - .color!
52 + ? Theme.of(context).accentTextTheme.bodyLarge!.color!
53 : null,
54 ),
55 child: Text(
@@ -63,10 +57,7 @@ class SettingsChoicesCell extends StatelessWidget {
57 style: TextStyle(
58 color: isSelected
59 ? Colors.white
66 - : Theme.of(context)
67 - .primaryTextTheme!
68 - .bodySmall!
69 - .color!,
60 + : Theme.of(context).primaryTextTheme.bodySmall!.color!,
61 fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal,
62 ),
63 ),
lib/src/screens/setup_2fa/modify_2fa_page.dart
+145 -28
@@ -1,12 +1,16 @@
1 +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart';
4 +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
5 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
6 import 'package:cake_wallet/utils/show_pop_up.dart';
4 -import 'package:flutter/cupertino.dart';
7 +import 'package:cake_wallet/view_model/settings/choices_list_item.dart';
8 import 'package:flutter/material.dart';
9 import 'package:cake_wallet/src/screens/base_page.dart';
10 import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
11 import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
12 import 'package:cake_wallet/src/widgets/standard_list.dart';
13 +import 'package:flutter_mobx/flutter_mobx.dart';
14
15 import '../../../routes.dart';
16
@@ -21,35 +25,148 @@ class Modify2FAPage extends BasePage {
25 @override
26 Widget body(BuildContext context) {
27 return SingleChildScrollView(
24 - child: Column(
25 - crossAxisAlignment: CrossAxisAlignment.start,
26 - children: [
27 - SettingsCellWithArrow(
28 - title: S.current.disable_cake_2fa,
29 - handler: (_) async {
30 - await showPopUp<void>(
31 - context: context,
32 - builder: (BuildContext context) {
33 - return AlertWithTwoActions(
34 - alertTitle: S.current.disable_cake_2fa,
35 - alertContent: S.current.question_to_disable_2fa,
36 - leftButtonText: S.current.cancel,
37 - rightButtonText: S.current.disable,
38 - actionLeftButton: () {
39 - Navigator.of(context).pop();
40 - },
41 - actionRightButton: () {
42 - setup2FAViewModel.setUseTOTP2FA(false);
43 - Navigator.pushNamedAndRemoveUntil(
44 - context, Routes.dashboard, (route) => false);
45 - },
46 - );
28 + child: _2FAControlsWidget(setup2FAViewModel: setup2FAViewModel),
29 + );
30 + }
31 +}
32 +
33 +class _2FAControlsWidget extends StatelessWidget {
34 + const _2FAControlsWidget({required this.setup2FAViewModel});
35 +
36 + final Setup2FAViewModel setup2FAViewModel;
37 +
38 + @override
39 + Widget build(BuildContext context) {
40 + return Column(
41 + crossAxisAlignment: CrossAxisAlignment.start,
42 + children: [
43 + SettingsCellWithArrow(
44 + title: S.current.disable_cake_2fa,
45 + handler: (_) async {
46 + await showPopUp<void>(
47 + context: context,
48 + builder: (BuildContext context) {
49 + return AlertWithTwoActions(
50 + alertTitle: S.current.disable_cake_2fa,
51 + alertContent: S.current.question_to_disable_2fa,
52 + leftButtonText: S.current.cancel,
53 + rightButtonText: S.current.disable,
54 + actionLeftButton: () => Navigator.of(context).pop(),
55 + actionRightButton: () {
56 + setup2FAViewModel.setUseTOTP2FA(false);
57 + Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false);
58 },
59 );
49 - }),
50 - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
51 - ],
52 - ),
60 + },
61 + );
62 + },
63 + ),
64 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
65 + Observer(
66 + builder: (context) {
67 + return SettingsChoicesCell(
68 + ChoicesListItem<Cake2FAPresetsOptions>(
69 + title: S.current.cake_2fa_preset,
70 + onItemSelected: setup2FAViewModel.selectCakePreset,
71 + selectedItem: setup2FAViewModel.selectedCake2FAPreset,
72 + items: [
73 + Cake2FAPresetsOptions.narrow,
74 + Cake2FAPresetsOptions.normal,
75 + Cake2FAPresetsOptions.aggressive,
76 + ],
77 + ),
78 + );
79 + },
80 + ),
81 + Observer(
82 + builder: (context) {
83 + return SettingsSwitcherCell(
84 + title: S.current.require_for_assessing_wallet,
85 + value: setup2FAViewModel.shouldRequireTOTP2FAForAccessingWallet,
86 + onValueChange: (context, value) async =>
87 + setup2FAViewModel.switchShouldRequireTOTP2FAForAccessingWallet(value),
88 + );
89 + },
90 + ),
91 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
92 + Observer(
93 + builder: (context) {
94 + return SettingsSwitcherCell(
95 + title: S.current.require_for_sends_to_non_contacts,
96 + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToNonContact,
97 + onValueChange: (context, value) async =>
98 + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToNonContact(value),
99 + );
100 + },
101 + ),
102 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
103 + Observer(
104 + builder: (context) {
105 + return SettingsSwitcherCell(
106 + title: S.current.require_for_sends_to_contacts,
107 + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToContact,
108 + onValueChange: (context, value) async =>
109 + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToContact(value),
110 + );
111 + },
112 + ),
113 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
114 + Observer(
115 + builder: (context) {
116 + return SettingsSwitcherCell(
117 + title: S.current.require_for_sends_to_internal_wallets,
118 + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToInternalWallets,
119 + onValueChange: (context, value) async =>
120 + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToInternalWallets(value),
121 + );
122 + },
123 + ),
124 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
125 + Observer(
126 + builder: (context) {
127 + return SettingsSwitcherCell(
128 + title: S.current.require_for_exchanges_to_internal_wallets,
129 + value: setup2FAViewModel.shouldRequireTOTP2FAForExchangesToInternalWallets,
130 + onValueChange: (context, value) async =>
131 + setup2FAViewModel.switchShouldRequireTOTP2FAForExchangesToInternalWallets(value),
132 + );
133 + },
134 + ),
135 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
136 + Observer(
137 + builder: (context) {
138 + return SettingsSwitcherCell(
139 + title: S.current.require_for_adding_contacts,
140 + value: setup2FAViewModel.shouldRequireTOTP2FAForAddingContacts,
141 + onValueChange: (context, value) async =>
142 + setup2FAViewModel.switchShouldRequireTOTP2FAForAddingContacts(value),
143 + );
144 + },
145 + ),
146 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
147 + Observer(
148 + builder: (context) {
149 + return SettingsSwitcherCell(
150 + title: S.current.require_for_creating_new_wallets,
151 + value: setup2FAViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
152 + onValueChange: (context, value) async =>
153 + setup2FAViewModel.switchShouldRequireTOTP2FAForCreatingNewWallet(value),
154 + );
155 + },
156 + ),
157 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
158 + Observer(
159 + builder: (context) {
160 + return SettingsSwitcherCell(
161 + title: S.current.require_for_all_security_and_backup_settings,
162 + value: setup2FAViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
163 + onValueChange: (context, value) async => setup2FAViewModel
164 + .switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(value),
165 + );
166 + },
167 + ),
168 + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
169 + ],
170 );
171 }
172 }
lib/src/screens/setup_2fa/setup_2fa.dart
+2 -1
@@ -52,7 +52,8 @@ class Setup2FAPage extends BasePage {
52 SizedBox(height: 86),
53 SettingsCellWithArrow(
54 title: S.current.setup_totp_recommended,
55 - handler: (_) => Navigator.of(context).pushNamed(Routes.setup_2faQRPage),
55 + handler: (_) => Navigator.of(context)
56 + .pushReplacementNamed(Routes.setup_2faQRPage),
57 ),
58 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
59 ],
lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
+33 -17
@@ -18,7 +18,8 @@ import 'package:mobx/mobx.dart';
18 import '../../../palette.dart';
19 import '../../../routes.dart';
20
21 -typedef OnTotpAuthenticationFinished = void Function(bool, TotpAuthCodePageState);
21 +typedef OnTotpAuthenticationFinished = void Function(
22 + bool, TotpAuthCodePageState);
23
24 class TotpAuthCodePage extends StatefulWidget {
25 TotpAuthCodePage(
@@ -43,8 +44,9 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
44
45 @override
46 void initState() {
46 - if(widget.totpArguments.onTotpAuthenticationFinished != null) {
47 - _reaction ??= reaction((_) => widget.setup2FAViewModel.state, (ExecutionState state) {
47 + if (widget.totpArguments.onTotpAuthenticationFinished != null) {
48 + _reaction ??= reaction((_) => widget.setup2FAViewModel.state,
49 + (ExecutionState state) {
50 WidgetsBinding.instance.addPostFrameCallback((_) {
51 if (state is ExecutedSuccessfullyState) {
52 widget.totpArguments.onTotpAuthenticationFinished!(true, this);
@@ -57,9 +59,9 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
59
60 if (state is AuthenticationBanned) {
61 widget.totpArguments.onTotpAuthenticationFinished!(false, this);
60 - }
61 - });
62 - });
62 + }
63 + });
64 + });
65 }
66
67 super.initState();
@@ -73,7 +75,8 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
75
76 void changeProcessText(String text) {
77 dismissFlushBar(_authBar);
76 - _progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!);
78 + _progressBar = createBar<void>(text, duration: null)
79 + ..show(_key.currentContext!);
80 }
81
82 Future<void> close({String? route, dynamic arguments}) async {
@@ -82,7 +85,8 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> {
85 }
86 await Future<void>.delayed(Duration(milliseconds: 50));
87 if (route != null) {
85 - Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments);
88 + Navigator.of(_key.currentContext!)
89 + .pushReplacementNamed(route, arguments: arguments);
90 } else {
91 Navigator.of(_key.currentContext!).pop();
92 }
@@ -120,7 +124,8 @@ class TOTPEnterCode extends BasePage {
124 }
125
126 @override
123 - String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
127 + String get title =>
128 + isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
129
130 Widget? leading(BuildContext context) {
131 return isClosable ? super.leading(context) : null;
@@ -166,21 +171,24 @@ class TOTPEnterCode extends BasePage {
171 return PrimaryButton(
172 isDisabled: setup2FAViewModel.enteredOTPCode.length != 8,
173 onPressed: () async {
169 - final result =
170 - await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup);
171 - final bannedState = setup2FAViewModel.state is AuthenticationBanned;
174 + final result = await setup2FAViewModel.totp2FAAuth(
175 + totpController.text, isForSetup);
176 + final bannedState =
177 + setup2FAViewModel.state is AuthenticationBanned;
178
179 await showPopUp<void>(
180 context: context,
181 builder: (BuildContext context) {
182 return PopUpCancellableAlertDialog(
177 - contentText: _textDisplayedInPopupOnResult(result, bannedState, context),
183 + contentText: _textDisplayedInPopupOnResult(
184 + result, bannedState, context),
185 actionButtonText: S.of(context).ok,
186 buttonAction: () {
187 result ? setup2FAViewModel.success() : null;
188 if (isForSetup && result) {
182 - Navigator.pushNamedAndRemoveUntil(
183 - context, Routes.dashboard, (route) => false);
189 + Navigator.pop(context);
190 + // Navigator.of(context)
191 + // .popAndPushNamed(Routes.modify2FAPage);
192 } else {
193 Navigator.of(context).pop(result);
194 }
@@ -188,6 +196,11 @@ class TOTPEnterCode extends BasePage {
196 );
197 },
198 );
199 + if (isForSetup && result) {
200 + Navigator.pushReplacementNamed(
201 + context, Routes.modify2FAPage);
202 + }
203 +
204 },
205 text: S.of(context).continue_text,
206 color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
@@ -201,10 +214,13 @@ class TOTPEnterCode extends BasePage {
214 );
215 }
216
204 - String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) {
217 + String _textDisplayedInPopupOnResult(
218 + bool result, bool bannedState, BuildContext context) {
219 switch (result) {
220 case true:
207 - return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success;
221 + return isForSetup
222 + ? S.current.totp_2fa_success
223 + : S.current.totp_verification_success;
224 case false:
225 if (bannedState) {
226 final state = setup2FAViewModel.state as AuthenticationBanned;
lib/src/screens/wallet/wallet_edit_page.dart
+3 -1
@@ -125,7 +125,9 @@ class WalletEditPage extends BasePage {
125 }
126
127 _onSuccessfulAuth(context);
128 - });
128 + },
129 + conditionToDetermineIfToUse2FA: false,
130 + );
131 }
132
133 void _onSuccessfulAuth(BuildContext context) async {
lib/src/screens/wallet_list/wallet_list_page.dart
+67 -35
@@ -55,7 +55,7 @@ class WalletListBodyState extends State<WalletListBody> {
55 final newWalletImage =
56 Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white);
57 final restoreWalletImage = Image.asset('assets/images/restore_wallet.png',
58 - height: 12, width: 12, color: Theme.of(context).primaryTextTheme!.titleLarge!.color!);
58 + height: 12, width: 12, color: Theme.of(context).primaryTextTheme.titleLarge!.color!);
59
60 return Container(
61 padding: EdgeInsets.only(top: 16),
@@ -72,7 +72,7 @@ class WalletListBodyState extends State<WalletListBody> {
72 itemBuilder: (__, index) {
73 final wallet = widget.walletListViewModel.wallets[index];
74 final currentColor = wallet.isCurrent
75 - ? Theme.of(context).accentTextTheme!.titleSmall!.decorationColor!
75 + ? Theme.of(context).accentTextTheme.titleSmall!.decorationColor!
76 : Theme.of(context).colorScheme.background;
77 final row = GestureDetector(
78 onTap: () => wallet.isCurrent ? null : _loadWallet(wallet),
@@ -131,8 +131,7 @@ class WalletListBodyState extends State<WalletListBody> {
131 : Row(children: [
132 Expanded(child: row),
133 GestureDetector(
134 - onTap: () => Navigator.of(context).pushNamed(
135 - Routes.walletEdit,
134 + onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit,
135 arguments: [widget.walletListViewModel, wallet]),
136 child: Container(
137 padding: EdgeInsets.only(right: 20),
@@ -150,10 +149,7 @@ class WalletListBodyState extends State<WalletListBody> {
149 child: Icon(
150 Icons.edit,
151 size: 14,
153 - color: Theme.of(context)
154 - .textTheme
155 - .headlineMedium!
156 - .color!,
152 + color: Theme.of(context).textTheme.headlineMedium!.color!,
153 ),
154 ),
155 ),
@@ -167,27 +163,59 @@ class WalletListBodyState extends State<WalletListBody> {
163 bottomSection: Column(children: <Widget>[
164 PrimaryImageButton(
165 onPressed: () {
166 + //TODO(David): Find a way to optimize this
167 if (isSingleCoin) {
171 - Navigator.of(context).pushNamed(Routes.newWallet,
172 - arguments: widget.walletListViewModel.currentWalletType);
168 + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
169 + widget.authService.authenticateAction(
170 + context,
171 + route: Routes.newWallet,
172 + arguments: widget.walletListViewModel.currentWalletType,
173 + conditionToDetermineIfToUse2FA:
174 + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
175 + );
176 + } else {
177 + Navigator.of(context).pushNamed(
178 + Routes.newWallet,
179 + arguments: widget.walletListViewModel.currentWalletType,
180 + );
181 + }
182 } else {
174 - Navigator.of(context).pushNamed(Routes.newWalletType);
183 + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
184 + widget.authService.authenticateAction(
185 + context,
186 + route: Routes.newWalletType,
187 + conditionToDetermineIfToUse2FA:
188 + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
189 + );
190 + } else {
191 + Navigator.of(context).pushNamed(Routes.newWalletType);
192 + }
193 }
194 },
195 image: newWalletImage,
196 text: S.of(context).wallet_list_create_new_wallet,
179 - color: Theme.of(context).accentTextTheme!.bodyLarge!.color!,
197 + color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
198 textColor: Colors.white,
199 ),
200 SizedBox(height: 10.0),
201 PrimaryImageButton(
202 onPressed: () {
185 - Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
203 + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
204 + widget.authService.authenticateAction(
205 + context,
206 + route: Routes.restoreOptions,
207 + arguments: false,
208 + conditionToDetermineIfToUse2FA:
209 + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
210 + );
211 + } else {
212 + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
213 + }
214 },
215 image: restoreWalletImage,
216 text: S.of(context).wallet_list_restore_wallet,
189 - color: Theme.of(context).accentTextTheme!.bodySmall!.color!,
190 - textColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!)
217 + color: Theme.of(context).accentTextTheme.bodySmall!.color!,
218 + textColor: Theme.of(context).primaryTextTheme.titleLarge!.color!)
219 ])),
220 );
221 }
@@ -208,27 +236,31 @@ class WalletListBodyState extends State<WalletListBody> {
236 }
237
238 Future<void> _loadWallet(WalletListItem wallet) async {
211 - await widget.authService.authenticateAction(context,
212 - onAuthSuccess: (isAuthenticatedSuccessfully) async {
213 - if (!isAuthenticatedSuccessfully) {
214 - return;
215 - }
216 -
217 - try {
218 - changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
219 - await widget.walletListViewModel.loadWallet(wallet);
220 - await hideProgressText();
221 - // only pop the wallets route in mobile as it will go back to dashboard page
222 - // in desktop platforms the navigation tree is different
223 - if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
224 - WidgetsBinding.instance.addPostFrameCallback((_) {
225 - Navigator.of(context).pop();
226 - });
239 + await widget.authService.authenticateAction(
240 + context,
241 + onAuthSuccess: (isAuthenticatedSuccessfully) async {
242 + if (!isAuthenticatedSuccessfully) {
243 + return;
244 }
228 - } catch (e) {
229 - changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
230 - }
231 - });
245 +
246 + try {
247 + changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
248 + await widget.walletListViewModel.loadWallet(wallet);
249 + await hideProgressText();
250 + // only pop the wallets route in mobile as it will go back to dashboard page
251 + // in desktop platforms the navigation tree is different
252 + if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
253 + WidgetsBinding.instance.addPostFrameCallback((_) {
254 + Navigator.of(context).pop();
255 + });
256 + }
257 + } catch (e) {
258 + changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
259 + }
260 + },
261 + conditionToDetermineIfToUse2FA:
262 + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
263 + );
264 }
265
266 void changeProcessText(String text) {
lib/store/settings_store.dart
+163
@@ -1,6 +1,7 @@
1 import 'dart:io';
2
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
5 import 'package:cake_wallet/entities/exchange_api_mode.dart';
6 import 'package:cake_wallet/entities/pin_code_required_duration.dart';
7 import 'package:cake_wallet/entities/preferences_key.dart';
@@ -56,6 +57,15 @@ abstract class SettingsStoreBase with Store {
57 required this.isBitcoinBuyEnabled,
58 required this.actionlistDisplayMode,
59 required this.pinTimeOutDuration,
60 + required Cake2FAPresetsOptions initialCake2FAPresetOptions,
61 + required bool initialShouldRequireTOTP2FAForAccessingWallet,
62 + required bool initialShouldRequireTOTP2FAForSendsToContact,
63 + required bool initialShouldRequireTOTP2FAForSendsToNonContact,
64 + required bool initialShouldRequireTOTP2FAForSendsToInternalWallets,
65 + required bool initialShouldRequireTOTP2FAForExchangesToInternalWallets,
66 + required bool initialShouldRequireTOTP2FAForAddingContacts,
67 + required bool initialShouldRequireTOTP2FAForCreatingNewWallets,
68 + required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
69 TransactionPriority? initialBitcoinTransactionPriority,
70 TransactionPriority? initialMoneroTransactionPriority,
71 TransactionPriority? initialHavenTransactionPriority,
@@ -67,6 +77,7 @@ abstract class SettingsStoreBase with Store {
77 shouldSaveRecipientAddress = initialSaveRecipientAddress,
78 fiatApiMode = initialFiatMode,
79 allowBiometricalAuthentication = initialAllowBiometricalAuthentication,
80 + selectedCake2FAPreset = initialCake2FAPresetOptions,
81 totpSecretKey = initialTotpSecretKey,
82 useTOTP2FA = initialUseTOTP2FA,
83 numberOfFailedTokenTrials = initialFailedTokenTrial,
@@ -78,6 +89,18 @@ abstract class SettingsStoreBase with Store {
89 currentTheme = initialTheme,
90 pinCodeLength = initialPinLength,
91 languageCode = initialLanguageCode,
92 + shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet,
93 + shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact,
94 + shouldRequireTOTP2FAForSendsToNonContact = initialShouldRequireTOTP2FAForSendsToNonContact,
95 + shouldRequireTOTP2FAForSendsToInternalWallets =
96 + initialShouldRequireTOTP2FAForSendsToInternalWallets,
97 + shouldRequireTOTP2FAForExchangesToInternalWallets =
98 + initialShouldRequireTOTP2FAForExchangesToInternalWallets,
99 + shouldRequireTOTP2FAForAddingContacts = initialShouldRequireTOTP2FAForAddingContacts,
100 + shouldRequireTOTP2FAForCreatingNewWallets =
101 + initialShouldRequireTOTP2FAForCreatingNewWallets,
102 + shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
103 + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
104 priority = ObservableMap<WalletType, TransactionPriority>() {
105 //this.nodes = ObservableMap<WalletType, Node>.of(nodes);
106
@@ -166,6 +189,57 @@ abstract class SettingsStoreBase with Store {
189 (bool biometricalAuthentication) => sharedPreferences.setBool(
190 PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication));
191
192 + reaction(
193 + (_) => selectedCake2FAPreset,
194 + (Cake2FAPresetsOptions selectedCake2FAPreset) => sharedPreferences.setInt(
195 + PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset.serialize()));
196 +
197 + reaction(
198 + (_) => shouldRequireTOTP2FAForAccessingWallet,
199 + (bool requireTOTP2FAForAccessingWallet) => sharedPreferences.setBool(
200 + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet,
201 + requireTOTP2FAForAccessingWallet));
202 +
203 + reaction(
204 + (_) => shouldRequireTOTP2FAForSendsToContact,
205 + (bool requireTOTP2FAForSendsToContact) => sharedPreferences.setBool(
206 + PreferencesKey.shouldRequireTOTP2FAForSendsToContact, requireTOTP2FAForSendsToContact));
207 +
208 + reaction(
209 + (_) => shouldRequireTOTP2FAForSendsToNonContact,
210 + (bool requireTOTP2FAForSendsToNonContact) => sharedPreferences.setBool(
211 + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact,
212 + requireTOTP2FAForSendsToNonContact));
213 +
214 + reaction(
215 + (_) => shouldRequireTOTP2FAForSendsToInternalWallets,
216 + (bool requireTOTP2FAForSendsToInternalWallets) => sharedPreferences.setBool(
217 + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets,
218 + requireTOTP2FAForSendsToInternalWallets));
219 +
220 + reaction(
221 + (_) => shouldRequireTOTP2FAForExchangesToInternalWallets,
222 + (bool requireTOTP2FAForExchangesToInternalWallets) => sharedPreferences.setBool(
223 + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets,
224 + requireTOTP2FAForExchangesToInternalWallets));
225 +
226 + reaction(
227 + (_) => shouldRequireTOTP2FAForAddingContacts,
228 + (bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool(
229 + PreferencesKey.shouldRequireTOTP2FAForAddingContacts, requireTOTP2FAForAddingContacts));
230 +
231 + reaction(
232 + (_) => shouldRequireTOTP2FAForCreatingNewWallets,
233 + (bool requireTOTP2FAForCreatingNewWallets) => sharedPreferences.setBool(
234 + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets,
235 + requireTOTP2FAForCreatingNewWallets));
236 +
237 + reaction(
238 + (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
239 + (bool requireTOTP2FAForAllSecurityAndBackupSettings) => sharedPreferences.setBool(
240 + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
241 + requireTOTP2FAForAllSecurityAndBackupSettings));
242 +
243 reaction(
244 (_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use));
245
@@ -249,6 +323,33 @@ abstract class SettingsStoreBase with Store {
323 @observable
324 bool allowBiometricalAuthentication;
325
326 + @observable
327 + bool shouldRequireTOTP2FAForAccessingWallet;
328 +
329 + @observable
330 + bool shouldRequireTOTP2FAForSendsToContact;
331 +
332 + @observable
333 + bool shouldRequireTOTP2FAForSendsToNonContact;
334 +
335 + @observable
336 + bool shouldRequireTOTP2FAForSendsToInternalWallets;
337 +
338 + @observable
339 + bool shouldRequireTOTP2FAForExchangesToInternalWallets;
340 +
341 + @observable
342 + Cake2FAPresetsOptions selectedCake2FAPreset;
343 +
344 + @observable
345 + bool shouldRequireTOTP2FAForAddingContacts;
346 +
347 + @observable
348 + bool shouldRequireTOTP2FAForCreatingNewWallets;
349 +
350 + @observable
351 + bool shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
352 +
353 @observable
354 String totpSecretKey;
355
@@ -356,6 +457,29 @@ abstract class SettingsStoreBase with Store {
457 FiatApiMode.enabled.raw);
458 final allowBiometricalAuthentication =
459 sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false;
460 + final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
461 + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
462 + Cake2FAPresetsOptions.normal.raw);
463 + final shouldRequireTOTP2FAForAccessingWallet =
464 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false;
465 + final shouldRequireTOTP2FAForSendsToContact =
466 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false;
467 + final shouldRequireTOTP2FAForSendsToNonContact =
468 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false;
469 + final shouldRequireTOTP2FAForSendsToInternalWallets =
470 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ??
471 + false;
472 + final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences
473 + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ??
474 + false;
475 + final shouldRequireTOTP2FAForAddingContacts =
476 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false;
477 + final shouldRequireTOTP2FAForCreatingNewWallets =
478 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ??
479 + false;
480 + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences
481 + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ??
482 + false;
483 final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? '';
484 final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false;
485 final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0;
@@ -433,6 +557,7 @@ abstract class SettingsStoreBase with Store {
557 initialDisableSell: disableSell,
558 initialFiatMode: currentFiatApiMode,
559 initialAllowBiometricalAuthentication: allowBiometricalAuthentication,
560 + initialCake2FAPresetOptions: selectedCake2FAPreset,
561 initialTotpSecretKey: totpSecretKey,
562 initialUseTOTP2FA: useTOTP2FA,
563 initialFailedTokenTrial: tokenTrialNumber,
@@ -446,6 +571,17 @@ abstract class SettingsStoreBase with Store {
571 initialBitcoinTransactionPriority: bitcoinTransactionPriority,
572 initialHavenTransactionPriority: havenTransactionPriority,
573 initialLitecoinTransactionPriority: litecoinTransactionPriority,
574 + initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet,
575 + initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact,
576 + initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact,
577 + initialShouldRequireTOTP2FAForSendsToInternalWallets:
578 + shouldRequireTOTP2FAForSendsToInternalWallets,
579 + initialShouldRequireTOTP2FAForExchangesToInternalWallets:
580 + shouldRequireTOTP2FAForExchangesToInternalWallets,
581 + initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts,
582 + initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets,
583 + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings:
584 + shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
585 shouldShowYatPopup: shouldShowYatPopup);
586 }
587
@@ -480,6 +616,7 @@ abstract class SettingsStoreBase with Store {
616 shouldSaveRecipientAddress;
617 totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey;
618 useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA;
619 +
620 numberOfFailedTokenTrials =
621 sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
622 sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ??
@@ -490,9 +627,35 @@ abstract class SettingsStoreBase with Store {
627 allowBiometricalAuthentication =
628 sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ??
629 allowBiometricalAuthentication;
630 + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
631 + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
632 + Cake2FAPresetsOptions.normal.raw);
633 + shouldRequireTOTP2FAForAccessingWallet =
634 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false;
635 + shouldRequireTOTP2FAForSendsToContact =
636 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false;
637 + shouldRequireTOTP2FAForSendsToNonContact =
638 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false;
639 + shouldRequireTOTP2FAForSendsToInternalWallets =
640 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ??
641 + false;
642 + shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences
643 + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ??
644 + false;
645 + shouldRequireTOTP2FAForAddingContacts =
646 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false;
647 + shouldRequireTOTP2FAForCreatingNewWallets =
648 + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ??
649 + false;
650 + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences
651 + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ??
652 + false;
653 shouldShowMarketPlaceInDashboard =
654 sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ??
655 shouldShowMarketPlaceInDashboard;
656 + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
657 + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
658 + Cake2FAPresetsOptions.narrow.raw);
659 exchangeStatus = ExchangeApiMode.deserialize(
660 raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ??
661 ExchangeApiMode.enabled.raw);
lib/view_model/contact_list/contact_list_view_model.dart
+16 -6
@@ -1,5 +1,6 @@
1 import 'dart:async';
2 import 'package:cake_wallet/entities/wallet_contact.dart';
3 +import 'package:cake_wallet/store/settings_store.dart';
4 import 'package:cw_core/wallet_info.dart';
5 import 'package:cw_core/wallet_type.dart';
6 import 'package:hive/hive.dart';
@@ -11,10 +12,12 @@ import 'package:cw_core/crypto_currency.dart';
12
13 part 'contact_list_view_model.g.dart';
14
14 -class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel;
15 +class ContactListViewModel = ContactListViewModelBase
16 + with _$ContactListViewModel;
17
18 abstract class ContactListViewModelBase with Store {
17 - ContactListViewModelBase(this.contactSource, this.walletInfoSource, this._currency)
19 + ContactListViewModelBase(this.contactSource, this.walletInfoSource,
20 + this._currency, this.settingsStore)
21 : contacts = ObservableList<ContactRecord>(),
22 walletContacts = [] {
23 walletInfoSource.values.forEach((info) {
@@ -42,16 +45,23 @@ abstract class ContactListViewModelBase with Store {
45 final List<WalletContact> walletContacts;
46 final CryptoCurrency? _currency;
47 StreamSubscription<BoxEvent>? _subscription;
48 + final SettingsStore settingsStore;
49
50 bool get isEditable => _currency == null;
51
52 + @computed
53 + bool get shouldRequireTOTP2FAForAddingContacts =>
54 + settingsStore.shouldRequireTOTP2FAForAddingContacts;
55 +
56 Future<void> delete(ContactRecord contact) async => contact.original.delete();
57
58 @computed
51 - List<ContactRecord> get contactsToShow =>
52 - contacts.where((element) => _currency == null || element.type == _currency).toList();
59 + List<ContactRecord> get contactsToShow => contacts
60 + .where((element) => _currency == null || element.type == _currency)
61 + .toList();
62
63 @computed
55 - List<WalletContact> get walletContactsToShow =>
56 - walletContacts.where((element) => _currency == null || element.type == _currency).toList();
64 + List<WalletContact> get walletContactsToShow => walletContacts
65 + .where((element) => _currency == null || element.type == _currency)
66 + .toList();
67 }
lib/view_model/exchange/exchange_view_model.dart
+148 -88
@@ -4,12 +4,14 @@ import 'dart:convert';
4
5 import 'package:cake_wallet/entities/exchange_api_mode.dart';
6 import 'package:cake_wallet/entities/preferences_key.dart';
7 +import 'package:cake_wallet/entities/wallet_contact.dart';
8 import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
9 import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
10 import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
11 import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart';
12 import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart';
13 import 'package:cake_wallet/exchange/trocador/trocador_request.dart';
14 +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
15 import 'package:cw_core/transaction_priority.dart';
16 import 'package:cw_core/wallet_base.dart';
17 import 'package:cw_core/crypto_currency.dart';
@@ -44,42 +46,55 @@ part 'exchange_view_model.g.dart';
46 class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
47
48 abstract class ExchangeViewModelBase with Store {
47 - ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
48 - this.tradesStore, this._settingsStore, this.sharedPreferences)
49 - : _cryptoNumberFormat = NumberFormat(),
50 - isFixedRateMode = false,
51 - isReceiveAmountEntered = false,
52 - depositAmount = '',
53 - receiveAmount = '',
54 - receiveAddress = '',
55 - depositAddress = '',
56 - isDepositAddressEnabled = false,
57 - isReceiveAddressEnabled = false,
58 - isReceiveAmountEditable = false,
59 - _useTorOnly = false,
60 - receiveCurrencies = <CryptoCurrency>[],
61 - depositCurrencies = <CryptoCurrency>[],
62 - limits = Limits(min: 0, max: 0),
63 - tradeState = ExchangeTradeStateInitial(),
64 - limitsState = LimitsInitialState(),
65 - receiveCurrency = wallet.currency,
66 - depositCurrency = wallet.currency,
67 - providerList = [],
68 - selectedProviders = ObservableList<ExchangeProvider>() {
49 + ExchangeViewModelBase(
50 + this.wallet,
51 + this.trades,
52 + this._exchangeTemplateStore,
53 + this.tradesStore,
54 + this._settingsStore,
55 + this.sharedPreferences,
56 + this.contactListViewModel)
57 + : _cryptoNumberFormat = NumberFormat(),
58 + isFixedRateMode = false,
59 + isReceiveAmountEntered = false,
60 + depositAmount = '',
61 + receiveAmount = '',
62 + receiveAddress = '',
63 + depositAddress = '',
64 + isDepositAddressEnabled = false,
65 + isReceiveAddressEnabled = false,
66 + isReceiveAmountEditable = false,
67 + _useTorOnly = false,
68 + receiveCurrencies = <CryptoCurrency>[],
69 + depositCurrencies = <CryptoCurrency>[],
70 + limits = Limits(min: 0, max: 0),
71 + tradeState = ExchangeTradeStateInitial(),
72 + limitsState = LimitsInitialState(),
73 + receiveCurrency = wallet.currency,
74 + depositCurrency = wallet.currency,
75 + providerList = [],
76 + selectedProviders = ObservableList<ExchangeProvider>() {
77 _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly;
78 _setProviders();
79 const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano];
72 - const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp,
73 - CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano];
80 + const excludeReceiveCurrencies = [
81 + CryptoCurrency.xlm,
82 + CryptoCurrency.xrp,
83 + CryptoCurrency.bnb,
84 + CryptoCurrency.btt,
85 + CryptoCurrency.nano
86 + ];
87 _initialPairBasedOnWallet();
88
76 - final Map<String, dynamic> exchangeProvidersSelection = json
77 - .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
89 + final Map<String, dynamic> exchangeProvidersSelection = json.decode(
90 + sharedPreferences
91 + .getString(PreferencesKey.exchangeProvidersSelection) ??
92 + "{}") as Map<String, dynamic>;
93
94 /// if the provider is not in the user settings (user's first time or newly added provider)
95 /// then use its default value decided by us
81 - selectedProviders = ObservableList.of(providersForCurrentPair().where(
82 - (element) => exchangeProvidersSelection[element.title] == null
96 + selectedProviders = ObservableList.of(providersForCurrentPair()
97 + .where((element) => exchangeProvidersSelection[element.title] == null
98 ? element.isEnabled
99 : (exchangeProvidersSelection[element.title] as bool))
100 .toList());
@@ -87,7 +102,8 @@ abstract class ExchangeViewModelBase with Store {
102 _setAvailableProviders();
103 _calculateBestRate();
104
90 - bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate());
105 + bestRateSync =
106 + Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate());
107
108 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
109 isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
@@ -95,7 +111,8 @@ abstract class ExchangeViewModelBase with Store {
111 receiveAmount = '';
112 receiveAddress = '';
113 depositAddress = depositCurrency == wallet.currency
98 - ? wallet.walletAddresses.address : '';
114 + ? wallet.walletAddresses.address
115 + : '';
116 provider = providersForCurrentPair().first;
117 final initialProvider = provider;
118 provider!.checkIsAvailable().then((bool isAvailable) {
@@ -107,20 +124,20 @@ abstract class ExchangeViewModelBase with Store {
124 }
125 });
126 receiveCurrencies = CryptoCurrency.all
110 - .where((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency))
111 - .toList();
127 + .where((cryptoCurrency) =>
128 + !excludeReceiveCurrencies.contains(cryptoCurrency))
129 + .toList();
130 depositCurrencies = CryptoCurrency.all
113 - .where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency))
114 - .toList();
131 + .where((cryptoCurrency) =>
132 + !excludeDepositCurrencies.contains(cryptoCurrency))
133 + .toList();
134 _defineIsReceiveAmountEditable();
135 loadLimits();
117 - reaction(
118 - (_) => isFixedRateMode,
119 - (Object _) {
120 - loadLimits();
121 - _bestRate = 0;
122 - _calculateBestRate();
123 - });
136 + reaction((_) => isFixedRateMode, (Object _) {
137 + loadLimits();
138 + _bestRate = 0;
139 + _calculateBestRate();
140 + });
141 }
142 bool _useTorOnly;
143 final WalletBase wallet;
@@ -148,7 +165,8 @@ abstract class ExchangeViewModelBase with Store {
165 /// initialize with descending comparator
166 /// since we want largest rate first
167 final SplayTreeMap<double, ExchangeProvider> _sortedAvailableProviders =
151 - SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
168 + SplayTreeMap<double, ExchangeProvider>(
169 + (double a, double b) => b.compareTo(a));
170
171 final List<ExchangeProvider> _tradeAvailableProviders = [];
172
@@ -207,6 +225,37 @@ abstract class ExchangeViewModelBase with Store {
225 ObservableList<ExchangeTemplate> get templates =>
226 _exchangeTemplateStore.templates;
227
228 + @computed
229 + List<WalletContact> get walletContactsToShow =>
230 + contactListViewModel.walletContacts
231 + .where((element) =>
232 + receiveCurrency == null || element.type == receiveCurrency)
233 + .toList();
234 +
235 + @action
236 + bool checkIfWalletIsAnInternalWallet(String address) {
237 + final walletContactList = walletContactsToShow
238 + .where((element) => element.address == address)
239 + .toList();
240 +
241 + return walletContactList.isNotEmpty;
242 + }
243 +
244 + @computed
245 + bool get shouldDisplayTOTP2FAForExchangesToInternalWallet =>
246 + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
247 +
248 + //* Still open to further optimize these checks
249 + //* It works but can be made better
250 + @action
251 + bool shouldDisplayTOTP() {
252 + final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress);
253 + if (isInternalWallet) {
254 + return shouldDisplayTOTP2FAForExchangesToInternalWallet;
255 + }
256 + return false;
257 + }
258 +
259
260 @computed
261 TransactionPriority get transactionPriority {
@@ -219,21 +268,23 @@ abstract class ExchangeViewModelBase with Store {
268 return priority;
269 }
270
222 -
271 bool get hasAllAmount =>
272 (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && depositCurrency == wallet.currency;
273
226 - bool get isMoneroWallet => wallet.type == WalletType.monero;
274 + bool get isMoneroWallet => wallet.type == WalletType.monero;
275
228 - bool get isLowFee {
276 + bool get isLowFee {
277 switch (wallet.type) {
278 case WalletType.monero:
279 case WalletType.haven:
232 - return transactionPriority == monero!.getMoneroTransactionPrioritySlow();
280 + return transactionPriority ==
281 + monero!.getMoneroTransactionPrioritySlow();
282 case WalletType.bitcoin:
234 - return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow();
283 + return transactionPriority ==
284 + bitcoin!.getBitcoinTransactionPrioritySlow();
285 case WalletType.litecoin:
236 - return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow();
286 + return transactionPriority ==
287 + bitcoin!.getLitecoinTransactionPrioritySlow();
288 default:
289 return false;
290 }
@@ -247,6 +298,8 @@ abstract class ExchangeViewModelBase with Store {
298
299 final SettingsStore _settingsStore;
300
301 + final ContactListViewModel contactListViewModel;
302 +
303 double _bestRate = 0.0;
304
305 late Timer bestRateSync;
@@ -337,23 +390,24 @@ abstract class ExchangeViewModelBase with Store {
390 }
391
392 Future<void> _calculateBestRate() async {
340 - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1;
393 + final amount =
394 + double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1;
395
396 final _providers = _tradeAvailableProviders
343 - .where((element) => !isFixedRateMode || element.supportsFixedRate).toList();
344 -
345 - final result = await Future.wait<double>(
346 - _providers.map((element) => element.fetchRate(
347 - from: depositCurrency,
348 - to: receiveCurrency,
349 - amount: amount,
350 - isFixedRateMode: isFixedRateMode,
351 - isReceiveAmount: isFixedRateMode))
352 - );
397 + .where((element) => !isFixedRateMode || element.supportsFixedRate)
398 + .toList();
399 +
400 + final result = await Future.wait<double>(_providers.map((element) =>
401 + element.fetchRate(
402 + from: depositCurrency,
403 + to: receiveCurrency,
404 + amount: amount,
405 + isFixedRateMode: isFixedRateMode,
406 + isReceiveAmount: isFixedRateMode)));
407
408 _sortedAvailableProviders.clear();
409
356 - for (int i=0;i<result.length;i++) {
410 + for (int i = 0; i < result.length; i++) {
411 if (result[i] != 0) {
412 /// add this provider as its valid for this trade
413 try {
@@ -377,12 +431,8 @@ abstract class ExchangeViewModelBase with Store {
431
432 limitsState = LimitsIsLoading();
433
380 - final from = isFixedRateMode
381 - ? receiveCurrency
382 - : depositCurrency;
383 - final to = isFixedRateMode
384 - ? depositCurrency
385 - : receiveCurrency;
434 + final from = isFixedRateMode ? receiveCurrency : depositCurrency;
435 + final to = isFixedRateMode ? depositCurrency : receiveCurrency;
436
437 double? lowestMin = double.maxFinite;
438 double? highestMax = 0.0;
@@ -396,14 +446,13 @@ abstract class ExchangeViewModelBase with Store {
446
447 try {
448 final tempLimits = await provider.fetchLimits(
399 - from: from,
400 - to: to,
401 - isFixedRateMode: isFixedRateMode);
449 + from: from, to: to, isFixedRateMode: isFixedRateMode);
450
451 if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin) {
452 lowestMin = tempLimits.min;
453 }
406 - if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax) {
454 + if (highestMax != null &&
455 + (tempLimits.max ?? double.maxFinite) > highestMax) {
456 highestMax = tempLimits.max;
457 }
458 } catch (e) {
@@ -445,7 +494,7 @@ abstract class ExchangeViewModelBase with Store {
494 settleMethod: receiveCurrency,
495 depositAmount: isFixedRateMode
496 ? receiveAmount.replaceAll(',', '.')
448 - : depositAmount.replaceAll(',', '.'),
497 + : depositAmount.replaceAll(',', '.'),
498 settleAddress: receiveAddress,
499 refundAddress: depositAddress,
500 );
@@ -525,6 +574,7 @@ abstract class ExchangeViewModelBase with Store {
574 tradesStore.setTrade(trade);
575 await trades.add(trade);
576 tradeState = TradeIsCreatedSuccessfully(trade: trade);
577 +
578 /// return after the first successful trade
579 return;
580 } catch (e) {
@@ -555,9 +605,11 @@ abstract class ExchangeViewModelBase with Store {
605 depositAmount = '';
606 receiveAmount = '';
607 depositAddress = depositCurrency == wallet.currency
558 - ? wallet.walletAddresses.address : '';
608 + ? wallet.walletAddresses.address
609 + : '';
610 receiveAddress = receiveCurrency == wallet.currency
560 - ? wallet.walletAddresses.address : '';
611 + ? wallet.walletAddresses.address
612 + : '';
613 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
614 isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
615 isFixedRateMode = false;
@@ -576,7 +628,8 @@ abstract class ExchangeViewModelBase with Store {
628 }
629
630 final amount = availableBalance - fee;
579 - changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
631 + changeDepositAmount(
632 + amount: bitcoin!.formatterBitcoinAmountToString(amount: amount));
633 }
634 }
635
@@ -612,8 +665,7 @@ abstract class ExchangeViewModelBase with Store {
665 {required CryptoCurrency from, required CryptoCurrency to}) {
666 final providers = providerList
667 .where((provider) => provider.pairList
615 - .where((pair) =>
616 - pair.from == from && pair.to == to)
668 + .where((pair) => pair.from == from && pair.to == to)
669 .isNotEmpty)
670 .toList();
671
@@ -690,11 +742,14 @@ abstract class ExchangeViewModelBase with Store {
742 _bestRate = 0;
743 _calculateBestRate();
744
693 - final Map<String, dynamic> exchangeProvidersSelection = json
694 - .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
745 + final Map<String, dynamic> exchangeProvidersSelection = json.decode(
746 + sharedPreferences
747 + .getString(PreferencesKey.exchangeProvidersSelection) ??
748 + "{}") as Map<String, dynamic>;
749
750 for (var provider in providerList) {
697 - exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider);
751 + exchangeProvidersSelection[provider.title] =
752 + selectedProviders.contains(provider);
753 }
754
755 sharedPreferences.setString(
@@ -705,15 +760,15 @@ abstract class ExchangeViewModelBase with Store {
760
761 bool get isAvailableInSelected {
762 final providersForPair = providersForCurrentPair();
708 - return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element));
763 + return selectedProviders.any(
764 + (element) => element.isAvailable && providersForPair.contains(element));
765 }
766
767 void _setAvailableProviders() {
768 _tradeAvailableProviders.clear();
769
714 - _tradeAvailableProviders.addAll(
715 - selectedProviders
716 - .where((provider) => providersForCurrentPair().contains(provider)));
770 + _tradeAvailableProviders.addAll(selectedProviders
771 + .where((provider) => providersForCurrentPair().contains(provider)));
772 }
773
774 @action
@@ -721,22 +776,27 @@ abstract class ExchangeViewModelBase with Store {
776 switch (wallet.type) {
777 case WalletType.monero:
778 case WalletType.haven:
724 - _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic();
779 + _settingsStore.priority[wallet.type] =
780 + monero!.getMoneroTransactionPriorityAutomatic();
781 break;
782 case WalletType.bitcoin:
727 - _settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium();
783 + _settingsStore.priority[wallet.type] =
784 + bitcoin!.getBitcoinTransactionPriorityMedium();
785 break;
786 case WalletType.litecoin:
730 - _settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium();
787 + _settingsStore.priority[wallet.type] =
788 + bitcoin!.getLitecoinTransactionPriorityMedium();
789 break;
790 default:
791 break;
792 }
793 }
794
737 - void _setProviders(){
795 + void _setProviders() {
796 if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) {
739 - providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList();
797 + providerList = _allProviders
798 + .where((provider) => provider.supportsOnionAddress)
799 + .toList();
800 } else {
801 providerList = _allProviders;
802 }
lib/view_model/send/send_view_model.dart
+86 -24
@@ -1,6 +1,8 @@
1 -import 'package:cake_wallet/entities/balance_display_mode.dart';
1 +import 'package:cake_wallet/entities/contact_record.dart';
2 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
3 import 'package:cake_wallet/entities/transaction_description.dart';
4 +import 'package:cake_wallet/entities/wallet_contact.dart';
5 +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
6 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
7 import 'package:cw_core/transaction_priority.dart';
8 import 'package:cake_wallet/view_model/send/output.dart';
@@ -38,6 +40,7 @@ abstract class SendViewModelBase with Store {
40 this.sendTemplateViewModel,
41 this._fiatConversationStore,
42 this.balanceViewModel,
43 + this.contactListViewModel,
44 this.transactionDescriptionBox)
45 : state = InitialExecutionState(),
46 currencies = _wallet.balance.keys.toList(),
@@ -50,8 +53,9 @@ abstract class SendViewModelBase with Store {
53 if (!priorityForWalletType(_wallet.type).contains(priority)) {
54 _settingsStore.priority[_wallet.type] = priorities.first;
55 }
53 -
54 - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
56 +
57 + outputs
58 + .add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
59 }
60
61 @observable
@@ -61,7 +65,8 @@ abstract class SendViewModelBase with Store {
65
66 @action
67 void addOutput() {
64 - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
68 + outputs
69 + .add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
70 }
71
72 @action
@@ -148,13 +153,11 @@ abstract class SendViewModelBase with Store {
153
154 @computed
155 String get pendingTransactionFiatAmountFormatted =>
151 - isFiatDisabled ? '' : pendingTransactionFiatAmount +
152 - ' ' + fiat.title;
156 + isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title;
157
158 @computed
159 String get pendingTransactionFeeFiatAmountFormatted =>
156 - isFiatDisabled ? '' : pendingTransactionFeeFiatAmount +
157 - ' ' + fiat.title;
160 + isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title;
161
162 @computed
163 bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus;
@@ -175,9 +178,8 @@ abstract class SendViewModelBase with Store {
178
179 bool get hasMultiRecipient => _wallet.type != WalletType.haven;
180
178 - bool get hasYat => outputs.any((out) =>
179 - out.isParsedAddress &&
180 - out.parsedAddress.parseFrom == ParseFrom.yatRecord);
181 + bool get hasYat => outputs
182 + .any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);
183
184 WalletType get walletType => _wallet.type;
185
@@ -193,9 +195,73 @@ abstract class SendViewModelBase with Store {
195 final SettingsStore _settingsStore;
196 final SendTemplateViewModel sendTemplateViewModel;
197 final BalanceViewModel balanceViewModel;
198 + final ContactListViewModel contactListViewModel;
199 final FiatConversionStore _fiatConversationStore;
200 final Box<TransactionDescription> transactionDescriptionBox;
201
202 + @computed
203 + List<ContactRecord> get contactsToShow => contactListViewModel.contacts
204 + .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency)
205 + .toList();
206 +
207 + @computed
208 + List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
209 + .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency)
210 + .toList();
211 +
212 + @action
213 + bool checkIfAddressIsAContact(String address) {
214 + final contactList = contactsToShow.where((element) => element.address == address).toList();
215 +
216 + return contactList.isNotEmpty;
217 + }
218 +
219 + @action
220 + bool checkIfWalletIsAnInternalWallet(String address) {
221 + final walletContactList =
222 + walletContactsToShow.where((element) => element.address == address).toList();
223 +
224 + return walletContactList.isNotEmpty;
225 + }
226 +
227 + @computed
228 + bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact;
229 +
230 + @computed
231 + bool get shouldDisplayTOTP2FAForNonContact =>
232 + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
233 +
234 + @computed
235 + bool get shouldDisplayTOTP2FAForSendsToInternalWallet =>
236 + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
237 +
238 + //* Still open to further optimize these checks
239 + //* It works but can be made better
240 + @action
241 + bool checkThroughChecksToDisplayTOTP(String address) {
242 + final isContact = checkIfAddressIsAContact(address);
243 + final isInternalWallet = checkIfWalletIsAnInternalWallet(address);
244 +
245 + if (isContact) {
246 + return shouldDisplayTOTP2FAForContact;
247 + } else if (isInternalWallet) {
248 + return shouldDisplayTOTP2FAForSendsToInternalWallet;
249 + } else {
250 + return shouldDisplayTOTP2FAForNonContact;
251 + }
252 + }
253 +
254 + bool shouldDisplayTotp() {
255 + List<bool> conditionsList = [];
256 +
257 + for (var output in outputs) {
258 + final show = checkThroughChecksToDisplayTOTP(output.address);
259 + conditionsList.add(show);
260 + }
261 +
262 + return conditionsList.contains(true);
263 + }
264 +
265 @action
266 Future<void> createTransaction() async {
267 try {
@@ -234,11 +300,9 @@ abstract class SendViewModelBase with Store {
300 if (pendingTransaction!.id.isNotEmpty) {
301 _settingsStore.shouldSaveRecipientAddress
302 ? await transactionDescriptionBox.add(TransactionDescription(
237 - id: pendingTransaction!.id,
238 - recipientAddress: address,
239 - transactionNote: note))
240 - : await transactionDescriptionBox.add(TransactionDescription(
241 - id: pendingTransaction!.id, transactionNote: note));
303 + id: pendingTransaction!.id, recipientAddress: address, transactionNote: note))
304 + : await transactionDescriptionBox
305 + .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note));
306 }
307
308 state = TransactionCommitted();
@@ -276,15 +340,15 @@ abstract class SendViewModelBase with Store {
340 throw Exception('Priority is null for wallet type: ${_wallet.type}');
341 }
342
279 - return monero!.createMoneroTransactionCreationCredentials(
280 - outputs: outputs, priority: priority);
343 + return monero!
344 + .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority);
345 case WalletType.haven:
346 final priority = _settingsStore.priority[_wallet.type];
347
348 if (priority == null) {
349 throw Exception('Priority is null for wallet type: ${_wallet.type}');
350 }
287 -
351 +
352 return haven!.createHavenTransactionCreationCredentials(
353 outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title);
354 default:
@@ -304,14 +368,12 @@ abstract class SendViewModelBase with Store {
368 return priority.toString();
369 }
370
307 - bool _isEqualCurrency(String currency) =>
371 + bool _isEqualCurrency(String currency) =>
372 currency.toLowerCase() == _wallet.currency.title.toLowerCase();
373
374 @action
311 - void onClose() =>
312 - _settingsStore.fiatCurrency = fiatFromSettings;
375 + void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
376
377 @action
315 - void setFiatCurrency(FiatCurrency fiat) =>
316 - _settingsStore.fiatCurrency = fiat;
378 + void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat;
379 }
lib/view_model/set_up_2fa_viewmodel.dart
+262
@@ -1,5 +1,6 @@
1 // ignore_for_file: prefer_final_fields
2
3 +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 import 'package:cake_wallet/utils/totp_utils.dart' as Utils;
6 import 'package:cake_wallet/view_model/auth_state.dart';
@@ -23,8 +24,11 @@ abstract class Setup2FAViewModelBase with Store {
24 Setup2FAViewModelBase(this._settingsStore, this._sharedPreferences, this._authService)
25 : _failureCounter = 0,
26 enteredOTPCode = '',
27 + unhighlightTabs = false,
28 + selected2FASettings = ObservableList<VerboseControlSettings>(),
29 state = InitialExecutionState() {
30 _getRandomBase32SecretKey();
31 + selectCakePreset(selectedCake2FAPreset);
32 reaction((_) => state, _saveLastAuthTime);
33 }
34
@@ -48,6 +52,38 @@ abstract class Setup2FAViewModelBase with Store {
52 @computed
53 bool get useTOTP2FA => _settingsStore.useTOTP2FA;
54
55 + @computed
56 + bool get shouldRequireTOTP2FAForAccessingWallet =>
57 + _settingsStore.shouldRequireTOTP2FAForAccessingWallet;
58 +
59 + @computed
60 + bool get shouldRequireTOTP2FAForSendsToContact =>
61 + _settingsStore.shouldRequireTOTP2FAForSendsToContact;
62 +
63 + @computed
64 + bool get shouldRequireTOTP2FAForSendsToNonContact =>
65 + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
66 +
67 + @computed
68 + bool get shouldRequireTOTP2FAForSendsToInternalWallets =>
69 + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
70 +
71 + @computed
72 + bool get shouldRequireTOTP2FAForExchangesToInternalWallets =>
73 + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
74 +
75 + @computed
76 + bool get shouldRequireTOTP2FAForAddingContacts =>
77 + _settingsStore.shouldRequireTOTP2FAForAddingContacts;
78 +
79 + @computed
80 + bool get shouldRequireTOTP2FAForCreatingNewWallets =>
81 + _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets;
82 +
83 + @computed
84 + bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings =>
85 + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
86 +
87 void _getRandomBase32SecretKey() {
88 final randomBase32Key = Utils.generateRandomBase32SecretKey(16);
89 _setBase32SecretKey(randomBase32Key);
@@ -156,4 +192,230 @@ abstract class Setup2FAViewModelBase with Store {
192 _authService.saveLastAuthTime();
193 }
194 }
195 +
196 + @computed
197 + Cake2FAPresetsOptions get selectedCake2FAPreset => _settingsStore.selectedCake2FAPreset;
198 +
199 + @observable
200 + bool unhighlightTabs = false;
201 +
202 + @observable
203 + ObservableList<VerboseControlSettings> selected2FASettings;
204 +
205 + //! The code here works, but can be improved
206 + //! Still trying out various ways to improve it
207 + @action
208 + void selectCakePreset(Cake2FAPresetsOptions cake2FAPreset) {
209 + // The tabs are ordered in the format [Narrow || Normal || Verbose]
210 + // Where Narrow = 0, Normal = 1 and Verbose = 2
211 + switch (cake2FAPreset) {
212 + case Cake2FAPresetsOptions.narrow:
213 + activateCake2FANarrowPreset();
214 + break;
215 + case Cake2FAPresetsOptions.normal:
216 + activateCake2FANormalPreset();
217 + break;
218 + case Cake2FAPresetsOptions.aggressive:
219 + activateCake2FAAggressivePreset();
220 + break;
221 + default:
222 + activateCake2FANormalPreset();
223 + }
224 + }
225 +
226 + @action
227 + void checkIfTheCurrentSettingMatchesAnyOfThePresets() {
228 + final hasNormalPreset = checkIfTheNormalPresetIsPresent();
229 + final hasNarrowPreset = checkIfTheNarrowPresetIsPresent();
230 + final hasVerbosePreset = checkIfTheVerbosePresetIsPresent();
231 +
232 + if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) {
233 + unhighlightTabs = false;
234 + } else {
235 + unhighlightTabs = true;
236 + }
237 + }
238 +
239 + @action
240 + bool checkIfTheNormalPresetIsPresent() {
241 + final hasContacts = selected2FASettings.contains(VerboseControlSettings.sendsToContacts);
242 + final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
243 + final hasSecurityAndBackup =
244 + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
245 +
246 + final hasSendToInternalWallet =
247 + selected2FASettings.contains(VerboseControlSettings.sendsToInternalWallets);
248 +
249 + final hasExchangesToInternalWallet =
250 + selected2FASettings.contains(VerboseControlSettings.exchangesToInternalWallets);
251 +
252 + bool isOnlyNormalPresetControlsPresent = selected2FASettings.length == 5;
253 +
254 + return (hasContacts &&
255 + hasNonContacts &&
256 + hasSecurityAndBackup &&
257 + hasSendToInternalWallet &&
258 + hasExchangesToInternalWallet &&
259 + isOnlyNormalPresetControlsPresent);
260 + }
261 +
262 + @action
263 + bool checkIfTheVerbosePresetIsPresent() {
264 + final hasAccessWallets = selected2FASettings.contains(VerboseControlSettings.accessWallet);
265 + final hasSecurityAndBackup =
266 + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
267 +
268 + bool isOnlyVerbosePresetControlsPresent = selected2FASettings.length == 2;
269 +
270 + return (hasAccessWallets && hasSecurityAndBackup && isOnlyVerbosePresetControlsPresent);
271 + }
272 +
273 + @action
274 + bool checkIfTheNarrowPresetIsPresent() {
275 + final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
276 + final hasAddContacts = selected2FASettings.contains(VerboseControlSettings.addingContacts);
277 + final hasCreateNewWallet =
278 + selected2FASettings.contains(VerboseControlSettings.creatingNewWallets);
279 + final hasSecurityAndBackup =
280 + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
281 +
282 + bool isOnlyNarrowPresetControlsPresent = selected2FASettings.length == 4;
283 +
284 + return (hasNonContacts &&
285 + hasAddContacts &&
286 + hasCreateNewWallet &&
287 + hasSecurityAndBackup &&
288 + isOnlyNarrowPresetControlsPresent);
289 + }
290 +
291 + @action
292 + void activateCake2FANormalPreset() {
293 + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.normal;
294 + setAllControlsToFalse();
295 + switchShouldRequireTOTP2FAForSendsToNonContact(true);
296 + switchShouldRequireTOTP2FAForSendsToContact(true);
297 + switchShouldRequireTOTP2FAForSendsToInternalWallets(true);
298 + switchShouldRequireTOTP2FAForExchangesToInternalWallets(true);
299 + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
300 + }
301 +
302 + @action
303 + void activateCake2FANarrowPreset() {
304 + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.narrow;
305 + setAllControlsToFalse();
306 + switchShouldRequireTOTP2FAForSendsToNonContact(true);
307 + switchShouldRequireTOTP2FAForAddingContacts(true);
308 + switchShouldRequireTOTP2FAForCreatingNewWallet(true);
309 + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
310 + }
311 +
312 + @action
313 + void activateCake2FAAggressivePreset() {
314 + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.aggressive;
315 + setAllControlsToFalse();
316 + switchShouldRequireTOTP2FAForAccessingWallet(true);
317 + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
318 + }
319 +
320 + @action
321 + void setAllControlsToFalse() {
322 + switchShouldRequireTOTP2FAForAccessingWallet(false);
323 + switchShouldRequireTOTP2FAForSendsToContact(false);
324 + switchShouldRequireTOTP2FAForSendsToNonContact(false);
325 + switchShouldRequireTOTP2FAForAddingContacts(false);
326 + switchShouldRequireTOTP2FAForCreatingNewWallet(false);
327 + switchShouldRequireTOTP2FAForExchangesToInternalWallets(false);
328 + switchShouldRequireTOTP2FAForSendsToInternalWallets(false);
329 + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false);
330 + selected2FASettings.clear();
331 + unhighlightTabs = false;
332 + }
333 +
334 + @action
335 + void switchShouldRequireTOTP2FAForAccessingWallet(bool value) {
336 + _settingsStore.shouldRequireTOTP2FAForAccessingWallet = value;
337 + if (value) {
338 + selected2FASettings.add(VerboseControlSettings.accessWallet);
339 + } else {
340 + selected2FASettings.remove(VerboseControlSettings.accessWallet);
341 + }
342 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
343 + }
344 +
345 + @action
346 + void switchShouldRequireTOTP2FAForSendsToContact(bool value) {
347 + _settingsStore.shouldRequireTOTP2FAForSendsToContact = value;
348 + if (value) {
349 + selected2FASettings.add(VerboseControlSettings.sendsToContacts);
350 + } else {
351 + selected2FASettings.remove(VerboseControlSettings.sendsToContacts);
352 + }
353 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
354 + }
355 +
356 + @action
357 + void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) {
358 + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value;
359 + if (value) {
360 + selected2FASettings.add(VerboseControlSettings.sendsToNonContacts);
361 + } else {
362 + selected2FASettings.remove(VerboseControlSettings.sendsToNonContacts);
363 + }
364 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
365 + }
366 +
367 + @action
368 + void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) {
369 + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value;
370 + if (value) {
371 + selected2FASettings.add(VerboseControlSettings.sendsToInternalWallets);
372 + } else {
373 + selected2FASettings.remove(VerboseControlSettings.sendsToInternalWallets);
374 + }
375 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
376 + }
377 +
378 + @action
379 + void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) {
380 + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value;
381 + if (value) {
382 + selected2FASettings.add(VerboseControlSettings.exchangesToInternalWallets);
383 + } else {
384 + selected2FASettings.remove(VerboseControlSettings.exchangesToInternalWallets);
385 + }
386 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
387 + }
388 +
389 + @action
390 + void switchShouldRequireTOTP2FAForAddingContacts(bool value) {
391 + _settingsStore.shouldRequireTOTP2FAForAddingContacts = value;
392 + if (value)
393 + selected2FASettings.add(VerboseControlSettings.addingContacts);
394 + else {
395 + selected2FASettings.remove(VerboseControlSettings.addingContacts);
396 + }
397 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
398 + }
399 +
400 + @action
401 + void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) {
402 + _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value;
403 + if (value) {
404 + selected2FASettings.add(VerboseControlSettings.creatingNewWallets);
405 + } else {
406 + selected2FASettings.remove(VerboseControlSettings.creatingNewWallets);
407 + }
408 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
409 + }
410 +
411 + @action
412 + void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) {
413 + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value;
414 + if (value)
415 + selected2FASettings.add(VerboseControlSettings.securityAndBackupSettings);
416 + else {
417 + selected2FASettings.remove(VerboseControlSettings.securityAndBackupSettings);
418 + }
419 + checkIfTheCurrentSettingMatchesAnyOfThePresets();
420 + }
421 }
lib/view_model/settings/security_settings_view_model.dart
+4
@@ -24,6 +24,10 @@ abstract class SecuritySettingsViewModelBase with Store {
24 @computed
25 bool get useTotp2FA => _settingsStore.useTOTP2FA;
26
27 + @computed
28 + bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings =>
29 + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
30 +
31 @computed
32 PinCodeRequiredDuration get pinCodeRequiredDuration => _settingsStore.pinTimeOutDuration;
33
lib/view_model/wallet_list/wallet_list_view_model.dart
+8 -1
@@ -27,6 +27,14 @@ abstract class WalletListViewModelBase with Store {
27 @observable
28 ObservableList<WalletListItem> wallets;
29
30 + @computed
31 + bool get shouldRequireTOTP2FAForAccessingWallet =>
32 + _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
33 +
34 + @computed
35 + bool get shouldRequireTOTP2FAForCreatingNewWallets =>
36 + _appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets;
37 +
38 final AppStore _appStore;
39 final Box<WalletInfo> _walletInfoSource;
40 final WalletLoadingService _walletLoadingService;
@@ -38,7 +46,6 @@ abstract class WalletListViewModelBase with Store {
46 Future<void> loadWallet(WalletListItem walletItem) async {
47 final wallet =
48 await _walletLoadingService.load(walletItem.type, walletItem.name);
41 -
49 _appStore.changeCurrentWallet(wallet);
50 }
51
macos/Flutter/GeneratedPluginRegistrant.swift
+2
@@ -12,6 +12,7 @@ import devicelocale
12 import flutter_secure_storage_macos
13 import in_app_review
14 import package_info
15 +import package_info_plus
16 import path_provider_foundation
17 import platform_device_id
18 import platform_device_id_macos
@@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
29 FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
30 InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
31 FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin"))
32 + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
33 PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
34 PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin"))
35 PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin"))
res/values/strings_ar.arb
+12
@@ -630,6 +630,18 @@
630 "setup_totp_recommended": "إعداد TOTP (موصى به)",
631 "disable_buy": "تعطيل إجراء الشراء",
632 "disable_sell": "قم بتعطيل إجراء البيع",
633 + "cake_2fa_preset" : " كعكة 2FA مسبقا",
634 + "narrow": "ضيق",
635 + "normal": "طبيعي",
636 + "aggressive": "عنيف",
637 + "require_for_assessing_wallet": "تتطلب الوصول إلى المحفظة",
638 + "require_for_sends_to_non_contacts" : "تتطلب لارسال لغير جهات الاتصال",
639 + "require_for_sends_to_contacts" : "تتطلب لارسال جهات الاتصال",
640 + "require_for_sends_to_internal_wallets" : "تتطلب عمليات الإرسال إلى المحافظ الداخلية",
641 + "require_for_exchanges_to_internal_wallets" : "تتطلب عمليات التبادل إلى المحافظ الداخلية",
642 + "require_for_adding_contacts" : "تتطلب إضافة جهات اتصال",
643 + "require_for_creating_new_wallets" : "تتطلب إنشاء محافظ جديدة",
644 + "require_for_all_security_and_backup_settings" : "مطلوب لجميع إعدادات الأمان والنسخ الاحتياطي",
645 "available_balance_description": "الرصيد المتاح هو الرصيد الذي يمكنك إنفاقه أو تحويله إلى محفظة أخرى. يتم تجميد الرصيد المتاح للمعاملات الصادرة والمعاملات الواردة غير المؤكدة.",
646 "syncing_wallet_alert_title": "محفظتك تتم مزامنتها",
647 "syncing_wallet_alert_content": "قد لا يكتمل رصيدك وقائمة المعاملات الخاصة بك حتى تظهر عبارة “SYNCHRONIZED“ في الأعلى. انقر / اضغط لمعرفة المزيد.",
res/values/strings_bg.arb
+12
@@ -626,6 +626,18 @@
626 "setup_totp_recommended": "Настройка на TOTP (препоръчително)",
627 "disable_buy": "Деактивирайте действието за покупка",
628 "disable_sell": "Деактивирайте действието за продажба",
629 + "cake_2fa_preset" : "Торта 2FA Preset",
630 + "narrow": "Тесен",
631 + "normal": "нормално",
632 + "aggressive": "Прекалено усърден",
633 + "require_for_assessing_wallet": "Изискване за достъп до портфейла",
634 + "require_for_sends_to_non_contacts" : "Изискване за изпращане до лица без контакт",
635 + "require_for_sends_to_contacts" : "Изискване за изпращане до контакти",
636 + "require_for_sends_to_internal_wallets" : "Изискване за изпращане до вътрешни портфейли",
637 + "require_for_exchanges_to_internal_wallets" : "Изискване за обмен към вътрешни портфейли",
638 + "require_for_adding_contacts" : "Изисква се за добавяне на контакти",
639 + "require_for_creating_new_wallets" : "Изискване за създаване на нови портфейли",
640 + "require_for_all_security_and_backup_settings" : "Изисква се за всички настройки за сигурност и архивиране",
641 "available_balance_description": "Това е балансът, който можете да използвате за покупка на криптовалути. Това не включва замразените средства.",
642 "syncing_wallet_alert_title": "Вашият портфейл се синхронизира",
643 "syncing_wallet_alert_content": "Списъкът ви с баланс и транзакции може да не е пълен, докато в горната част не пише „СИНХРОНИЗИРАН“. Кликнете/докоснете, за да научите повече.",
res/values/strings_cs.arb
+12
@@ -626,6 +626,18 @@
626 "setup_totp_recommended": "Nastavit TOTP (doporučeno)",
627 "disable_buy": "Zakázat akci nákupu",
628 "disable_sell": "Zakázat akci prodeje",
629 + "cake_2fa_preset" : "Předvolba Cake 2FA",
630 + "narrow": "Úzký",
631 + "normal": "Normální",
632 + "aggressive": "Agresivní",
633 + "require_for_assessing_wallet": "Vyžadovat pro přístup k peněžence",
634 + "require_for_sends_to_non_contacts" : "Vyžadovat pro odesílání nekontaktním osobám",
635 + "require_for_sends_to_contacts" : "Vyžadovat pro odeslání kontaktům",
636 + "require_for_sends_to_internal_wallets" : "Vyžadovat pro odesílání do interních peněženek",
637 + "require_for_exchanges_to_internal_wallets" : "Vyžadovat pro výměny do interních peněženek",
638 + "require_for_adding_contacts" : "Vyžadovat pro přidání kontaktů",
639 + "require_for_creating_new_wallets" : "Vyžadovat pro vytváření nových peněženek",
640 + "require_for_all_security_and_backup_settings" : "Vyžadovat všechna nastavení zabezpečení a zálohování",
641 "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.",
642 "syncing_wallet_alert_title": "Vaše peněženka se synchronizuje",
643 "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.",
res/values/strings_de.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "TOTP einrichten (empfohlen)",
633 "disable_buy": "Kaufaktion deaktivieren",
634 "disable_sell": "Verkaufsaktion deaktivieren",
635 + "cake_2fa_preset" : "Kuchen 2FA-Voreinstellung",
636 + "narrow": "Eng",
637 + "normal": "Normal",
638 + "aggressive": "Übereifrig",
639 + "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich",
640 + "require_for_sends_to_non_contacts" : "Erforderlich für Versendungen an Nichtkontakte",
641 + "require_for_sends_to_contacts" : "Erforderlich für Versendungen an Kontakte",
642 + "require_for_sends_to_internal_wallets" : "Erforderlich für Sendungen an interne Wallets",
643 + "require_for_exchanges_to_internal_wallets" : "Erforderlich für den Umtausch in interne Wallets",
644 + "require_for_adding_contacts" : "Erforderlich zum Hinzufügen von Kontakten",
645 + "require_for_creating_new_wallets" : "Erforderlich zum Erstellen neuer Wallets",
646 + "require_for_all_security_and_backup_settings" : "Für alle Sicherheits- und Sicherungseinstellungen erforderlich",
647 "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.",
648 "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert",
649 "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.",
res/values/strings_en.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Set up TOTP (Recommended)",
633 "disable_buy": "Disable buy action",
634 "disable_sell": "Disable sell action",
635 + "cake_2fa_preset" : "Cake 2FA Preset",
636 + "narrow": "Narrow",
637 + "normal": "Normal",
638 + "aggressive": "Aggressive",
639 + "require_for_assessing_wallet": "Require for accessing wallet",
640 + "require_for_sends_to_non_contacts" : "Require for sends to non-contacts",
641 + "require_for_sends_to_contacts" : "Require for sends to contacts",
642 + "require_for_sends_to_internal_wallets" : "Require for sends to internal wallets",
643 + "require_for_exchanges_to_internal_wallets" : "Require for exchanges to internal wallets",
644 + "require_for_adding_contacts" : "Require for adding contacts",
645 + "require_for_creating_new_wallets" : "Require for creating new wallets",
646 + "require_for_all_security_and_backup_settings" : "Require for all security and backup settings",
647 "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.",
648 "syncing_wallet_alert_title": "Your wallet is syncing",
649 "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.",
res/values/strings_es.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Configurar TOTP (Recomendado)",
633 "disable_buy": "Desactivar acción de compra",
634 "disable_sell": "Desactivar acción de venta",
635 + "cake_2fa_preset" : "Pastel 2FA preestablecido",
636 + "narrow": "Angosto",
637 + "normal": "Normal",
638 + "aggressive": "Demasiado entusiasta",
639 + "require_for_assessing_wallet": "Requerido para acceder a la billetera",
640 + "require_for_sends_to_non_contacts" : "Requerido para envíos a no contactos",
641 + "require_for_sends_to_contacts" : "Requerir para envíos a contactos",
642 + "require_for_sends_to_internal_wallets" : "Requerido para envíos a billeteras internas",
643 + "require_for_exchanges_to_internal_wallets" : "Requerido para intercambios a billeteras internas",
644 + "require_for_adding_contacts" : "Requerido para agregar contactos",
645 + "require_for_creating_new_wallets" : "Requerido para crear nuevas billeteras",
646 + "require_for_all_security_and_backup_settings" : "Requerido para todas las configuraciones de seguridad y copia de seguridad",
647 "available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.",
648 "syncing_wallet_alert_title": "Tu billetera se está sincronizando",
649 "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.",
res/values/strings_fr.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Configurer TOTP (recommandé)",
633 "disable_buy": "Désactiver l'action d'achat",
634 "disable_sell": "Désactiver l'action de vente",
635 + "cake_2fa_preset" : "Gâteau 2FA prédéfini",
636 + "narrow": "Étroit",
637 + "normal": "Normal",
638 + "aggressive": "Trop zélé",
639 + "require_for_assessing_wallet": "Nécessaire pour accéder au portefeuille",
640 + "require_for_sends_to_non_contacts" : "Exiger pour les envois à des non-contacts",
641 + "require_for_sends_to_contacts" : "Exiger pour les envois aux contacts",
642 + "require_for_sends_to_internal_wallets" : "Exiger pour les envois vers des portefeuilles internes",
643 + "require_for_exchanges_to_internal_wallets" : "Exiger pour les échanges vers des portefeuilles internes",
644 + "require_for_adding_contacts" : "Requis pour ajouter des contacts",
645 + "require_for_creating_new_wallets" : "Nécessaire pour créer de nouveaux portefeuilles",
646 + "require_for_all_security_and_backup_settings" : "Exiger pour tous les paramètres de sécurité et de sauvegarde",
647 "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.",
648 "syncing_wallet_alert_title": "Votre portefeuille est en cours de synchronisation",
649 "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.",
res/values/strings_ha.arb
+12 -1
@@ -612,6 +612,18 @@
612 "prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi",
613 "disable_buy": "Kashe alama",
614 "disable_sell": "Kashe karbuwa",
615 + "cake_2fa_preset" : "Cake 2FA saiti",
616 + "narrow": "kunkuntar",
617 + "normal": "Na al'ada",
618 + "aggressive": "Mai tsananin kishi",
619 + "require_for_assessing_wallet": "Bukatar samun damar walat",
620 + "require_for_sends_to_non_contacts" : "Bukatar aika zuwa waɗanda ba lambobin sadarwa ba",
621 + "require_for_sends_to_contacts" : "Bukatar aika zuwa lambobin sadarwa",
622 + "require_for_sends_to_internal_wallets" : "Bukatar aika zuwa wallet na ciki",
623 + "require_for_exchanges_to_internal_wallets" : "Bukatar musanya zuwa wallet na ciki",
624 + "require_for_adding_contacts" : "Bukatar ƙara lambobin sadarwa",
625 + "require_for_creating_new_wallets" : "Bukatar ƙirƙirar sabbin wallet",
626 + "require_for_all_security_and_backup_settings" : "Bukatar duk tsaro da saitunan wariyar ajiya",
627 "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.",
628 "syncing_wallet_alert_title": "Walat ɗin ku yana aiki tare",
629 "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 @@
633 "slidable": "Mai iya zamewa",
634 "template_name": "Sunan Samfura"
635 }
624 -
res/values/strings_hi.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "टीओटीपी सेट अप करें (अनुशंसित)",
633 "disable_buy": "खरीद कार्रवाई अक्षम करें",
634 "disable_sell": "बेचने की कार्रवाई अक्षम करें",
635 + "cake_2fa_preset" : "केक 2एफए प्रीसेट",
636 + "narrow": "सँकरा",
637 + "normal": "सामान्य",
638 + "aggressive": "ज्यादा",
639 + "require_for_assessing_wallet": "वॉलेट तक पहुँचने के लिए आवश्यकता है",
640 + "require_for_sends_to_non_contacts" : "गैर-संपर्कों को भेजने की आवश्यकता",
641 + "require_for_sends_to_contacts" : "संपर्कों को भेजने के लिए आवश्यक है",
642 + "require_for_sends_to_internal_wallets" : "आंतरिक वॉलेट में भेजने की आवश्यकता है",
643 + "require_for_exchanges_to_internal_wallets" : "आंतरिक वॉलेट में आदान-प्रदान की आवश्यकता है",
644 + "require_for_adding_contacts" : "संपर्क जोड़ने के लिए आवश्यकता है",
645 + "require_for_creating_new_wallets" : "नए वॉलेट बनाने की आवश्यकता है",
646 + "require_for_all_security_and_backup_settings" : "सभी सुरक्षा और बैकअप सेटिंग्स की आवश्यकता है",
647 "available_balance_description": "उपलब्ध शेष या ”पुष्टिकृत शेष”, वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड निचले बैलेंस में दिखाई देते हैं, लेकिन शीर्ष बैलेंस में नहीं, तो आपको आने वाले फंड के लिए अधिक नेटवर्क पुष्टिकरण प्राप्त करने के लिए कुछ मिनट इंतजार करना होगा। अधिक पुष्टि मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।",
648 "syncing_wallet_alert_title": "आपका वॉलेट सिंक हो रहा है",
649 "syncing_wallet_alert_content": "आपकी शेष राशि और लेनदेन सूची तब तक पूरी नहीं हो सकती जब तक कि शीर्ष पर \"सिंक्रनाइज़्ड\" न लिखा हो। अधिक जानने के लिए क्लिक/टैप करें।",
res/values/strings_hr.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Postavite TOTP (preporučeno)",
633 "disable_buy": "Onemogući kupnju",
634 "disable_sell": "Onemogući akciju prodaje",
635 + "cake_2fa_preset" : "Cake 2FA Preset",
636 + "narrow": "Usko",
637 + "normal": "Normalno",
638 + "aggressive": "Preterano",
639 + "require_for_assessing_wallet": "Potreban za pristup novčaniku",
640 + "require_for_sends_to_non_contacts" : "Zahtijeva za slanje nekontaktima",
641 + "require_for_sends_to_contacts" : "Zahtijeva za slanje kontaktima",
642 + "require_for_sends_to_internal_wallets" : "Zahtijeva za slanje u interne novčanike",
643 + "require_for_exchanges_to_internal_wallets" : "Potreban za razmjenu na interne novčanike",
644 + "require_for_adding_contacts" : "Zahtijeva za dodavanje kontakata",
645 + "require_for_creating_new_wallets" : "Potreban za kreiranje novih novčanika",
646 + "require_for_all_security_and_backup_settings" : "Zahtijeva za sve postavke sigurnosti i sigurnosne kopije",
647 "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.",
648 "syncing_wallet_alert_title": "Vaš novčanik se sinkronizira",
649 "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.",
res/values/strings_id.arb
+12
@@ -622,6 +622,18 @@
622 "setup_totp_recommended": "Siapkan TOTP (Disarankan)",
623 "disable_buy": "Nonaktifkan tindakan beli",
624 "disable_sell": "Nonaktifkan aksi jual",
625 + "cake_2fa_preset" : "Preset Kue 2FA",
626 + "narrow": "Sempit",
627 + "normal": "Normal",
628 + "aggressive": "Terlalu bersemangat",
629 + "require_for_assessing_wallet": "Diperlukan untuk mengakses dompet",
630 + "require_for_sends_to_non_contacts" : "Wajibkan untuk mengirim ke non-kontak",
631 + "require_for_sends_to_contacts" : "Membutuhkan untuk mengirim ke kontak",
632 + "require_for_sends_to_internal_wallets" : "Diperlukan untuk mengirim ke dompet internal",
633 + "require_for_exchanges_to_internal_wallets" : "Diperlukan untuk pertukaran ke dompet internal",
634 + "require_for_adding_contacts" : "Membutuhkan untuk menambahkan kontak",
635 + "require_for_creating_new_wallets" : "Diperlukan untuk membuat dompet baru",
636 + "require_for_all_security_and_backup_settings" : "Memerlukan untuk semua pengaturan keamanan dan pencadangan",
637 "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.",
638 "syncing_wallet_alert_title": "Dompet Anda sedang disinkronkan",
639 "syncing_wallet_alert_content": "Saldo dan daftar transaksi Anda mungkin belum lengkap sampai tertulis “SYNCHRONIZED” di bagian atas. Klik/ketuk untuk mempelajari lebih lanjut.",
res/values/strings_it.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Imposta TOTP (consigliato)",
633 "disable_buy": "Disabilita l'azione di acquisto",
634 "disable_sell": "Disabilita l'azione di vendita",
635 + "cake_2fa_preset" : "Torta 2FA Preset",
636 + "narrow": "Stretto",
637 + "normal": "Normale",
638 + "aggressive": "Fervente",
639 + "require_for_assessing_wallet": "Richiesto per l'accesso al portafoglio",
640 + "require_for_sends_to_non_contacts" : "Richiesto per invii a non contatti",
641 + "require_for_sends_to_contacts" : "Richiedi per gli invii ai contatti",
642 + "require_for_sends_to_internal_wallets" : "Richiesto per invii a portafogli interni",
643 + "require_for_exchanges_to_internal_wallets" : "Richiedi per gli scambi ai portafogli interni",
644 + "require_for_adding_contacts" : "Richiesto per l'aggiunta di contatti",
645 + "require_for_creating_new_wallets" : "Richiesto per la creazione di nuovi portafogli",
646 + "require_for_all_security_and_backup_settings" : "Richiedi per tutte le impostazioni di sicurezza e backup",
647 "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.",
648 "syncing_wallet_alert_title": "Il tuo portafoglio si sta sincronizzando",
649 "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ù.",
res/values/strings_ja.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "TOTP を設定する (推奨)",
633 "disable_buy": "購入アクションを無効にする",
634 "disable_sell": "販売アクションを無効にする",
635 + "cake_2fa_preset" : "ケーキ 2FA プリセット",
636 + "narrow": "狭い",
637 + "normal": "普通",
638 + "aggressive": "熱心すぎる",
639 + "require_for_assessing_wallet": "ウォレットにアクセスするために必要です",
640 + "require_for_sends_to_non_contacts" : "非連絡先への送信に必須",
641 + "require_for_sends_to_contacts" : "連絡先に送信する場合に必須",
642 + "require_for_sends_to_internal_wallets" : "内部ウォレットへの送信に必須",
643 + "require_for_exchanges_to_internal_wallets" : "内部ウォレットへの交換に必要",
644 + "require_for_adding_contacts" : "連絡先の追加に必要",
645 + "require_for_creating_new_wallets" : "新しいウォレットを作成するために必要です",
646 + "require_for_all_security_and_backup_settings" : "すべてのセキュリティおよびバックアップ設定に必須",
647 "available_balance_description": "利用可能な残高は、ウォレットの残高から冷凍残高を差し引いたものです。",
648 "syncing_wallet_alert_title": "ウォレットは同期中です",
649 "syncing_wallet_alert_content": "上部に「同期済み」と表示されるまで、残高と取引リストが完了していない可能性があります。詳細については、クリック/タップしてください。",
res/values/strings_ko.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "TOTP 설정(권장)",
633 "disable_buy": "구매 행동 비활성화",
634 "disable_sell": "판매 조치 비활성화",
635 + "cake_2fa_preset" : "케이크 2FA 프리셋",
636 + "narrow": "좁은",
637 + "normal": "정상",
638 + "aggressive": "지나치게 열심인",
639 + "require_for_assessing_wallet": "지갑 접근을 위해 필요",
640 + "require_for_sends_to_non_contacts" : "비접촉자에게 보내는 데 필요",
641 + "require_for_sends_to_contacts" : "연락처로 보내기에 필요",
642 + "require_for_sends_to_internal_wallets" : "내부 지갑으로 보내는 데 필요",
643 + "require_for_exchanges_to_internal_wallets" : "내부 지갑으로의 교환에 필요",
644 + "require_for_adding_contacts" : "연락처 추가에 필요",
645 + "require_for_creating_new_wallets" : "새 지갑 생성에 필요",
646 + "require_for_all_security_and_backup_settings" : "모든 보안 및 백업 설정에 필요",
647 "available_balance_description": "이 지갑에서 사용할 수 있는 잔액입니다. 이 잔액은 블록체인에서 가져온 것이며, Cake Wallet이 사용할 수 없습니다.",
648 "syncing_wallet_alert_title": "지갑 동기화 중",
649 "syncing_wallet_alert_content": "상단에 \"동기화됨\"이라고 표시될 때까지 잔액 및 거래 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.",
res/values/strings_my.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "TOTP ကို ​​စနစ်ထည့်သွင်းပါ (အကြံပြုထားသည်)",
633 "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။",
634 "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။",
635 + "cake_2fa_preset" : "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်",
636 + "narrow": "ကျဉ်းသော",
637 + "normal": "ပုံမှန်",
638 + "aggressive": "စိတ်အားထက်သန်ခြင်း။",
639 + "require_for_assessing_wallet": "ပိုက်ဆံအိတ်ကို ဝင်သုံးရန် လိုအပ်သည်။",
640 + "require_for_sends_to_non_contacts" : "အဆက်အသွယ်မရှိသူများထံ ပေးပို့ရန် လိုအပ်သည်။",
641 + "require_for_sends_to_contacts" : "အဆက်အသွယ်များထံ ပေးပို့ရန် လိုအပ်သည်။",
642 + "require_for_sends_to_internal_wallets" : "အတွင်းပိုင်း ပိုက်ဆံအိတ်များသို့ ပေးပို့ရန် လိုအပ်သည်။",
643 + "require_for_exchanges_to_internal_wallets" : "အတွင်းပိုင်းပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။",
644 + "require_for_adding_contacts" : "အဆက်အသွယ်များထည့်ရန် လိုအပ်သည်။",
645 + "require_for_creating_new_wallets" : "ပိုက်ဆံအိတ်အသစ်များ ဖန်တီးရန် လိုအပ်သည်။",
646 + "require_for_all_security_and_backup_settings" : "လုံခြုံရေးနှင့် အရန်ဆက်တင်များအားလုံးအတွက် လိုအပ်ပါသည်။",
647 "available_balance_description": "သင့်ရဲ့ အကောင့်တွင် ရရှိနိုင်သော ငွေကျန်ငွေကို ပြန်လည်ပေးသွင်းပါ။",
648 "syncing_wallet_alert_title": "သင့်ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်နေပါသည်။",
649 "syncing_wallet_alert_content": "သင်၏လက်ကျန်နှင့် ငွေပေးငွေယူစာရင်းသည် ထိပ်တွင် \"Synchronizeed\" ဟုပြောသည်အထိ မပြီးမြောက်နိုင်ပါ။ ပိုမိုလေ့လာရန် နှိပ်/နှိပ်ပါ။",
res/values/strings_nl.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "TOTP instellen (aanbevolen)",
633 "disable_buy": "Koopactie uitschakelen",
634 "disable_sell": "Verkoopactie uitschakelen",
635 + "cake_2fa_preset" : "Taart 2FA Voorinstelling",
636 + "narrow": "Smal",
637 + "normal": "Normaal",
638 + "aggressive": "Overijverig",
639 + "require_for_assessing_wallet": "Vereist voor toegang tot portemonnee",
640 + "require_for_sends_to_non_contacts" : "Vereist voor verzendingen naar niet-contacten",
641 + "require_for_sends_to_contacts" : "Vereist voor verzending naar contacten",
642 + "require_for_sends_to_internal_wallets" : "Vereist voor verzendingen naar interne portefeuilles",
643 + "require_for_exchanges_to_internal_wallets" : "Vereist voor uitwisselingen naar interne portefeuilles",
644 + "require_for_adding_contacts" : "Vereist voor het toevoegen van contacten",
645 + "require_for_creating_new_wallets" : "Vereist voor het maken van nieuwe portefeuilles",
646 + "require_for_all_security_and_backup_settings" : "Vereist voor alle beveiligings- en back-upinstellingen",
647 "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.",
648 "syncing_wallet_alert_title": "Uw portemonnee wordt gesynchroniseerd",
649 "syncing_wallet_alert_content": "Uw saldo- en transactielijst is mogelijk pas compleet als er bovenaan 'GESYNCHRONISEERD' staat. Klik/tik voor meer informatie.",
res/values/strings_pl.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)",
633 "disable_buy": "Wyłącz akcję kupna",
634 "disable_sell": "Wyłącz akcję sprzedaży",
635 + "cake_2fa_preset" : "Ciasto 2FA Preset",
636 + "narrow": "Wąski",
637 + "normal": "Normalna",
638 + "aggressive": "Nadgorliwy",
639 + "require_for_assessing_wallet": "Wymagaj dostępu do portfela",
640 + "require_for_sends_to_non_contacts" : "Wymagaj wysyłania do osób niekontaktowych",
641 + "require_for_sends_to_contacts" : "Wymagaj wysyłania do kontaktów",
642 + "require_for_sends_to_internal_wallets" : "Wymagaj wysyłania do portfeli wewnętrznych",
643 + "require_for_exchanges_to_internal_wallets" : "Wymagaj wymiany do portfeli wewnętrznych",
644 + "require_for_adding_contacts" : "Wymagane do dodania kontaktów",
645 + "require_for_creating_new_wallets" : "Wymagane do tworzenia nowych portfeli",
646 + "require_for_all_security_and_backup_settings" : "Wymagaj dla wszystkich ustawień zabezpieczeń i kopii zapasowych",
647 "available_balance_description": "Dostępne saldo jest równoważne z saldem portfela minus zamrożone saldo.",
648 "syncing_wallet_alert_title": "Twój portfel się synchronizuje",
649 "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.",
res/values/strings_pt.arb
+12
@@ -631,6 +631,18 @@
631 "setup_totp_recommended": "Configurar TOTP (recomendado)",
632 "disable_buy": "Desativar ação de compra",
633 "disable_sell": "Desativar ação de venda",
634 + "cake_2fa_preset" : "Predefinição de bolo 2FA",
635 + "narrow": "Estreito",
636 + "normal": "Normal",
637 + "aggressive": "excessivamente zeloso",
638 + "require_for_assessing_wallet": "Requer para acessar a carteira",
639 + "require_for_sends_to_non_contacts" : "Exigir para envios para não-contatos",
640 + "require_for_sends_to_contacts" : "Exigir para envios para contatos",
641 + "require_for_sends_to_internal_wallets" : "Exigir envios para carteiras internas",
642 + "require_for_exchanges_to_internal_wallets" : "Requer trocas para carteiras internas",
643 + "require_for_adding_contacts" : "Requer para adicionar contatos",
644 + "require_for_creating_new_wallets" : "Requer para criar novas carteiras",
645 + "require_for_all_security_and_backup_settings" : "Exigir todas as configurações de segurança e backup",
646 "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.",
647 "syncing_wallet_alert_title": "Sua carteira está sincronizando",
648 "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.",
res/values/strings_ru.arb
+12
@@ -633,6 +633,18 @@
633 "setup_totp_recommended": "Настроить TOTP (рекомендуется)",
634 "disable_buy": "Отключить действие покупки",
635 "disable_sell": "Отключить действие продажи",
636 + "cake_2fa_preset" : "Торт 2FA Preset",
637 + "narrow": "Узкий",
638 + "normal": "Нормальный",
639 + "aggressive": "чрезмерно усердный",
640 + "require_for_assessing_wallet": "Требовать для доступа к кошельку",
641 + "require_for_sends_to_non_contacts" : "Требовать для отправки не контактам",
642 + "require_for_sends_to_contacts" : "Требовать для отправки контактам",
643 + "require_for_sends_to_internal_wallets" : "Требовать отправки на внутренние кошельки",
644 + "require_for_exchanges_to_internal_wallets" : "Требовать для обмена на внутренние кошельки",
645 + "require_for_adding_contacts" : "Требовать добавления контактов",
646 + "require_for_creating_new_wallets" : "Требовать для создания новых кошельков",
647 + "require_for_all_security_and_backup_settings" : "Требовать все настройки безопасности и резервного копирования",
648 "available_balance_description": "Доступный баланс - это средства, которые вы можете использовать для покупки или продажи криптовалюты.",
649 "syncing_wallet_alert_title": "Ваш кошелек синхронизируется",
650 "syncing_wallet_alert_content": "Ваш баланс и список транзакций могут быть неполными, пока вверху не будет написано «СИНХРОНИЗИРОВАНО». Щелкните/коснитесь, чтобы узнать больше.",
res/values/strings_th.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "ตั้งค่า TOTP (แนะนำ)",
633 "disable_buy": "ปิดการใช้งานการซื้อ",
634 "disable_sell": "ปิดการใช้งานการขาย",
635 + "cake_2fa_preset" : "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า",
636 + "narrow": "แคบ",
637 + "normal": "ปกติ",
638 + "aggressive": "กระตือรือร้นมากเกินไป",
639 + "require_for_assessing_wallet": "จำเป็นสำหรับการเข้าถึงกระเป๋าเงิน",
640 + "require_for_sends_to_non_contacts" : "จำเป็นต้องส่งไปยังผู้ที่ไม่ได้ติดต่อ",
641 + "require_for_sends_to_contacts" : "จำเป็นต้องส่งไปยังผู้ติดต่อ",
642 + "require_for_sends_to_internal_wallets" : "จำเป็นต้องส่งไปยังกระเป๋าเงินภายใน",
643 + "require_for_exchanges_to_internal_wallets" : "ต้องการการแลกเปลี่ยนไปยังกระเป๋าเงินภายใน",
644 + "require_for_adding_contacts" : "ต้องการสำหรับการเพิ่มผู้ติดต่อ",
645 + "require_for_creating_new_wallets" : "จำเป็นสำหรับการสร้างกระเป๋าเงินใหม่",
646 + "require_for_all_security_and_backup_settings" : "จำเป็นสำหรับการตั้งค่าความปลอดภัยและการสำรองข้อมูลทั้งหมด",
647 "available_balance_description": "จำนวนเงินที่คุณสามารถใช้ได้ในการซื้อหรือขาย",
648 "syncing_wallet_alert_title": "กระเป๋าสตางค์ของคุณกำลังซิงค์",
649 "syncing_wallet_alert_content": "รายการยอดเงินและธุรกรรมของคุณอาจไม่สมบูรณ์จนกว่าจะมีข้อความว่า “ซิงโครไนซ์” ที่ด้านบน คลิก/แตะเพื่อเรียนรู้เพิ่มเติม่",
res/values/strings_tr.arb
+13
@@ -631,6 +631,19 @@
631 "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.",
632 "setup_totp_recommended": "TOTP'yi kurun (Önerilir)",
633 "disable_buy": "Satın alma işlemini devre dışı bırak",
634 + "disable_sell": "Satış işlemini devre dışı bırak",
635 + "cake_2fa_preset" : "Kek 2FA Ön Ayarı",
636 + "narrow": "Dar",
637 + "normal": "Normal",
638 + "aggressive": "Aşırı duyarlı",
639 + "require_for_assessing_wallet": "Cüzdana erişmek için gerekli",
640 + "require_for_sends_to_non_contacts" : "Kişi olmayan kişilere göndermeler için gerekli kıl",
641 + "require_for_sends_to_contacts" : "Kişilere göndermeler için gerekli kıl",
642 + "require_for_sends_to_internal_wallets" : "Dahili cüzdanlara yapılan gönderimler için gereklilik",
643 + "require_for_exchanges_to_internal_wallets" : "Dahili cüzdanlara değişim gerektir",
644 + "require_for_adding_contacts" : "Kişi eklemek için gerekli",
645 + "require_for_creating_new_wallets" : "Yeni cüzdan oluşturmak için gerekli",
646 + "require_for_all_security_and_backup_settings" : "Tüm güvenlik ve yedekleme ayarları için iste",
647 "disable_sell": "Satış işlemini devre dışı bırak",
648 "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.",
649 "syncing_wallet_alert_title": "Cüzdanınız senkronize ediliyor",
res/values/strings_uk.arb
+12
@@ -632,6 +632,18 @@
632 "setup_totp_recommended": "Налаштувати TOTP (рекомендовано)",
633 "disable_buy": "Вимкнути дію покупки",
634 "disable_sell": "Вимкнути дію продажу",
635 + "cake_2fa_preset" : "Торт 2FA Preset",
636 + "narrow": "вузькі",
637 + "normal": "нормальний",
638 + "aggressive": "Надто старанний",
639 + "require_for_assessing_wallet": "Потрібен доступ до гаманця",
640 + "require_for_sends_to_non_contacts" : "Вимагати для надсилання неконтактним особам",
641 + "require_for_sends_to_contacts" : "Вимагати для надсилання контактам",
642 + "require_for_sends_to_internal_wallets" : "Вимагати надсилання на внутрішні гаманці",
643 + "require_for_exchanges_to_internal_wallets" : "Вимагати обміну на внутрішні гаманці",
644 + "require_for_adding_contacts" : "Потрібен для додавання контактів",
645 + "require_for_creating_new_wallets" : "Потрібно для створення нових гаманців",
646 + "require_for_all_security_and_backup_settings" : "Вимагати всіх налаштувань безпеки та резервного копіювання",
647 "available_balance_description": "Це сума, яку ви можете витратити, не включаючи невизначені кошти. Це може бути менше, ніж загальний баланс, якщо ви витратили кошти, які ще не підтверджені.",
648 "syncing_wallet_alert_title": "Ваш гаманець синхронізується",
649 "syncing_wallet_alert_content": "Ваш баланс та список транзакцій може бути неповним, доки вгорі не буде написано «СИНХРОНІЗОВАНО». Натисніть/торкніться, щоб дізнатися більше.",
res/values/strings_ur.arb
+12
@@ -626,6 +626,18 @@
626 "setup_totp_recommended": "TOTP ترتیب دیں (تجویز کردہ)",
627 "disable_buy": "خرید ایکشن کو غیر فعال کریں۔",
628 "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔",
629 + "cake_2fa_preset" : "کیک 2FA پیش سیٹ",
630 + "narrow": "تنگ",
631 + "normal": "نارمل",
632 + "aggressive": "حد سے زیادہ پرجوش",
633 + "require_for_assessing_wallet": "بٹوے تک رسائی کے لیے درکار ہے۔",
634 + "require_for_sends_to_non_contacts" : "غیر رابطوں کو بھیجنے کی ضرورت ہے۔",
635 + "require_for_sends_to_contacts" : "رابطوں کو بھیجنے کی ضرورت ہے۔",
636 + "require_for_sends_to_internal_wallets" : "اندرونی بٹوے پر بھیجنے کے لیے درکار ہے۔",
637 + "require_for_exchanges_to_internal_wallets" : "اندرونی بٹوے میں تبادلے کی ضرورت ہے۔",
638 + "require_for_adding_contacts" : "رابطوں کو شامل کرنے کی ضرورت ہے۔",
639 + "require_for_creating_new_wallets" : "نئے بٹوے بنانے کی ضرورت ہے۔",
640 + "require_for_all_security_and_backup_settings" : "تمام سیکورٹی اور بیک اپ کی ترتیبات کے لیے درکار ہے۔",
641 "available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جو فوری طور پر خرچ کیے جا سکتے ہیں۔ اگر فنڈز کم بیلنس میں ظاہر ہوتے ہیں لیکن اوپر کے بیلنس میں نہیں، تو آپ کو مزید نیٹ ورک کی تصدیقات حاصل کرنے کے لیے آنے والے فنڈز کے لیے چند منٹ انتظار کرنا چاہیے۔ مزید تصدیق حاصل کرنے کے بعد، وہ قابل خرچ ہوں گے۔",
642 "syncing_wallet_alert_title": "آپ کا بٹوہ مطابقت پذیر ہو رہا ہے۔",
643 "syncing_wallet_alert_content": "آپ کے بیلنس اور لین دین کی فہرست اس وقت تک مکمل نہیں ہو سکتی جب تک کہ یہ سب سے اوپر \"SYNCRONIZED\" نہ کہے۔ مزید جاننے کے لیے کلک/تھپتھپائیں۔",
res/values/strings_yo.arb
+12
@@ -628,6 +628,18 @@
628 "setup_totp_recommended": "Sọ TOTP (Kẹṣọdọ)",
629 "disable_buy": "Ko iṣọrọ ọja",
630 "disable_sell": "Ko iṣọrọ iṣọrọ",
631 + "cake_2fa_preset" : "Cake 2FA Tito",
632 + "narrow": "Taara",
633 + "normal": "Deede",
634 + "aggressive": "Onítara",
635 + "require_for_assessing_wallet": "Beere fun wiwọle si apamọwọ",
636 + "require_for_sends_to_non_contacts" : "Beere fun fifiranṣẹ si awọn ti kii ṣe awọn olubasọrọ",
637 + "require_for_sends_to_contacts" : "Beere fun fifiranṣẹ si awọn olubasọrọ",
638 + "require_for_sends_to_internal_wallets" : "Beere fun fifiranṣẹ si awọn apamọwọ inu",
639 + "require_for_exchanges_to_internal_wallets" : "Beere fun awọn paṣipaarọ si awọn apamọwọ inu",
640 + "require_for_adding_contacts" : "Beere fun fifi awọn olubasọrọ kun",
641 + "require_for_creating_new_wallets" : "Beere fun ṣiṣẹda titun Woleti",
642 + "require_for_all_security_and_backup_settings" : "Beere fun gbogbo aabo ati awọn eto afẹyinti",
643 "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.",
644 "syncing_wallet_alert_title": "Apamọwọ rẹ n muṣiṣẹpọ",
645 "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.",
res/values/strings_zh.arb
+12
@@ -631,6 +631,18 @@
631 "setup_totp_recommended": "设置 TOTP(推荐)",
632 "disable_buy": "禁用购买操作",
633 "disable_sell": "禁用卖出操作",
634 + "cake_2fa_preset" : "蛋糕 2FA 预设",
635 + "narrow": "狭窄的",
636 + "normal": "普通的",
637 + "aggressive": "过分热心",
638 + "require_for_assessing_wallet": "需要访问钱包",
639 + "require_for_sends_to_non_contacts" : "需要发送给非联系人",
640 + "require_for_sends_to_contacts" : "需要发送给联系人",
641 + "require_for_sends_to_internal_wallets" : "需要发送到内部钱包",
642 + "require_for_exchanges_to_internal_wallets" : "需要兑换到内部钱包",
643 + "require_for_adding_contacts" : "需要添加联系人",
644 + "require_for_creating_new_wallets" : "创建新钱包的要求",
645 + "require_for_all_security_and_backup_settings" : "需要所有安全和备份设置",
646 "available_balance_description": "可用余额是您可以使用的金额。冻结余额是您当前正在等待确认的金额。",
647 "syncing_wallet_alert_title": "您的钱包正在同步",
648 "syncing_wallet_alert_content": "您的余额和交易列表可能不完整,直到顶部显示“已同步”。单击/点击以了解更多信息。",