From 90486f6839c68aa1ddff59b68b3ca5c2f0f74c62 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 14 May 2025 16:12:03 -0600 Subject: [PATCH 1/6] initial duress pin integration --- lib/main.dart | 29 +- .../new/steps/frost_create_step_5.dart | 55 +- .../reshare/frost_reshare_step_5.dart | 114 ++- .../restore/restore_frost_ms_wallet_view.dart | 329 ++++---- ...w_wallet_recovery_phrase_warning_view.dart | 714 +++++++++--------- .../restore_view_only_wallet_view.dart | 197 +++-- .../restore_wallet_view.dart | 560 +++++++------- .../verify_recovery_phrase_view.dart | 334 ++++---- lib/pages/pinpad_views/create_pin_view.dart | 123 ++- lib/pages/pinpad_views/lock_screen_view.dart | 380 ++++++---- lib/pages/pinpad_views/pinpad_dialog.dart | 44 +- .../change_pin_view/change_pin_view.dart | 119 +-- .../create_duress_pin_view.dart | 257 +++++++ .../security_views/security_view.dart | 328 +++++++- .../wallet_settings_view.dart | 303 ++++---- .../wallet_settings_wallet_settings_view.dart | 251 +++--- lib/providers/global/duress_provider.dart | 4 + lib/providers/providers.dart | 1 + lib/route_generator.dart | 8 + lib/services/wallets.dart | 194 ++--- lib/utilities/prefs.dart | 457 ++++++----- lib/wallets/isar/models/wallet_info.dart | 107 ++- .../providers/all_wallets_info_provider.dart | 48 +- .../draggable_switch_button.dart | 104 ++- 24 files changed, 2899 insertions(+), 2161 deletions(-) create mode 100644 lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart create mode 100644 lib/providers/global/duress_provider.dart diff --git a/lib/main.dart b/lib/main.dart index ba3583668..c521236fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,7 +50,6 @@ import 'pages/pinpad_views/create_pin_view.dart'; import 'pages/pinpad_views/lock_screen_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart'; import 'pages_desktop_specific/password/desktop_login_view.dart'; -import 'providers/db/main_db_provider.dart'; import 'providers/desktop/storage_crypto_handler_provider.dart'; import 'providers/global/auto_swb_service_provider.dart'; import 'providers/global/base_currencies_provider.dart'; @@ -357,7 +356,7 @@ class _MaterialAppWithThemeState extends ConsumerState } } - Future load() async { + Future load(bool loadWallets) async { try { if (didLoad) { return; @@ -387,12 +386,15 @@ class _MaterialAppWithThemeState extends ConsumerState prefs: ref.read(prefsChangeNotifierProvider), ); ref.read(priceAnd24hChangeNotifierProvider).start(true); - await ref - .read(pWallets) - .load( - ref.read(prefsChangeNotifierProvider), - ref.read(mainDBProvider), - ); + if (loadWallets) { + await ref + .read(pWallets) + .load( + ref.read(prefsChangeNotifierProvider), + ref.read(mainDBProvider), + false, + ); + } loadingCompleter.complete(); // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet // unawaited(_nodeService.updateCommunityNodes()); @@ -445,6 +447,13 @@ class _MaterialAppWithThemeState extends ConsumerState @override void initState() { + if (Util.isDesktop) { + // set to false for desktop + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pDuress.notifier).state = false; + }); + } + String themeId; if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { final brightness = WidgetsBinding.instance.window.platformBrightness; @@ -781,7 +790,7 @@ class _MaterialAppWithThemeState extends ConsumerState return DesktopLoginView( startupWalletId: startupWalletId, - load: load, + load: () => load(true), ); } else { return const IntroView(); @@ -792,7 +801,7 @@ class _MaterialAppWithThemeState extends ConsumerState }, ) : FutureBuilder( - future: load(), + future: load(false), builder: ( BuildContext context, AsyncSnapshot snapshot, diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 3c59c7524..9d835ea4e 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -8,12 +8,9 @@ import '../../../../../app_config.dart'; import '../../../../../frost_route_generator.dart'; import '../../../../../notifications/show_flush_bar.dart'; import '../../../../../pages_desktop_specific/desktop_home_view.dart'; -import '../../../../../providers/db/main_db_provider.dart'; import '../../../../../providers/frost_wallet/frost_wallet_providers.dart'; -import '../../../../../providers/global/node_service_provider.dart'; -import '../../../../../providers/global/prefs_provider.dart'; import '../../../../../providers/global/secure_store_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../services/frost.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; @@ -43,7 +40,8 @@ class FrostCreateStep5 extends ConsumerStatefulWidget { } class _FrostCreateStep5State extends ConsumerState { - static const _warning = "These are your private keys. Please back them up, " + static const _warning = + "These are your private keys. Please back them up, " "keep them safe and never share it with anyone. Your private keys are the" " only way you can access your funds if you forget PIN, lose your phone, " "etc. ${AppConfig.prefix} does not keep nor is able to restore your private keys" @@ -79,9 +77,10 @@ class _FrostCreateStep5State extends ConsumerState { child: Text( _warning, style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, ), ), ), @@ -89,25 +88,19 @@ class _FrostCreateStep5State extends ConsumerState { DetailItem( title: "Multisig Config", detail: multisigConfig, - button: Util.isDesktop - ? IconCopyButton( - data: multisigConfig, - ) - : SimpleCopyButton( - data: multisigConfig, - ), + button: + Util.isDesktop + ? IconCopyButton(data: multisigConfig) + : SimpleCopyButton(data: multisigConfig), ), const SizedBox(height: 12), DetailItem( title: "Keys", detail: serializedKeys, - button: Util.isDesktop - ? IconCopyButton( - data: serializedKeys, - ) - : SimpleCopyButton( - data: serializedKeys, - ), + button: + Util.isDesktop + ? IconCopyButton(data: serializedKeys) + : SimpleCopyButton(data: serializedKeys), ), if (!Util.isDesktop) const Spacer(), const SizedBox(height: 12), @@ -133,10 +126,7 @@ class _FrostCreateStep5State extends ConsumerState { useSafeArea: true, builder: (ctx) { return const Center( - child: LoadingIndicator( - width: 50, - height: 50, - ), + child: LoadingIndicator(width: 50, height: 50), ); }, ), @@ -177,6 +167,13 @@ class _FrostCreateStep5State extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); // pop progress dialog @@ -191,9 +188,7 @@ class _FrostCreateStep5State extends ConsumerState { if (Util.isDesktop) { nav.popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), + ModalRoute.withName(DesktopHomeView.routeName), ); } else { unawaited( @@ -219,7 +214,7 @@ class _FrostCreateStep5State extends ConsumerState { ); } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); // pop progress dialog if (context.mounted && !progressPopped) { diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart index 83fa55944..356edef59 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -4,12 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../frost_route_generator.dart'; import '../../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../../pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/frost_wallet/frost_wallet_providers.dart'; -import '../../../../providers/global/node_service_provider.dart'; -import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; -import '../../../../providers/global/wallets_provider.dart'; +import '../../../../providers/providers.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; @@ -68,12 +65,20 @@ class _FrostReshareStep5State extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); } else { - wallet = ref - .read(pWallets) - .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) - as BitcoinFrostWallet; + wallet = + ref + .read(pWallets) + .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; } if (mounted) { @@ -96,22 +101,19 @@ class _FrostReshareStep5State extends ConsumerState { if (mounted) { ref.read(pFrostResharingData).reset(); ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; - ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( - ModalRoute.withName( - _popUntilPath, - ), - ); + ref + .read(pFrostScaffoldArgs) + ?.parentNav + .popUntil(ModalRoute.withName(_popUntilPath)); } } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (mounted) { await showDialog( context: context, - builder: (_) => FrostErrorDialog( - title: "Error", - message: e.toString(), - ), + builder: + (_) => FrostErrorDialog(title: "Error", message: e.toString()), ); } } finally { @@ -119,11 +121,12 @@ class _FrostReshareStep5State extends ConsumerState { } } - String get _popUntilPath => isNew - ? Util.isDesktop - ? DesktopHomeView.routeName - : HomeView.routeName - : Util.isDesktop + String get _popUntilPath => + isNew + ? Util.isDesktop + ? DesktopHomeView.routeName + : HomeView.routeName + : Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName; @@ -134,7 +137,8 @@ class _FrostReshareStep5State extends ConsumerState { ref.read(pFrostResharingData).newWalletData!.serializedKeys; reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId; - isNew = ref.read(pFrostResharingData).incompleteWallet != null && + isNew = + ref.read(pFrostResharingData).incompleteWallet != null && ref.read(pFrostResharingData).incompleteWallet!.walletId == ref.read(pFrostScaffoldArgs)!.walletId!; @@ -151,66 +155,42 @@ class _FrostReshareStep5State extends ConsumerState { "Ensure your reshare ID matches that of each other participant", style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), DetailItem( title: "ID", detail: reshareId, - button: Util.isDesktop - ? IconCopyButton( - data: reshareId, - ) - : SimpleCopyButton( - data: reshareId, - ), - ), - const SizedBox( - height: 12, - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: reshareId) + : SimpleCopyButton(data: reshareId), ), + const SizedBox(height: 12), + const SizedBox(height: 12), Text( "Back up your keys and config", style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), DetailItem( title: "Config", detail: config, - button: Util.isDesktop - ? IconCopyButton( - data: config, - ) - : SimpleCopyButton( - data: config, - ), - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: config) + : SimpleCopyButton(data: config), ), + const SizedBox(height: 12), DetailItem( title: "Keys", detail: serializedKeys, - button: Util.isDesktop - ? IconCopyButton( - data: serializedKeys, - ) - : SimpleCopyButton( - data: serializedKeys, - ), + button: + Util.isDesktop + ? IconCopyButton(data: serializedKeys) + : SimpleCopyButton(data: serializedKeys), ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), - PrimaryButton( - label: "Confirm", - onPressed: _onPressed, - ), + const SizedBox(height: 12), + PrimaryButton(label: "Confirm", onPressed: _onPressed), ], ), ); diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 88f7c3b26..64deee9be 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -9,11 +9,8 @@ import 'package:frostdart/frostdart.dart' as frost; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages_desktop_specific/desktop_home_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; -import '../../../../providers/global/node_service_provider.dart'; -import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; -import '../../../../providers/global/wallets_provider.dart'; +import '../../../../providers/providers.dart'; import '../../../../services/frost.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; @@ -95,9 +92,7 @@ class _RestoreFrostMsWalletViewState knownSalts: [], participants: participants, myName: myName, - threshold: frost.multisigThreshold( - multisigConfig: config, - ), + threshold: frost.multisigThreshold(multisigConfig: config), ); await ref.read(mainDBProvider).isar.writeTxn(() async { @@ -110,9 +105,14 @@ class _RestoreFrostMsWalletViewState isRescan: false, ); - await info.setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); + await info.setMnemonicVerified(isar: ref.read(mainDBProvider).isar); + + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } return wallet; } @@ -147,17 +147,14 @@ class _RestoreFrostMsWalletViewState if (mounted) { if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), + Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false), ); } @@ -171,20 +168,17 @@ class _RestoreFrostMsWalletViewState ); } } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Failed to restore", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: "Failed to restore", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } } finally { @@ -215,9 +209,7 @@ class _RestoreFrostMsWalletViewState if (Platform.isAndroid || Platform.isIOS) { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } final qrResult = await BarcodeScanner.scan(); @@ -235,9 +227,7 @@ class _RestoreFrostMsWalletViewState ); if (qrResult == null) { - Logging.instance.d( - "Qr scanning cancelled", - ); + Logging.instance.d("Qr scanning cancelled"); } else { // TODO [prio=low]: Validate QR code data. configFieldController.text = qrResult; @@ -260,67 +250,64 @@ class _RestoreFrostMsWalletViewState Widget build(BuildContext context) { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - // TODO: [prio=high] get rid of placeholder text?? - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + builder: + (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? + trailing: FrostMascot( + title: 'Lorem ipsum', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), + ), + body: SizedBox(width: 480, child: child), ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), child: ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Restore FROST multisig wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Restore FROST multisig wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), ), - ), - ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -350,51 +337,53 @@ class _RestoreFrostMsWalletViewState right: 5, ), suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_configEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: + _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_configEmpty) TextFieldIconButton( semanticsLabel: @@ -410,9 +399,7 @@ class _RestoreFrostMsWalletViewState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -442,51 +429,53 @@ class _RestoreFrostMsWalletViewState right: 5, ), suffixIcon: Padding( - padding: _keysEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _keysEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_keysEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Keys Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - keysFieldController.text = ""; - - setState(() { - _keysEmpty = true; - }); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Keys Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + keysFieldController.text = ""; + + setState(() { + _keysEmpty = true; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Keys Field.", - key: const Key("frKeysPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data!.text!.isNotEmpty) { - keysFieldController.text = - data.text!.trim(); - } - - setState(() { - _keysEmpty = - keysFieldController.text.isEmpty; - }); - }, - child: _keysEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Keys Field.", + key: const Key("frKeysPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + keysFieldController.text = + data.text!.trim(); + } + + setState(() { + _keysEmpty = + keysFieldController.text.isEmpty; + }); + }, + child: + _keysEmpty + ? const ClipboardIcon() + : const XIcon(), + ), ], ), ), @@ -494,13 +483,9 @@ class _RestoreFrostMsWalletViewState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), PrimaryButton( label: "Restore", enabled: !_keysEmpty && !_configEmpty, diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 7d93d038a..a35c4563a 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -19,7 +19,6 @@ import 'package:tuple/tuple.dart'; import '../../../app_config.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../services/transaction_notification_tracker.dart'; @@ -81,11 +80,12 @@ class _NewWalletRecoveryPhraseWarningViewState if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Create Wallet Error", - message: ex?.toString() ?? "Unknown error", - maxWidth: 600, - ), + builder: + (_) => StackOkDialog( + title: "Create Wallet Error", + message: ex?.toString() ?? "Unknown error", + maxWidth: 600, + ), ); } return; @@ -95,10 +95,7 @@ class _NewWalletRecoveryPhraseWarningViewState unawaited( nav.pushNamed( NewWalletRecoveryPhraseView.routeName, - arguments: Tuple2( - result.$1, - result.$2, - ), + arguments: Tuple2(result.$1, result.$2), ), ); } @@ -107,12 +104,12 @@ class _NewWalletRecoveryPhraseWarningViewState Future<(Wallet, List)> _initNewFuture() async { try { - String? otherDataJsonString; + Map? otherDataJson; if (widget.coin is Tezos) { - otherDataJsonString = jsonEncode({ + otherDataJson = { WalletInfoKeys.tezosDerivationPath: Tezos.standardDerivationPath.value, - }); + }; // }//todo: probably not needed (broken anyways) // else if (widget.coin is Epiccash) { // final int secondsSinceEpoch = @@ -145,42 +142,32 @@ class _NewWalletRecoveryPhraseWarningViewState // }, // ); } else if (widget.coin is Firo) { - otherDataJsonString = jsonEncode( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - }, - ); + otherDataJson = {WalletInfoKeys.lelantusCoinIsarRescanRequired: false}; + } + + if (ref.read(pDuress)) { + otherDataJson ??= {}; + otherDataJson[WalletInfoKeys.duressMarkedVisibleWalletKey] = true; } final info = WalletInfo.createNew( coin: widget.coin, name: widget.walletName, - otherDataJsonString: otherDataJsonString, + otherDataJsonString: jsonEncode(otherDataJson), ); var node = ref - .read( - nodeServiceChangeNotifierProvider, - ) - .getPrimaryNodeFor( - currency: coin, - ); + .read(nodeServiceChangeNotifierProvider) + .getPrimaryNodeFor(currency: coin); if (node == null) { node = coin.defaultNode; await ref - .read( - nodeServiceChangeNotifierProvider, - ) - .setPrimaryNodeFor( - coin: coin, - node: node, - ); + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor(coin: coin, node: node); } - final txTracker = TransactionNotificationTracker( - walletId: info.walletId, - ); + final txTracker = TransactionNotificationTracker(walletId: info.walletId); String? mnemonicPassphrase; String? mnemonic; @@ -195,22 +182,14 @@ class _NewWalletRecoveryPhraseWarningViewState // currently a special case due to the // xmr/wow libraries handling their // own mnemonic generation - wordCount = ref.read(pNewWalletOptions)?.mnemonicWordsCount ?? + wordCount = + ref.read(pNewWalletOptions)?.mnemonicWordsCount ?? info.coin.defaultSeedPhraseLength; } else if (wordCount > 0) { - if (ref - .read( - pNewWalletOptions.state, - ) - .state != - null) { + if (ref.read(pNewWalletOptions.state).state != null) { if (coin.hasMnemonicPassphraseSupport) { - mnemonicPassphrase = ref - .read( - pNewWalletOptions.state, - ) - .state! - .mnemonicPassphrase; + mnemonicPassphrase = + ref.read(pNewWalletOptions.state).state!.mnemonicPassphrase; } else { // this may not be epiccash specific? if (coin is Epiccash) { @@ -218,39 +197,27 @@ class _NewWalletRecoveryPhraseWarningViewState } } - wordCount = ref - .read( - pNewWalletOptions.state, - ) - .state! - .mnemonicWordsCount; + wordCount = + ref.read(pNewWalletOptions.state).state!.mnemonicWordsCount; } else { mnemonicPassphrase = ""; } if (wordCount < 12 || 24 < wordCount || wordCount % 3 != 0) { - throw Exception( - "Invalid word count", - ); + throw Exception("Invalid word count"); } final strength = (wordCount ~/ 3) * 32; - mnemonic = bip39.generateMnemonic( - strength: strength, - ); + mnemonic = bip39.generateMnemonic(strength: strength); } final wallet = await Wallet.create( walletInfo: info, mainDB: ref.read(mainDBProvider), secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read( - nodeServiceChangeNotifierProvider, - ), - prefs: ref.read( - prefsChangeNotifierProvider, - ), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), mnemonicPassphrase: mnemonicPassphrase, mnemonic: mnemonic, privateKey: privateKey, @@ -263,18 +230,14 @@ class _NewWalletRecoveryPhraseWarningViewState } // set checkbox back to unchecked to annoy users to agree again :P - ref - .read( - checkBoxStateProvider.state, - ) - .state = false; + ref.read(checkBoxStateProvider.state).state = false; final fetchedMnemonic = await (wallet as MnemonicInterface).getMnemonicAsWords(); return (wallet, fetchedMnemonic); } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); rethrow; } } @@ -297,302 +260,309 @@ class _NewWalletRecoveryPhraseWarningViewState return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: const AppBarBackButton(), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AppBarIconButton( - semanticsLabel: - "Question Button. Opens A Dialog For Recovery Phrase Explanation.", - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: const AppBarBackButton(), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AppBarIconButton( + semanticsLabel: + "Question Button. Opens A Dialog For Recovery Phrase Explanation.", + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: () async { + await showDialog( + context: context, + builder: + (context) => + const RecoveryPhraseExplanationDialog(), + ); + }, ), - onPressed: () async { - await showDialog( - context: context, - builder: (context) => - const RecoveryPhraseExplanationDialog(), - ); - }, ), - ), - ], - ), + ], + ), body: SingleChildScrollView( child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: isDesktop ? 480 : double.infinity), + constraints: BoxConstraints( + maxWidth: isDesktop ? 480 : double.infinity, + ), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(16), child: Center( child: Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.center - : CrossAxisAlignment.stretch, + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.center + : CrossAxisAlignment.stretch, children: [ /*if (isDesktop) const Spacer( flex: 10, ),*/ - if (!isDesktop) - const SizedBox( - height: 4, - ), + if (!isDesktop) const SizedBox(height: 4), if (!isDesktop) Text( walletName, textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - if (!isDesktop) - const SizedBox( - height: 4, + style: STextStyles.label( + context, + ).copyWith(fontSize: 12), ), + if (!isDesktop) const SizedBox(height: 4), Text( "Recovery Phrase", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 32 : 16, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 32 : 16), RoundedWhiteContainer( padding: const EdgeInsets.all(32), width: isDesktop ? 480 : null, - child: isDesktop - ? Text( - "On the next screen you will see " - "$seedCount " - "words that make up your recovery phrase.\n\nPlease " - "write it down. Keep it safe and never share it with " - "anyone. Your recovery phrase is the only way you can" - " access your funds if you forget your PIN, lose your" - " phone, etc.\n\n${AppConfig.appName} does not keep nor is " - "able to restore your recover phrase. Only you have " - "access to your wallet.", - style: isDesktop - ? STextStyles.desktopTextMediumRegular( + child: + isDesktop + ? Text( + "On the next screen you will see " + "$seedCount " + "words that make up your recovery phrase.\n\nPlease " + "write it down. Keep it safe and never share it with " + "anyone. Your recovery phrase is the only way you can" + " access your funds if you forget your PIN, lose your" + " phone, etc.\n\n${AppConfig.appName} does not keep nor is " + "able to restore your recover phrase. Only you have " + "access to your wallet.", + style: + isDesktop + ? STextStyles.desktopTextMediumRegular( + context, + ) + : STextStyles.subtitle( + context, + ).copyWith(fontSize: 12), + ) + : Column( + children: [ + Text( + "Important", + style: STextStyles.desktopH3( context, - ) - : STextStyles.subtitle(context).copyWith( - fontSize: 12, - ), - ) - : Column( - children: [ - Text( - "Important", - style: - STextStyles.desktopH3(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - ), - const SizedBox( - height: 24, - ), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: STextStyles.desktopH3(context) - .copyWith(fontSize: 18), - children: [ - TextSpan( - text: - "On the next screen you will be given ", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: "$seedCount words", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: ". They are your ", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: "recovery phrase", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) + ).copyWith( + color: + Theme.of(context) .extension()! .accentColorBlue, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: ".", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - fontSize: 18, - height: 1.3, - ), - ), - ], + ), ), - ), - const SizedBox( - height: 40, - ), - Column( - children: [ - Row( + const SizedBox(height: 24), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: STextStyles.desktopH3( + context, + ).copyWith(fontSize: 18), children: [ - SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - radiusMultiplier: 20, - padding: const EdgeInsets.all(9), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: SvgPicture.asset( - Assets.svg.pencil, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), + TextSpan( + text: + "On the next screen you will be given ", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + fontSize: 18, + height: 1.3, ), ), - const SizedBox( - width: 20, - ), - Text( - "Write them down.", - style: - STextStyles.navBarTitle(context), + TextSpan( + text: "$seedCount words", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorBlue, + fontSize: 18, + height: 1.3, + ), ), - ], - ), - const SizedBox( - height: 30, - ), - Row( - children: [ - SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - radiusMultiplier: 20, - padding: const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: SvgPicture.asset( - Assets.svg.lock, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), + TextSpan( + text: ". They are your ", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + fontSize: 18, + height: 1.3, ), ), - const SizedBox( - width: 20, + TextSpan( + text: "recovery phrase", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorBlue, + fontSize: 18, + height: 1.3, + ), ), - Text( - "Keep them safe.", - style: - STextStyles.navBarTitle(context), + TextSpan( + text: ".", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + fontSize: 18, + height: 1.3, + ), ), ], ), - const SizedBox( - height: 30, - ), - Row( - children: [ - SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - radiusMultiplier: 20, - padding: const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: SvgPicture.asset( - Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .accentColorDark, + ), + const SizedBox(height: 40), + Column( + children: [ + Row( + children: [ + SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + radiusMultiplier: 20, + padding: const EdgeInsets.all(9), + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, + child: SvgPicture.asset( + Assets.svg.pencil, + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), ), ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: Text( - "Do not show them to anyone.", + const SizedBox(width: 20), + Text( + "Write them down.", style: STextStyles.navBarTitle( context, ), ), - ), - ], - ), - ], - ), - ], - ), + ], + ), + const SizedBox(height: 30), + Row( + children: [ + SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + radiusMultiplier: 20, + padding: const EdgeInsets.all(8), + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, + child: SvgPicture.asset( + Assets.svg.lock, + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + ), + ), + const SizedBox(width: 20), + Text( + "Keep them safe.", + style: STextStyles.navBarTitle( + context, + ), + ), + ], + ), + const SizedBox(height: 30), + Row( + children: [ + SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + radiusMultiplier: 20, + padding: const EdgeInsets.all(8), + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, + child: SvgPicture.asset( + Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Text( + "Do not show them to anyone.", + style: STextStyles.navBarTitle( + context, + ), + ), + ), + ], + ), + ], + ), + ], + ), ), if (!isDesktop) const Spacer(), - if (!isDesktop) - const SizedBox( - height: 16, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), + if (!isDesktop) const SizedBox(height: 16), + if (isDesktop) const SizedBox(height: 32), ConstrainedBox( constraints: BoxConstraints( maxWidth: isDesktop ? 480 : 0, @@ -605,9 +575,10 @@ class _NewWalletRecoveryPhraseWarningViewState children: [ GestureDetector( onTap: () { - final value = ref - .read(checkBoxStateProvider.state) - .state; + final value = + ref + .read(checkBoxStateProvider.state) + .state; ref.read(checkBoxStateProvider.state).state = !value; }, @@ -623,11 +594,12 @@ class _NewWalletRecoveryPhraseWarningViewState child: Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: ref - .watch( - checkBoxStateProvider.state, - ) - .state, + value: + ref + .watch( + checkBoxStateProvider.state, + ) + .state, onChanged: (newValue) { ref .read( @@ -637,65 +609,67 @@ class _NewWalletRecoveryPhraseWarningViewState }, ), ), - SizedBox( - width: isDesktop ? 20 : 10, - ), + SizedBox(width: isDesktop ? 20 : 10), Flexible( child: Text( "I understand that ${AppConfig.appName} does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.", - style: isDesktop - ? STextStyles.desktopTextMedium( - context, - ) - : STextStyles.baseXS(context) - .copyWith( - height: 1.3, - ), + style: + isDesktop + ? STextStyles.desktopTextMedium( + context, + ) + : STextStyles.baseXS( + context, + ).copyWith(height: 1.3), ), ), ], ), ), ), - SizedBox( - height: isDesktop ? 32 : 16, - ), + SizedBox(height: isDesktop ? 32 : 16), ConstrainedBox( constraints: BoxConstraints( minHeight: isDesktop ? 70 : 0, ), child: TextButton( - onPressed: ref - .read(checkBoxStateProvider.state) - .state - ? _initNewWallet - : null, - style: ref - .read(checkBoxStateProvider.state) - .state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle( - context, - ), - child: Text( - "View recovery phrase", - style: isDesktop - ? ref - .read( - checkBoxStateProvider.state, - ) - .state - ? STextStyles.desktopButtonEnabled( + onPressed: + ref + .read(checkBoxStateProvider.state) + .state + ? _initNewWallet + : null, + style: + ref + .read(checkBoxStateProvider.state) + .state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle( context, ) - : STextStyles.desktopButtonDisabled( + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( context, - ) - : STextStyles.button(context), + ), + child: Text( + "View recovery phrase", + style: + isDesktop + ? ref + .read( + checkBoxStateProvider + .state, + ) + .state + ? STextStyles.desktopButtonEnabled( + context, + ) + : STextStyles.desktopButtonDisabled( + context, + ) + : STextStyles.button(context), ), ), ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart index b4b7d03e3..75bda4577 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:cs_monero/src/deprecated/get_height_by_date.dart' - as cs_monero_deprecated; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,7 +11,6 @@ import 'package:wakelock_plus/wakelock_plus.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; @@ -91,9 +88,7 @@ class _RestoreViewOnlyWalletViewState if (!Util.isDesktop) { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); } if (mounted) { @@ -102,9 +97,7 @@ class _RestoreViewOnlyWalletViewState useSafeArea: false, barrierDismissible: true, builder: (context) { - return ConfirmRecoveryDialog( - onConfirm: _attemptRestore, - ); + return ConfirmRecoveryDialog(onConfirm: _attemptRestore); }, ); } @@ -121,17 +114,15 @@ class _RestoreViewOnlyWalletViewState final ViewOnlyWalletType viewOnlyWalletType; if (widget.coin is Bip39HDCurrency) { if (widget.coin is Firo) { - otherDataJson.addAll( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - WalletInfoKeys.enableLelantusScanning: - widget.enableLelantusScanning, - }, - ); + otherDataJson.addAll({ + WalletInfoKeys.lelantusCoinIsarRescanRequired: false, + WalletInfoKeys.enableLelantusScanning: widget.enableLelantusScanning, + }); } - viewOnlyWalletType = _addressOnly - ? ViewOnlyWalletType.addressOnly - : ViewOnlyWalletType.xPub; + viewOnlyWalletType = + _addressOnly + ? ViewOnlyWalletType.addressOnly + : ViewOnlyWalletType.xPub; } else if (widget.coin is CryptonoteCurrency) { viewOnlyWalletType = ViewOnlyWalletType.cryptonote; } else { @@ -166,10 +157,9 @@ class _RestoreViewOnlyWalletViewState onCancel: () async { isRestoring = false; - await ref.read(pWallets).deleteWallet( - info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(info, ref.read(secureStoreProvider)); }, ); }, @@ -220,10 +210,9 @@ class _RestoreViewOnlyWalletViewState if (node == null) { node = widget.coin.defaultNode; - await ref.read(nodeServiceChangeNotifierProvider).setPrimaryNodeFor( - coin: widget.coin, - node: node, - ); + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor(coin: widget.coin, node: node); } try { @@ -267,21 +256,25 @@ class _RestoreViewOnlyWalletViewState isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); if (mounted) { if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), + Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false), ); } @@ -329,9 +322,10 @@ class _RestoreViewOnlyWalletViewState viewKeyController = TextEditingController(); if (widget.coin is Bip39HDCurrency) { - _currentDropDownValue = (widget.coin as Bip39HDCurrency) - .supportedHardenedDerivationPaths - .last; + _currentDropDownValue = + (widget.coin as Bip39HDCurrency) + .supportedHardenedDerivationPaths + .last; } } @@ -350,27 +344,28 @@ class _RestoreViewOnlyWalletViewState return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), ), - ), body: Container( color: Theme.of(context).extension()!.background, child: LayoutBuilder( @@ -386,28 +381,21 @@ class _RestoreViewOnlyWalletViewState padding: const EdgeInsets.all(16), child: Column( children: [ - if (isDesktop) - const Spacer( - flex: 10, - ), + if (isDesktop) const Spacer(flex: 10), if (!isDesktop) Text( widget.walletName, style: STextStyles.itemSubtitle(context), ), - SizedBox( - height: isDesktop ? 0 : 4, - ), + SizedBox(height: isDesktop ? 0 : 4), Text( "Enter view only details", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), - if (isElectrumX) - SizedBox( - height: isDesktop ? 24 : 16, - ), + if (isElectrumX) SizedBox(height: isDesktop ? 24 : 16), if (isElectrumX) SizedBox( height: isDesktop ? 56 : 48, @@ -416,12 +404,14 @@ class _RestoreViewOnlyWalletViewState key: UniqueKey(), onText: "Extended pub key", offText: "Single address", - onColor: Theme.of(context) - .extension()! - .popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + onColor: + Theme.of( + context, + ).extension()!.popupBG, + offColor: + Theme.of(context) + .extension()! + .textFieldDefaultBG, isOn: _addressOnly, onValueChanged: (value) { setState(() { @@ -436,9 +426,7 @@ class _RestoreViewOnlyWalletViewState ), ), ), - SizedBox( - height: isDesktop ? 24 : 16, - ), + SizedBox(height: isDesktop ? 24 : 16), if (!isElectrumX || _addressOnly) FullTextField( key: const Key("viewOnlyAddressRestoreFieldKey"), @@ -452,16 +440,14 @@ class _RestoreViewOnlyWalletViewState }); } else { setState(() { - _enableRestoreButton = newValue.isNotEmpty && + _enableRestoreButton = + newValue.isNotEmpty && viewKeyController.text.isNotEmpty; }); } }, ), - if (!isElectrumX) - SizedBox( - height: isDesktop ? 16 : 12, - ), + if (!isElectrumX) SizedBox(height: isDesktop ? 16 : 12), if (isElectrumX && !_addressOnly) DropdownButtonHideUnderline( child: DropdownButton2( @@ -489,9 +475,10 @@ class _RestoreViewOnlyWalletViewState isExpanded: true, buttonStyleData: ButtonStyleData( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -504,9 +491,10 @@ class _RestoreViewOnlyWalletViewState Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ), @@ -514,9 +502,10 @@ class _RestoreViewOnlyWalletViewState offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -531,9 +520,7 @@ class _RestoreViewOnlyWalletViewState ), ), if (isElectrumX && !_addressOnly) - SizedBox( - height: isDesktop ? 16 : 12, - ), + SizedBox(height: isDesktop ? 16 : 12), if (!isElectrumX || !_addressOnly) FullTextField( key: const Key("viewOnlyKeyRestoreFieldKey"), @@ -548,26 +535,22 @@ class _RestoreViewOnlyWalletViewState }); } else { setState(() { - _enableRestoreButton = value.isNotEmpty && + _enableRestoreButton = + value.isNotEmpty && addressController.text.isNotEmpty; }); } }, ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 24 : 16, - ), + SizedBox(height: isDesktop ? 24 : 16), PrimaryButton( enabled: _enableRestoreButton, onPressed: _requestRestore, width: isDesktop ? 480 : null, label: "Restore", ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 59d479112..90ef055e2 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -22,13 +22,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; - import 'package:xelis_flutter/src/api/seed_search_engine.dart' as x_seed; import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../services/transaction_notification_tracker.dart'; @@ -48,8 +46,8 @@ import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; -import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -158,9 +156,10 @@ class _RestoreWalletViewState extends ConsumerState { _seedWordCount = widget.seedWordsLength; isDesktop = Util.isDesktop; - textSelectionControls = Platform.isIOS - ? CustomCupertinoTextSelectionControls(onPaste: onControlsPaste) - : CustomMaterialTextSelectionControls(onPaste: onControlsPaste); + textSelectionControls = + Platform.isIOS + ? CustomCupertinoTextSelectionControls(onPaste: onControlsPaste) + : CustomMaterialTextSelectionControls(onPaste: onControlsPaste); scanner = widget.barcodeScanner; for (int i = 0; i < _seedWordCount; i++) { @@ -170,7 +169,9 @@ class _RestoreWalletViewState extends ConsumerState { } if (widget.coin is Xelis) { - _xelisSeedSearch = x_seed.SearchEngine.init(languageIndex: BigInt.from(0)); + _xelisSeedSearch = x_seed.SearchEngine.init( + languageIndex: BigInt.from(0), + ); } super.initState(); @@ -213,10 +214,7 @@ class _RestoreWalletViewState extends ConsumerState { OutlineInputBorder _buildOutlineInputBorder(Color color) { return OutlineInputBorder( - borderSide: BorderSide( - width: 1, - color: color, - ), + borderSide: BorderSide(width: 1, color: color), borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ); } @@ -236,34 +234,31 @@ class _RestoreWalletViewState extends ConsumerState { // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin is Epiccash) { - otherDataJsonString = jsonEncode( - { - WalletInfoKeys.epiccashData: jsonEncode( - ExtraEpiccashWalletInfo( - receivingIndex: 0, - changeIndex: 0, - slatesToAddresses: {}, - slatesToCommits: {}, - lastScannedBlock: height, - restoreHeight: height, - creationHeight: height, - ).toMap(), - ), - }, - ); + otherDataJsonString = jsonEncode({ + WalletInfoKeys.epiccashData: jsonEncode( + ExtraEpiccashWalletInfo( + receivingIndex: 0, + changeIndex: 0, + slatesToAddresses: {}, + slatesToCommits: {}, + lastScannedBlock: height, + restoreHeight: height, + creationHeight: height, + ).toMap(), + ), + }); } else if (widget.coin is Firo) { - otherDataJsonString = jsonEncode( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - WalletInfoKeys.enableLelantusScanning: - widget.enableLelantusScanning, - }, - ); + otherDataJsonString = jsonEncode({ + WalletInfoKeys.lelantusCoinIsarRescanRequired: false, + WalletInfoKeys.enableLelantusScanning: widget.enableLelantusScanning, + }); } // TODO: do actual check to make sure it is a valid mnemonic for monero + xelis if (bip39.validateMnemonic(mnemonic) == false && - !(widget.coin is Monero || widget.coin is Wownero || widget.coin is Xelis)) { + !(widget.coin is Monero || + widget.coin is Wownero || + widget.coin is Xelis)) { unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -297,10 +292,9 @@ class _RestoreWalletViewState extends ConsumerState { if (mounted) setState(() => _hideSeedWords = false); - await ref.read(pWallets).deleteWallet( - info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(info, ref.read(secureStoreProvider)); }, ); }, @@ -314,14 +308,14 @@ class _RestoreWalletViewState extends ConsumerState { if (node == null) { node = widget.coin.defaultNode; - await ref.read(nodeServiceChangeNotifierProvider).setPrimaryNodeFor( - coin: widget.coin, - node: node, - ); + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor(coin: widget.coin, node: node); } - final txTracker = - TransactionNotificationTracker(walletId: info.walletId); + final txTracker = TransactionNotificationTracker( + walletId: info.walletId, + ); try { final wallet = await Wallet.create( @@ -368,15 +362,24 @@ class _RestoreWalletViewState extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); - final isCreateSpecialEthWallet = - ref.read(createSpecialEthWalletRoutingFlag); + final isCreateSpecialEthWallet = ref.read( + createSpecialEthWalletRoutingFlag, + ); if (isCreateSpecialEthWallet) { ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; - ref.read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state).state = - !ref + ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = !ref .read( newEthWalletTriggerTempUntilHiveCompletelyDeleted.state, ) @@ -385,17 +388,13 @@ class _RestoreWalletViewState extends ConsumerState { if (mounted) { if (isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { if (isCreateSpecialEthWallet) { Navigator.of(context).popUntil( - ModalRoute.withName( - SelectWalletForTokenView.routeName, - ), + ModalRoute.withName(SelectWalletForTokenView.routeName), ); } else { unawaited( @@ -488,57 +487,54 @@ class _RestoreWalletViewState extends ConsumerState { break; case FormInputStatus.invalid: color = Theme.of(context).extension()!.textFieldErrorBG; - prefixColor = Theme.of(context) - .extension()! - .textFieldErrorSearchIconLeft; + prefixColor = + Theme.of( + context, + ).extension()!.textFieldErrorSearchIconLeft; borderColor = Theme.of(context).extension()!.textFieldErrorBorder; suffixIcon = SvgPicture.asset( Assets.svg.alertCircle, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .textFieldErrorSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldErrorSearchIconRight, ); break; case FormInputStatus.valid: color = Theme.of(context).extension()!.textFieldSuccessBG; - prefixColor = Theme.of(context) - .extension()! - .textFieldSuccessSearchIconLeft; + prefixColor = + Theme.of( + context, + ).extension()!.textFieldSuccessSearchIconLeft; borderColor = Theme.of(context).extension()!.textFieldSuccessBorder; suffixIcon = SvgPicture.asset( Assets.svg.checkCircle, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .textFieldSuccessSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldSuccessSearchIconRight, ); break; } return InputDecoration( fillColor: color, filled: true, - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), + contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), prefixIcon: Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.only( - left: 12, - bottom: 2, - ), + padding: const EdgeInsets.only(left: 12, bottom: 2), child: Text( prefix, - style: STextStyles.fieldLabel(context).copyWith( - color: prefixColor, - fontSize: Util.isDesktop ? 16 : 14, - ), + style: STextStyles.fieldLabel( + context, + ).copyWith(color: prefixColor, fontSize: Util.isDesktop ? 16 : 14), ), ), ), @@ -633,8 +629,9 @@ class _RestoreWalletViewState extends ConsumerState { Future pasteMnemonic() async { //todo: check if print needed // debugPrint("restoreWalletPasteButton tapped"); - final ClipboardData? data = - await widget.clipboard.getData(Clipboard.kTextPlain); + final ClipboardData? data = await widget.clipboard.getData( + Clipboard.kTextPlain, + ); if (data?.text != null && data!.text!.isNotEmpty) { final content = data.text!.trim(); @@ -646,9 +643,7 @@ class _RestoreWalletViewState extends ConsumerState { Future requestRestore() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); if (mounted) { await showDialog( @@ -656,9 +651,7 @@ class _RestoreWalletViewState extends ConsumerState { useSafeArea: false, barrierDismissible: true, builder: (context) { - return ConfirmRecoveryDialog( - onConfirm: attemptRestore, - ); + return ConfirmRecoveryDialog(onConfirm: attemptRestore); }, ); } @@ -669,85 +662,90 @@ class _RestoreWalletViewState extends ConsumerState { final isDesktop = Util.isDesktop; return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - semanticsLabel: - "View QR Code Button. Opens Camera To Scan QR Code For Restoring Wallet.", - key: const Key("restoreWalletViewQrCodeButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: QrCodeIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: + "View QR Code Button. Opens Camera To Scan QR Code For Restoring Wallet.", + key: const Key("restoreWalletViewQrCodeButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: QrCodeIcon( + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: scanMnemonicQr, ), - onPressed: scanMnemonicQr, ), ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard For Restoring Wallet.", - key: const Key("restoreWalletPasteButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: ClipboardIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard For Restoring Wallet.", + key: const Key("restoreWalletPasteButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: ClipboardIcon( + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: pasteMnemonic, ), - onPressed: pasteMnemonic, ), ), - ), - ], - ), + ], + ), body: Container( color: Theme.of(context).extension()!.background, child: Padding( @@ -765,27 +763,23 @@ class _RestoreWalletViewState extends ConsumerState { widget.walletName, style: STextStyles.itemSubtitle(context), ), - SizedBox( - height: isDesktop ? 0 : 4, - ), + SizedBox(height: isDesktop ? 0 : 4), Text( "Recovery phrase", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 8, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 16 : 8), Text( "Enter your $_seedWordCount-word recovery phrase.", - style: isDesktop - ? STextStyles.desktopSubtitleH2(context) - : STextStyles.subtitle(context), - ), - SizedBox( - height: isDesktop ? 16 : 10, + style: + isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), ), + SizedBox(height: isDesktop ? 16 : 10), if (isDesktop) Row( mainAxisAlignment: MainAxisAlignment.center, @@ -803,19 +797,18 @@ class _RestoreWalletViewState extends ConsumerState { Assets.svg.clipboard, width: 22, height: 22, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - const SizedBox( - width: 8, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), + const SizedBox(width: 8), Text( "Paste", - style: STextStyles - .desktopButtonSmallSecondaryEnabled( - context, - ), + style: + STextStyles.desktopButtonSmallSecondaryEnabled( + context, + ), ), ], ), @@ -823,15 +816,10 @@ class _RestoreWalletViewState extends ConsumerState { ), ], ), - if (isDesktop) - const SizedBox( - height: 20, - ), + if (isDesktop) const SizedBox(height: 20), if (isDesktop) ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 1008, - ), + constraints: const BoxConstraints(maxWidth: 1008), child: Builder( builder: (BuildContext context) { const cols = 4; @@ -868,24 +856,23 @@ class _RestoreWalletViewState extends ConsumerState { ), decoration: _getInputDecorationFor( - _inputStatuses[ - i * 4 + j - 1], - "${i * 4 + j}", - ), + _inputStatuses[i * 4 + + j - + 1], + "${i * 4 + j}", + ), autovalidateMode: AutovalidateMode .onUserInteraction, - selectionControls: i * 4 + - j - - 1 == - 1 - ? textSelectionControls - : null, + selectionControls: + i * 4 + j - 1 == 1 + ? textSelectionControls + : null, // focusNode: // _focusNodes[i * 4 + j - 1], onChanged: (value) { final FormInputStatus - formInputStatus; + formInputStatus; if (value.isEmpty) { formInputStatus = @@ -917,25 +904,31 @@ class _RestoreWalletViewState extends ConsumerState { // } setState(() { _inputStatuses[i * 4 + - j - - 1] = formInputStatus; + j - + 1] = + formInputStatus; }); }, - controller: _controllers[ - i * 4 + j - 1], - style: - STextStyles.field(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textRestore, + controller: + _controllers[i * 4 + + j - + 1], + style: STextStyles.field( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textRestore, fontSize: isDesktop ? 16 : 14, ), ), - if (_inputStatuses[ - i * 4 + j - 1] == + if (_inputStatuses[i * 4 + + j - + 1] == FormInputStatus.invalid) Align( alignment: @@ -943,23 +936,22 @@ class _RestoreWalletViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), + left: 12.0, + bottom: 4.0, + ), child: Text( "Please check spelling", textAlign: TextAlign.left, - style: - STextStyles.label( + style: STextStyles.label( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textError, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textError, ), ), ), @@ -974,19 +966,23 @@ class _RestoreWalletViewState extends ConsumerState { TableViewRow( spacing: 16, cells: [ - for (int i = rows * cols; - i < _seedWordCount - remainder; - i++) ...[ + for ( + int i = rows * cols; + i < _seedWordCount - remainder; + i++ + ) ...[ const TableViewCell( flex: 1, child: Column( - // ... (existing code for input field) - ), + // ... (existing code for input field) + ), ), ], - for (int i = _seedWordCount - remainder; - i < _seedWordCount; - i++) ...[ + for ( + int i = _seedWordCount - remainder; + i < _seedWordCount; + i++ + ) ...[ TableViewCell( flex: 1, child: Column( @@ -1002,18 +998,19 @@ class _RestoreWalletViewState extends ConsumerState { ), decoration: _getInputDecorationFor( - _inputStatuses[i], - "${i + 1}", - ), + _inputStatuses[i], + "${i + 1}", + ), autovalidateMode: AutovalidateMode .onUserInteraction, - selectionControls: i == 1 - ? textSelectionControls - : null, + selectionControls: + i == 1 + ? textSelectionControls + : null, onChanged: (value) { final FormInputStatus - formInputStatus; + formInputStatus; if (value.isEmpty) { formInputStatus = @@ -1037,13 +1034,15 @@ class _RestoreWalletViewState extends ConsumerState { }); }, controller: _controllers[i], - style: - STextStyles.field(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .overlay, + style: STextStyles.field( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .overlay, fontSize: isDesktop ? 16 : 14, ), @@ -1056,23 +1055,22 @@ class _RestoreWalletViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), + left: 12.0, + bottom: 4.0, + ), child: Text( "Please check spelling", textAlign: TextAlign.left, - style: - STextStyles.label( + style: STextStyles.label( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textError, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textError, ), ), ), @@ -1081,9 +1079,11 @@ class _RestoreWalletViewState extends ConsumerState { ), ), ], - for (int i = 0; - i < cols - remainder; - i++) ...[ + for ( + int i = 0; + i < cols - remainder; + i++ + ) ...[ TableViewCell( flex: 1, child: Container(), @@ -1095,9 +1095,7 @@ class _RestoreWalletViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), PrimaryButton( label: "Restore wallet", width: 480, @@ -1124,8 +1122,9 @@ class _RestoreWalletViewState extends ConsumerState { Column( children: [ Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: 4, + ), child: TextFormField( obscureText: _hideSeedWords, autocorrect: !isDesktop, @@ -1169,9 +1168,10 @@ class _RestoreWalletViewState extends ConsumerState { }, controller: _controllers[i - 1], style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textRestore, + color: + Theme.of(context) + .extension()! + .textRestore, fontSize: isDesktop ? 16 : 14, ), ), @@ -1188,11 +1188,13 @@ class _RestoreWalletViewState extends ConsumerState { child: Text( "Please check spelling", textAlign: TextAlign.left, - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textError, ), ), ), @@ -1200,9 +1202,7 @@ class _RestoreWalletViewState extends ConsumerState { ], ), Padding( - padding: const EdgeInsets.only( - top: 8.0, - ), + padding: const EdgeInsets.only(top: 8.0), child: PrimaryButton( onPressed: requestRestore, label: "Restore", diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 04dc94d33..97c467b34 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -23,7 +23,6 @@ import '../../../models/keys/view_only_wallet_data.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; @@ -36,7 +35,6 @@ import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; -import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; @@ -117,12 +115,10 @@ class _VerifyRecoveryPhraseViewState final ViewOnlyWalletType viewOnlyWalletType; if (widget.wallet is ExtendedKeysInterface) { if (widget.wallet.cryptoCurrency is Firo) { - otherDataJson.addAll( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - WalletInfoKeys.enableLelantusScanning: false, - }, - ); + otherDataJson.addAll({ + WalletInfoKeys.lelantusCoinIsarRescanRequired: false, + WalletInfoKeys.enableLelantusScanning: false, + }); } viewOnlyWalletType = ViewOnlyWalletType.xPub; } else if (widget.wallet is LibMoneroWallet) { @@ -184,8 +180,9 @@ class _VerifyRecoveryPhraseViewState } else if (widget.wallet is LibMoneroWallet) { final w = widget.wallet as LibMoneroWallet; - final info = await w - .hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing(); + final info = + await w + .hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing(); final address = info.$1; final privateViewKey = info.$2; @@ -241,23 +238,27 @@ class _VerifyRecoveryPhraseViewState isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await voWallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(voWallet); await voWallet.exit(); - await ref.read(pWallets).deleteWallet( - widget.wallet.info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(widget.wallet.info, ref.read(secureStoreProvider)); } catch (e) { - await ref.read(pWallets).deleteWallet( - widget.wallet.info, - ref.read(secureStoreProvider), - ); - await ref.read(pWallets).deleteWallet( - voWallet.info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(widget.wallet.info, ref.read(secureStoreProvider)); + await ref + .read(pWallets) + .deleteWallet(voWallet.info, ref.read(secureStoreProvider)); rethrow; } @@ -274,20 +275,27 @@ class _VerifyRecoveryPhraseViewState } } - await ref.read(pWalletInfo(widget.wallet.walletId)).setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); + await widget.wallet.info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + if (ref.read(pDuress)) { + await widget.wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } ref.read(pWallets).addWallet(widget.wallet); - final isCreateSpecialEthWallet = - ref.read(createSpecialEthWalletRoutingFlag); + final isCreateSpecialEthWallet = ref.read( + createSpecialEthWalletRoutingFlag, + ); if (isCreateSpecialEthWallet) { ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; ref - .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) - .state = - !ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = !ref .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) .state; } @@ -311,23 +319,22 @@ class _VerifyRecoveryPhraseViewState throw ex!; } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } if (mounted) { Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseView.routeName, - ), + ModalRoute.withName(NewWalletRecoveryPhraseView.routeName), ); } return; @@ -337,17 +344,13 @@ class _VerifyRecoveryPhraseViewState if (mounted) { if (isDesktop) { if (isCreateSpecialEthWallet) { - Navigator.of(context).popUntil( - ModalRoute.withName( - SelectWalletForTokenView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(SelectWalletForTokenView.routeName)); } else { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); if (_coin is Ethereum) { unawaited( Navigator.of(context).pushNamed( @@ -359,17 +362,14 @@ class _VerifyRecoveryPhraseViewState } } else { if (isCreateSpecialEthWallet) { - Navigator.of(context).popUntil( - ModalRoute.withName( - SelectWalletForTokenView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(SelectWalletForTokenView.routeName)); } else { unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), + Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false), ); if (_coin is Ethereum) { unawaited( @@ -460,10 +460,9 @@ class _VerifyRecoveryPhraseViewState } Future delete() async { - await ref.read(pWallets).deleteWallet( - widget.wallet.info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(widget.wallet.info, ref.read(secureStoreProvider)); } @override @@ -476,40 +475,41 @@ class _VerifyRecoveryPhraseViewState onWillPop: onWillPop, child: MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseView.routeName, - ), - ); - }, - ), - trailing: ExitToMyStackButton( - onPressed: () async { - await delete(); - if (context.mounted) { + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { Navigator.of(context).popUntil( - ModalRoute.withName(DesktopHomeView.routeName), + ModalRoute.withName( + NewWalletRecoveryPhraseView.routeName, + ), ); - } - }, - ), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseView.routeName, - ), - ); - }, + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await delete(); + if (context.mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopHomeView.routeName), + ); + } + }, + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).popUntil( + ModalRoute.withName( + NewWalletRecoveryPhraseView.routeName, + ), + ); + }, + ), ), - ), body: SizedBox( width: isDesktop ? 410 : null, child: Padding( @@ -518,40 +518,32 @@ class _VerifyRecoveryPhraseViewState child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (isDesktop) - const Spacer( - flex: 10, - ), - SizedBox( - height: isDesktop ? 24 : 4, - ), + if (isDesktop) const Spacer(flex: 10), + SizedBox(height: isDesktop ? 24 : 4), Text( "Verify recovery phrase", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - SizedBox( - height: isDesktop ? 16 : 4, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.label(context).copyWith(fontSize: 12), ), + SizedBox(height: isDesktop ? 16 : 4), Text( isDesktop ? "Select word number" : "Tap word number ", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopSubtitleH1(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 12, + style: + isDesktop + ? STextStyles.desktopSubtitleH1(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 16 : 12), Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -564,76 +556,79 @@ class _VerifyRecoveryPhraseViewState child: Text( "${correctIndex + 1}", textAlign: TextAlign.center, - style: STextStyles.subtitle600(context).copyWith( - fontSize: 32, - letterSpacing: 0.25, - ), + style: STextStyles.subtitle600( + context, + ).copyWith(fontSize: 32, letterSpacing: 0.25), ), ), ), - if (isDesktop) - const SizedBox( - height: 40, - ), + if (isDesktop) const SizedBox(height: 40), WordTable( words: randomize(_mnemonic, correctIndex, 9).item1, isDesktop: isDesktop, ), if (!isDesktop) const Spacer(), - if (isDesktop) - const SizedBox( - height: 40, - ), + if (isDesktop) const SizedBox(height: 40), Row( children: [ Expanded( child: Consumer( builder: (_, ref, __) { - final selectedWord = ref - .watch( - verifyMnemonicSelectedWordStateProvider.state, - ) - .state; - final correctWord = ref - .watch( - verifyMnemonicCorrectWordStateProvider.state, - ) - .state; + final selectedWord = + ref + .watch( + verifyMnemonicSelectedWordStateProvider + .state, + ) + .state; + final correctWord = + ref + .watch( + verifyMnemonicCorrectWordStateProvider + .state, + ) + .state; return ConstrainedBox( constraints: BoxConstraints( minHeight: isDesktop ? 70 : 0, ), child: TextButton( - onPressed: selectedWord.isNotEmpty - ? () async { - await _continue( - correctWord == selectedWord, - ); - } - : null, - style: selectedWord.isNotEmpty - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: isDesktop - ? Text( - "Verify", - style: selectedWord.isNotEmpty - ? STextStyles.desktopButtonEnabled( - context, - ) - : STextStyles.desktopButtonDisabled( - context, - ), - ) - : Text( - "Continue", - style: STextStyles.button(context), - ), + onPressed: + selectedWord.isNotEmpty + ? () async { + await _continue( + correctWord == selectedWord, + ); + } + : null, + style: + selectedWord.isNotEmpty + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( + context, + ), + child: + isDesktop + ? Text( + "Verify", + style: + selectedWord.isNotEmpty + ? STextStyles.desktopButtonEnabled( + context, + ) + : STextStyles.desktopButtonDisabled( + context, + ), + ) + : Text( + "Continue", + style: STextStyles.button(context), + ), ), ); }, @@ -641,10 +636,7 @@ class _VerifyRecoveryPhraseViewState ), ], ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index 586e6c9fd..df1b4e557 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -26,6 +26,7 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_pin_put/custom_pin_put.dart'; import '../home_view/home_view.dart'; +import 'lock_screen_view.dart'; class CreatePinView extends ConsumerStatefulWidget { const CreatePinView({ @@ -55,8 +56,10 @@ class _CreatePinViewState extends ConsumerState { ); } - final PageController _pageController = - PageController(initialPage: 0, keepPage: true); + final PageController _pageController = PageController( + initialPage: 0, + keepPage: true, + ); // Attributes for Page 1 of the pageview final TextEditingController _pinPutController1 = TextEditingController(); @@ -118,27 +121,18 @@ class _CreatePinViewState extends ConsumerState { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - "Create a PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), + Text("Create a PIN", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 8), Text( "This PIN protects access to your wallet.", style: STextStyles.subtitle(context), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), + textStyle: STextStyles.label(context).copyWith(fontSize: 1), focusNode: _pinPutFocusNode1, controller: _pinPutController1, useNativeKeyboard: false, @@ -150,9 +144,10 @@ class _CreatePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), isRandom: @@ -188,28 +183,22 @@ class _CreatePinViewState extends ConsumerState { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - "Confirm PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), + Text("Confirm PIN", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 8), Text( "This PIN protects access to your wallet.", style: STextStyles.subtitle(context), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + color: + Theme.of( + context, + ).extension()!.textSubtitle3, fontSize: 1, ), focusNode: _pinPutFocusNode2, @@ -223,20 +212,23 @@ class _CreatePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, border: Border.all( width: 1, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), ), selectedFieldDecoration: _pinPutDecoration, @@ -249,14 +241,15 @@ class _CreatePinViewState extends ConsumerState { if (_pinPutController1.text == _pinPutController2.text) { // ask if want to use biometrics - final bool useBiometrics = (Platform.isLinux) - ? false - : await biometrics.authenticate( - cancelButtonText: "SKIP", - localizedReason: - "You can use your fingerprint to unlock the wallet and confirm transactions.", - title: "Enable fingerprint authentication", - ); + final bool useBiometrics = + (Platform.isLinux) + ? false + : await biometrics.authenticate( + cancelButtonText: "SKIP", + localizedReason: + "You can use your fingerprint to unlock the wallet and confirm transactions.", + title: "Enable fingerprint authentication", + ); //TODO investigate why this crashes IOS, maybe ios persists securestorage even after an uninstall? // This should never fail as we are writing a new pin @@ -270,7 +263,7 @@ class _CreatePinViewState extends ConsumerState { ref.read(prefsChangeNotifierProvider).hasPin == false, ); - await _secureStore.write(key: "stack_pin", value: pin); + await _secureStore.write(key: kPinKey, value: pin); ref.read(prefsChangeNotifierProvider).useBiometrics = useBiometrics; @@ -280,11 +273,13 @@ class _CreatePinViewState extends ConsumerState { const Duration(milliseconds: 200), ); - if (mounted) { + if (context.mounted) { if (!widget.popOnSuccess) { - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), ); } else { Navigator.of(context).pop(); @@ -292,17 +287,21 @@ class _CreatePinViewState extends ConsumerState { } } else { // _onSubmitFailCount++; - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, + unawaited( + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), ); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ), ); _pinPutController1.text = ''; diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index c7d6bb254..f3552cf82 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -15,11 +15,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; -// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; -import '../../providers/global/node_service_provider.dart'; -import '../../providers/global/prefs_provider.dart'; import '../../providers/global/secure_store_provider.dart'; -import '../../providers/global/wallets_provider.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import '../../utilities/assets.dart'; @@ -30,6 +27,7 @@ import '../../utilities/show_node_tor_settings_mismatch.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; @@ -38,6 +36,9 @@ import '../../widgets/shake/shake.dart'; import '../home_view/home_view.dart'; import '../wallet_view/wallet_view.dart'; +const kPinKey = "stack_pin"; +const kDuressPinKey = "stack_pin_duress"; + class LockscreenView extends ConsumerStatefulWidget { const LockscreenView({ super.key, @@ -82,6 +83,54 @@ class _LockscreenViewState extends ConsumerState { static const maxAttemptsBeforeThrottling = 3; Timer? _timer; + Future _loadWallets(String? loadIntoWalletId) async { + await ref + .read(pWallets) + .load( + ref.read(prefsChangeNotifierProvider), + ref.read(mainDBProvider), + ref.read(pDuress), + ); + + if (loadIntoWalletId != null) { + final Wallet wallet; + try { + wallet = ref.read(pWallets).getWallet(loadIntoWalletId); + } catch (_) { + // wallet isn't marked as duress, continue without loading into it + return true; + } + + final canContinue = + mounted && + await checkShowNodeTorSettingsMismatch( + context: context, + currency: wallet.cryptoCurrency, + prefs: ref.read(prefsChangeNotifierProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + allowCancel: false, + rootNavigator: Util.isDesktop, + ); + + if (!canContinue) { + return false; + } + + final Future loadFuture; + if (wallet is ExternalWallet) { + loadFuture = wallet.init().then( + (value) async => await (wallet as ExternalWallet).open(), + ); + } else { + loadFuture = wallet.init(); + } + + await loadFuture; + } + + return true; + } + Future _onUnlock() async { final now = DateTime.now().toUtc(); ref.read(prefsChangeNotifierProvider).lastUnlocked = @@ -97,41 +146,23 @@ class _LockscreenViewState extends ConsumerState { if (widget.popOnSuccess) { Navigator.of(context).pop(widget.routeOnSuccessArguments); } else { - final loadIntoWallet = widget.routeOnSuccess == HomeView.routeName && + final loadIntoWallet = + widget.routeOnSuccess == HomeView.routeName && widget.routeOnSuccessArguments is String; - if (loadIntoWallet) { - final walletId = widget.routeOnSuccessArguments as String; - - final wallet = ref.read(pWallets).getWallet(walletId); - - final canContinue = await checkShowNodeTorSettingsMismatch( + if (widget.isInitialAppLogin) { + final loadSuccess = await showLoading( + whileFuture: _loadWallets( + loadIntoWallet ? widget.routeOnSuccessArguments as String : null, + ), + opaqueBG: true, context: context, - currency: wallet.cryptoCurrency, - prefs: ref.read(prefsChangeNotifierProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - allowCancel: false, - rootNavigator: Util.isDesktop, + message: "Loading wallets...", ); - if (!canContinue) { + if (loadSuccess == null || loadSuccess == false) { return; } - - final Future loadFuture; - if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); - } else { - loadFuture = wallet.init(); - } - - await showLoading( - opaqueBG: true, - whileFuture: loadFuture, - context: context, - message: "Loading ${wallet.info.coin.prettyName} wallet...", - ); } if (mounted) { @@ -146,10 +177,9 @@ class _LockscreenViewState extends ConsumerState { final walletId = widget.routeOnSuccessArguments as String; unawaited( - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: walletId, - ), + Navigator.of( + context, + ).pushNamed(WalletView.routeName, arguments: walletId), ); } } @@ -169,11 +199,27 @@ class _LockscreenViewState extends ConsumerState { final cancelButtonText = widget.biometricsCancelButtonString; if (useBiometrics) { - if (await biometrics.authenticate( - title: title, - localizedReason: localizedReason, - cancelButtonText: cancelButtonText, - )) { + final bool canTryUnlock; + if (widget.isInitialAppLogin) { + final hasDuressPin = + (await _secureStore.read(key: kDuressPinKey)) != null; + + ref.read(pDuress.notifier).state = hasDuressPin; + canTryUnlock = true; + } else { + if (ref.read(pDuress)) { + canTryUnlock = ref.read(prefsChangeNotifierProvider).biometricsDuress; + } else { + canTryUnlock = true; + } + } + + if (canTryUnlock && + (await biometrics.authenticate( + title: title, + localizedReason: localizedReason, + cancelButtonText: cancelButtonText, + ))) { // check if initial log in // if (widget.routeOnSuccess == "/mainview") { // await logIn(await walletsService.networkName, currentWalletName, @@ -239,6 +285,29 @@ class _LockscreenViewState extends ConsumerState { final _pinTextController = TextEditingController(); + Future _checkCanUnlock(String pin) async { + final bool canUnlock; + if (widget.isInitialAppLogin) { + final storedPin = await _secureStore.read(key: kPinKey); + final duressPin = await _secureStore.read(key: kDuressPinKey); + if (pin == storedPin || pin == duressPin) { + ref.read(pDuress.notifier).state = pin == duressPin; + canUnlock = true; + } else { + canUnlock = false; + } + } else { + if (ref.read(pDuress)) { + final duressPin = await _secureStore.read(key: kDuressPinKey); + canUnlock = pin == duressPin; + } else { + final storedPin = await _secureStore.read(key: kPinKey); + canUnlock = pin == storedPin; + } + } + return canUnlock; + } + final Mutex _autoPinCheckLock = Mutex(); void _onPinChangedAutologinCheck() async { if (mounted) { @@ -247,11 +316,8 @@ class _LockscreenViewState extends ConsumerState { try { if (_autoPin && _pinTextController.text.length >= 4) { - final storedPin = await _secureStore.read(key: 'stack_pin'); - if (_pinTextController.text == storedPin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + if (await _checkCanUnlock(_pinTextController.text)) { + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } } @@ -319,21 +385,15 @@ class _LockscreenViewState extends ConsumerState { ), ); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; return; } - final storedPin = await _secureStore.read(key: 'stack_pin'); - - if (storedPin == pin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + if (await _checkCanUnlock(pin)) { + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } else { unawaited(_shakeController.shake()); @@ -348,136 +408,128 @@ class _LockscreenViewState extends ConsumerState { ); } - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; } } Widget get _body => Background( - child: SafeArea( - child: Scaffold( - extendBodyBehindAppBar: true, - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: widget.showBackButton + child: SafeArea( + child: Scaffold( + extendBodyBehindAppBar: true, + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: + widget.showBackButton ? AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 70), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ) + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 70), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ) : Container(), - actions: [ - // check prefs and hide if user has biometrics toggle off? - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 16.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (ref - .read(prefsChangeNotifierProvider) - .useBiometrics == - true) - CustomTextButton( - text: "Use biometrics", - onTap: () async { - await _checkUseBiometrics(); - }, - ), - ], - ), - ), - ], - ), - ], - ), - body: Column( + actions: [ + // check prefs and hide if user has biometrics toggle off? + Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Shake( - animationDuration: const Duration(milliseconds: 700), - animationRange: 12, - controller: _shakeController, - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Enter PIN", - style: STextStyles.pageTitleH1(context), - ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: pinCount, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), - focusNode: _pinFocusNode, - controller: _pinTextController, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration, - isRandom: ref - .read(prefsChangeNotifierProvider) - .randomizePIN, - onSubmit: (pin) { - if (!_autoPinCheckLock.isLocked) { - _onSubmitPin(pin); - } + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (ref.read(prefsChangeNotifierProvider).useBiometrics == + true) + CustomTextButton( + text: "Use biometrics", + onTap: () async { + await _checkUseBiometrics(); }, ), - ], - ), + ], ), ), ], ), - ), + ], ), - ); + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Shake( + animationDuration: const Duration(milliseconds: 700), + animationRange: 12, + controller: _shakeController, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Enter PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox(height: 52), + CustomPinPut( + fieldsCount: pinCount, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label( + context, + ).copyWith(fontSize: 1), + focusNode: _pinFocusNode, + controller: _pinTextController, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: + Theme.of( + context, + ).extension()!.background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration, + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, + onSubmit: (pin) { + if (!_autoPinCheckLock.isLocked) { + _onSubmitPin(pin); + } + }, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); @override Widget build(BuildContext context) { return widget.showBackButton ? _body : WillPopScope( - onWillPop: () async { - return widget.showBackButton; - }, - child: _body, - ); + onWillPop: () async { + return widget.showBackButton; + }, + child: _body, + ); } } diff --git a/lib/pages/pinpad_views/pinpad_dialog.dart b/lib/pages/pinpad_views/pinpad_dialog.dart index 68dbc144c..f962632d0 100644 --- a/lib/pages/pinpad_views/pinpad_dialog.dart +++ b/lib/pages/pinpad_views/pinpad_dialog.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; +import '../../providers/global/duress_provider.dart'; import '../../providers/global/prefs_provider.dart'; import '../../providers/global/secure_store_provider.dart'; import '../../themes/stack_colors.dart'; @@ -15,6 +16,7 @@ import '../../utilities/text_styles.dart'; import '../../widgets/custom_pin_put/custom_pin_put.dart'; import '../../widgets/shake/shake.dart'; import '../../widgets/stack_dialog.dart'; +import 'lock_screen_view.dart'; class PinpadDialog extends ConsumerStatefulWidget { const PinpadDialog({ @@ -74,11 +76,14 @@ class _PinpadDialogState extends ConsumerState { try { if (_autoPin && _pinTextController.text.length >= 4) { - final storedPin = await _secureStore.read(key: 'stack_pin'); + final String? storedPin; + if (ref.read(pDuress)) { + storedPin = await _secureStore.read(key: kDuressPinKey); + } else { + storedPin = await _secureStore.read(key: kPinKey); + } if (_pinTextController.text == storedPin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } } @@ -181,22 +186,23 @@ class _PinpadDialogState extends ConsumerState { ), ); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; return; } - final storedPin = await _secureStore.read(key: 'stack_pin'); + final String? storedPin; + if (ref.read(pDuress)) { + storedPin = await _secureStore.read(key: kDuressPinKey); + } else { + storedPin = await _secureStore.read(key: kPinKey); + } if (mounted) { if (storedPin == pin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } else { unawaited(_shakeController.shake()); @@ -209,9 +215,7 @@ class _PinpadDialogState extends ConsumerState { ), ); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; } @@ -262,16 +266,12 @@ class _PinpadDialogState extends ConsumerState { style: STextStyles.pageTitleH1(context), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), + textStyle: STextStyles.label(context).copyWith(fontSize: 1), focusNode: _pinFocusNode, controller: _pinTextController, useNativeKeyboard: false, @@ -296,9 +296,7 @@ class _PinpadDialogState extends ConsumerState { } }, ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 57fa710ad..0afaecb7c 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -8,12 +8,14 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../notifications/show_flush_bar.dart'; -import '../../../../../providers/global/prefs_provider.dart'; import '../../../../../providers/global/secure_store_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/flutter_secure_storage_interface.dart'; @@ -21,12 +23,11 @@ import '../../../../../utilities/text_styles.dart'; import '../../../../../widgets/background.dart'; import '../../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../../widgets/custom_pin_put/custom_pin_put.dart'; +import '../../../../pinpad_views/lock_screen_view.dart'; import '../security_view.dart'; class ChangePinView extends ConsumerStatefulWidget { - const ChangePinView({ - super.key, - }); + const ChangePinView({super.key}); static const String routeName = "/changePin"; @@ -46,8 +47,10 @@ class _ChangePinViewState extends ConsumerState { ); } - final PageController _pageController = - PageController(initialPage: 0, keepPage: true); + final PageController _pageController = PageController( + initialPage: 0, + keepPage: true, + ); // Attributes for Page 1 of the page view final TextEditingController _pinPutController1 = TextEditingController(); @@ -110,16 +113,12 @@ class _ChangePinViewState extends ConsumerState { style: STextStyles.pageTitleH1(context), ), ), - const SizedBox( - height: 52, - ), + const SizedBox(height: 52), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), + textStyle: STextStyles.label(context).copyWith(fontSize: 1), focusNode: _pinPutFocusNode1, controller: _pinPutController1, useNativeKeyboard: false, @@ -131,9 +130,10 @@ class _ChangePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), isRandom: @@ -161,7 +161,6 @@ class _ChangePinViewState extends ConsumerState { ), // page 2 - Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -171,17 +170,16 @@ class _ChangePinViewState extends ConsumerState { style: STextStyles.pageTitleH1(context), ), ), - const SizedBox( - height: 52, - ), + const SizedBox(height: 52), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + color: + Theme.of( + context, + ).extension()!.textSubtitle3, fontSize: 1, ), focusNode: _pinPutFocusNode2, @@ -195,9 +193,10 @@ class _ChangePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), isRandom: @@ -207,40 +206,58 @@ class _ChangePinViewState extends ConsumerState { followingFieldDecoration: _pinPutDecoration, onSubmit: (String pin) async { if (_pinPutController1.text == _pinPutController2.text) { - // This should never fail as we are overwriting the existing pin - assert( - (await _secureStore.read(key: "stack_pin")) != null, - ); - await _secureStore.write(key: "stack_pin", value: pin); + final isDuress = ref.read(pDuress); - showFloatingFlushBar( - type: FlushBarType.success, - message: "New PIN is set up", - context: context, - iconAsset: Assets.svg.check, - ); + if (isDuress) { + await _secureStore.write( + key: kDuressPinKey, + value: pin, + ); + } else { + // This should never fail as we are overwriting the existing pin + assert( + (await _secureStore.read(key: kPinKey)) != null, + ); + await _secureStore.write(key: kPinKey, value: pin); + } - await Future.delayed( - const Duration(milliseconds: 1200), - ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: + "New${isDuress ? " duress" : ""} PIN is set up", + context: context, + iconAsset: Assets.svg.check, + ), + ); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(SecurityView.routeName), + await Future.delayed( + const Duration(milliseconds: 1200), ); + + if (context.mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(SecurityView.routeName), + ); + } } } else { - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, + unawaited( + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), ); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ), ); _pinPutController1.text = ''; diff --git a/lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart new file mode 100644 index 000000000..7db6dbd42 --- /dev/null +++ b/lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../../notifications/show_flush_bar.dart'; +import '../../../../../providers/global/prefs_provider.dart'; +import '../../../../../providers/global/secure_store_provider.dart'; +import '../../../../../themes/stack_colors.dart'; +import '../../../../../utilities/assets.dart'; +import '../../../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../../../utilities/text_styles.dart'; +import '../../../../../widgets/background.dart'; +import '../../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../../widgets/custom_pin_put/custom_pin_put.dart'; +import '../../../pinpad_views/lock_screen_view.dart'; +import 'security_view.dart'; + +class CreateDuressPinView extends ConsumerStatefulWidget { + const CreateDuressPinView({super.key}); + + static const String routeName = "/createDuressPinView"; + + @override + ConsumerState createState() => + _CreateDuressPinViewState(); +} + +class _CreateDuressPinViewState extends ConsumerState { + BoxDecoration get _pinPutDecoration { + return BoxDecoration( + color: Theme.of(context).extension()!.infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context).extension()!.infoItemIcons, + ), + borderRadius: BorderRadius.circular(6), + ); + } + + final PageController _pageController = PageController( + initialPage: 0, + keepPage: true, + ); + + // Attributes for Page 1 of the page view + final TextEditingController _pinPutController1 = TextEditingController(); + final FocusNode _pinPutFocusNode1 = FocusNode(); + + // Attributes for Page 2 of the page view + final TextEditingController _pinPutController2 = TextEditingController(); + final FocusNode _pinPutFocusNode2 = FocusNode(); + + late final SecureStorageInterface _secureStore; + + int pinCount = 1; + + @override + void initState() { + super.initState(); + _secureStore = ref.read(secureStoreProvider); + } + + @override + void dispose() { + _pageController.dispose(); + _pinPutController1.dispose(); + _pinPutController2.dispose(); + _pinPutFocusNode1.dispose(); + _pinPutFocusNode2.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 70)); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + body: SafeArea( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + // page 1 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Create duress PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox(height: 52), + CustomPinPut( + fieldsCount: pinCount, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith(fontSize: 1), + focusNode: _pinPutFocusNode1, + controller: _pinPutController1, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: + Theme.of( + context, + ).extension()!.background, + counterText: "", + ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, + submittedFieldDecoration: _pinPutDecoration, + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) { + if (pin.length < 4) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN not long enough!", + iconAsset: Assets.svg.alertCircle, + context: context, + ); + } else { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + } + }, + ), + ], + ), + + // page 2 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Confirm duress PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox(height: 52), + CustomPinPut( + fieldsCount: pinCount, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.infoSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle3, + fontSize: 1, + ), + focusNode: _pinPutFocusNode2, + controller: _pinPutController2, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: + Theme.of( + context, + ).extension()!.background, + counterText: "", + ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, + submittedFieldDecoration: _pinPutDecoration, + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + if (_pinPutController1.text == _pinPutController2.text) { + await _secureStore.write( + key: kDuressPinKey, + value: pin, + ); + ref.read(prefsChangeNotifierProvider).hasDuressPin = + true; + + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Duress PIN is set up", + context: context, + iconAsset: Assets.svg.check, + ), + ); + } + + await Future.delayed( + const Duration(milliseconds: 1200), + ); + + if (context.mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(SecurityView.routeName), + ); + } + } else { + unawaited( + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ), + ); + + _pinPutController1.text = ''; + _pinPutController2.text = ''; + } + }, + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index e80374320..7ccfd2650 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -11,25 +11,192 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../providers/global/duress_provider.dart'; import '../../../../providers/global/prefs_provider.dart'; +import '../../../../providers/global/secure_store_provider.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_white_container.dart'; +import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; import 'change_pin_view/change_pin_view.dart'; +import 'create_duress_pin_view.dart'; -class SecurityView extends StatelessWidget { - const SecurityView({ - super.key, - }); +class SecurityView extends ConsumerStatefulWidget { + const SecurityView({super.key}); static const String routeName = "/security"; + @override + ConsumerState createState() => _SecurityViewState(); +} + +class _SecurityViewState extends ConsumerState { + bool _lock = false; + + Future _duressToggled(bool newValue) async { + if (_lock) return; + try { + _lock = true; + + if (newValue) { + await _createDuressPin(); + } else { + await _deleteDuressPin(); + } + } finally { + _lock = false; + } + } + + Future _createDuressPin() async { + final result = await showDialog( + context: context, + builder: + (context) => StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable duress PIN", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox(height: 8), + Row( + children: [ + Flexible( + child: Text( + "When unlocking the app with a duress PIN, only wallets" + " marked as visible in duress mode will be loaded and" + " shown. Be aware that providing a duress PIN instead" + " of your real PIN to law enforcement, border agents," + " or other authorities may be considered deception and" + " could carry legal consequences depending on your" + " jurisdiction. Use with care and according to your" + " threat model.", + style: STextStyles.smallMed14(context), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () => Navigator.of(context).pop(false), + ), + ), + const SizedBox(width: 8), + Expanded( + child: PrimaryButton( + label: "Ok", + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ], + ), + ], + ), + ), + ); + + if (result == true && mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: CreateDuressPinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to create duress PIN", + biometricsAuthenticationTitle: "Create duress PIN", + ), + settings: const RouteSettings(name: "/createDuressPinLockscreen"), + ), + ); + } + } + + Future _deleteDuressPin() async { + await showDialog( + context: context, + builder: + (context) => StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Disable duress PIN", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox(height: 8), + Row( + children: [ + Flexible( + child: Text( + "Your duress pin will be deleted. " + "You will be asked to create a PIN when you enable this again. " + "Are you sure you want to continue?", + + style: STextStyles.smallMed14(context), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 8), + Expanded( + child: PrimaryButton( + label: "Ok", + onPressed: () async { + try { + await ref + .read(secureStoreProvider) + .delete(key: kDuressPinKey); + } catch (e, s) { + Logging.instance.f( + "dpin delete failed!!", + error: e, + stackTrace: s, + ); + } + + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ), + ], + ), + ), + ); + + ref.read(prefsChangeNotifierProvider).hasDuressPin = false; + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -43,10 +210,7 @@ class SecurityView extends StatelessWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Security", - style: STextStyles.navBarTitle(context), - ), + title: Text("Security", style: STextStyles.navBarTitle(context)), ), body: Padding( padding: const EdgeInsets.all(16), @@ -69,16 +233,18 @@ class SecurityView extends StatelessWidget { RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: ChangePinView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to change PIN", - biometricsAuthenticationTitle: "Change PIN", + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: ChangePinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change PIN", + biometricsAuthenticationTitle: "Change PIN", + ), + settings: const RouteSettings( + name: "/changepinlockscreen", ), - settings: - const RouteSettings(name: "/changepinlockscreen"), ), ); }, @@ -99,9 +265,7 @@ class SecurityView extends StatelessWidget { ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Consumer( builder: (_, ref, __) { @@ -140,8 +304,9 @@ class SecurityView extends StatelessWidget { width: 40, child: DraggableSwitchButton( isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.useBiometrics), + prefsChangeNotifierProvider.select( + (value) => value.useBiometrics, + ), ), onValueChanged: (newValue) { ref @@ -157,9 +322,7 @@ class SecurityView extends StatelessWidget { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Consumer( builder: (_, ref, __) { @@ -187,8 +350,9 @@ class SecurityView extends StatelessWidget { width: 40, child: DraggableSwitchButton( isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.randomizePIN), + prefsChangeNotifierProvider.select( + (value) => value.randomizePIN, + ), ), onValueChanged: (newValue) { ref @@ -205,9 +369,7 @@ class SecurityView extends StatelessWidget { ), ), // The "autoPin" preference (whether to automatically accept a correct PIN). - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Consumer( builder: (_, ref, __) { @@ -235,8 +397,9 @@ class SecurityView extends StatelessWidget { width: 40, child: DraggableSwitchButton( isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.autoPin), + prefsChangeNotifierProvider.select( + (value) => value.autoPin, + ), ), onValueChanged: (newValue) { ref @@ -252,6 +415,107 @@ class SecurityView extends StatelessWidget { }, ), ), + if (!ref.watch(pDuress)) const SizedBox(height: 8), + if (!ref.watch(pDuress)) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Duress PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitch( + value: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hasDuressPin, + ), + ), + onChanged: _duressToggled, + ), + ), + ], + ), + ), + ); + }, + ), + ), + if (!ref.watch(pDuress) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hasDuressPin, + ), + )) + const SizedBox(height: 8), + if (!ref.watch(pDuress) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hasDuressPin, + ), + )) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Duress uses biometrics", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitch( + value: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.biometricsDuress, + ), + ), + onChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .biometricsDuress = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), ], ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 18b18d9e1..68436c634 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -115,27 +115,26 @@ class _WalletSettingsViewState extends ConsumerState { eventBus = widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == walletId) { - switch (event.newStatus) { - case WalletSyncStatus.unableToSync: - // TODO: Handle this case. - break; - case WalletSyncStatus.synced: - // TODO: Handle this case. - break; - case WalletSyncStatus.syncing: - // TODO: Handle this case. - break; + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == walletId) { + switch (event.newStatus) { + case WalletSyncStatus.unableToSync: + // TODO: Handle this case. + break; + case WalletSyncStatus.synced: + // TODO: Handle this case. + break; + case WalletSyncStatus.syncing: + // TODO: Handle this case. + break; + } + setState(() { + _currentSyncStatus = event.newStatus; + }); } - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + }); // _nodeStatusSubscription = // eventBus.on().listen( @@ -189,19 +188,12 @@ class _WalletSettingsViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Settings", - style: STextStyles.navBarTitle(context), - ), + title: Text("Settings", style: STextStyles.navBarTitle(context)), ), body: LayoutBuilder( builder: (builderContext, constraints) { return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), child: SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( @@ -229,9 +221,7 @@ class _WalletSettingsViewState extends ConsumerState { }, ), if (coin is FrostCurrency) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (coin is FrostCurrency) SettingsListButton( iconAssetName: Assets.svg.addressBook2, @@ -244,9 +234,7 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SettingsListButton( iconAssetName: Assets.svg.node, iconSize: 16, @@ -262,10 +250,7 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), - if (canBackup) - const SizedBox( - height: 8, - ), + if (canBackup) const SizedBox(height: 8), if (canBackup) Consumer( builder: (_, ref, __) { @@ -281,11 +266,10 @@ class _WalletSettingsViewState extends ConsumerState { String myName, String config, String keys, - ({ - String config, - String keys - })? prevGen, - })? frostWalletData; + ({String config, String keys})? + prevGen, + })? + frostWalletData; if (wallet is BitcoinFrostWallet) { final futures = [ wallet.getSerializedKeys(), @@ -294,21 +278,23 @@ class _WalletSettingsViewState extends ConsumerState { wallet.getMultisigConfigPrevGen(), ]; - final results = - await Future.wait(futures); + final results = await Future.wait( + futures, + ); if (results.length == 4) { frostWalletData = ( myName: wallet.frostInfo.myName, config: results[1]!, keys: results[0]!, - prevGen: results[2] == null || - results[3] == null - ? null - : ( - config: results[3]!, - keys: results[2]!, - ), + prevGen: + results[2] == null || + results[3] == null + ? null + : ( + config: results[3]!, + keys: results[2]!, + ), ); } } else { @@ -319,8 +305,9 @@ class _WalletSettingsViewState extends ConsumerState { .isViewOnly) { // TODO: is something needed here? } else { - mnemonic = await wallet - .getMnemonicAsWords(); + mnemonic = + await wallet + .getMnemonicAsWords(); } } } @@ -329,8 +316,9 @@ class _WalletSettingsViewState extends ConsumerState { if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { - keyData = await wallet - .getViewOnlyWalletData(); + keyData = + await wallet + .getViewOnlyWalletData(); } else if (wallet is ExtendedKeysInterface) { keyData = await wallet.getXPrivs(); @@ -350,23 +338,25 @@ class _WalletSettingsViewState extends ConsumerState { shouldUseMaterialRoute: RouteGenerator .useMaterialPageRoute, - builder: (_) => - LockscreenView( - routeOnSuccessArguments: ( - walletId: walletId, - keyData: keyData, - ), - showBackButton: true, - routeOnSuccess: - MobileKeyDataView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery data", - biometricsAuthenticationTitle: - "View recovery data", - ), + builder: + (_) => LockscreenView( + routeOnSuccessArguments: + ( + walletId: + walletId, + keyData: keyData, + ), + showBackButton: true, + routeOnSuccess: + MobileKeyDataView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery data", + biometricsAuthenticationTitle: + "View recovery data", + ), settings: const RouteSettings( name: "/viewRecoveryDataLockscreen", @@ -380,26 +370,27 @@ class _WalletSettingsViewState extends ConsumerState { shouldUseMaterialRoute: RouteGenerator .useMaterialPageRoute, - builder: (_) => - LockscreenView( - routeOnSuccessArguments: ( - walletId: walletId, - mnemonic: mnemonic ?? [], - frostWalletData: - frostWalletData, - keyData: keyData, - ), - showBackButton: true, - routeOnSuccess: - WalletBackupView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", - ), + builder: + (_) => LockscreenView( + routeOnSuccessArguments: ( + walletId: walletId, + mnemonic: + mnemonic ?? [], + frostWalletData: + frostWalletData, + keyData: keyData, + ), + showBackButton: true, + routeOnSuccess: + WalletBackupView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), settings: const RouteSettings( name: "/viewRecoverPhraseLockscreen", @@ -412,9 +403,7 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SettingsListButton( iconAssetName: Assets.svg.downloadFolder, title: "Wallet settings", @@ -427,9 +416,7 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SettingsListButton( iconAssetName: Assets.svg.arrowRotate, title: "Syncing preferences", @@ -439,10 +426,7 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), - if (xPubEnabled) - const SizedBox( - height: 8, - ), + if (xPubEnabled) const SizedBox(height: 8), if (xPubEnabled) Consumer( builder: (_, ref, __) { @@ -454,22 +438,24 @@ class _WalletSettingsViewState extends ConsumerState { delay: const Duration( milliseconds: 800, ), - whileFuture: (ref - .read(pWallets) - .getWallet(walletId) - as ExtendedKeysInterface) - .getXPubs(), + whileFuture: + (ref + .read(pWallets) + .getWallet(walletId) + as ExtendedKeysInterface) + .getXPubs(), context: context, message: "Loading xpubs", rootNavigator: Util.isDesktop, ); if (context.mounted) { - await Navigator.of(context) - .pushNamed( + await Navigator.of( + context, + ).pushNamed( XPubView.routeName, arguments: ( widget.walletId, - xpubData + xpubData, ), ); } @@ -477,10 +463,7 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), - if (coin is Firo) - const SizedBox( - height: 8, - ), + if (coin is Firo) const SizedBox(height: 8), if (coin is Firo) Consumer( builder: (_, ref, __) { @@ -493,43 +476,43 @@ class _WalletSettingsViewState extends ConsumerState { useSafeArea: false, barrierDismissible: true, context: context, - builder: (_) => StackOkDialog( - title: - "Are you sure you want to clear " - "${coin.prettyName} electrumx cache?", - onOkPressed: (value) { - result = value; - }, - leftButton: SecondaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), + builder: + (_) => StackOkDialog( + title: + "Are you sure you want to clear " + "${coin.prettyName} electrumx cache?", + onOkPressed: (value) { + result = value; + }, + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of( + context, + ).pop(); + }, + ), + ), ); if (result == "OK" && context.mounted) { await showLoading( - whileFuture: Future.wait( - [ - Future.delayed( - const Duration( - milliseconds: 1500, - ), + whileFuture: Future.wait([ + Future.delayed( + const Duration( + milliseconds: 1500, ), - DB.instance - .clearSharedTransactionCache( - currency: coin, - ), - if (coin is Firo) - FiroCacheCoordinator - .clearSharedCache( - coin.network, + ), + DB.instance + .clearSharedTransactionCache( + currency: coin, ), - ], - ), + if (coin is Firo) + FiroCacheCoordinator.clearSharedCache( + coin.network, + ), + ]), context: context, message: "Clearing cache...", ); @@ -539,9 +522,7 @@ class _WalletSettingsViewState extends ConsumerState { }, ), if (coin is NanoCurrency) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (coin is NanoCurrency) Consumer( builder: (_, ref, __) { @@ -571,9 +552,7 @@ class _WalletSettingsViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), const Spacer(), Consumer( builder: (_, ref, __) { @@ -598,9 +577,10 @@ class _WalletSettingsViewState extends ConsumerState { child: Text( "Log out", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ); @@ -621,10 +601,7 @@ class _WalletSettingsViewState extends ConsumerState { } class EpicBoxInfoForm extends ConsumerStatefulWidget { - const EpicBoxInfoForm({ - super.key, - required this.walletId, - }); + const EpicBoxInfoForm({super.key, required this.walletId}); final String walletId; @@ -668,9 +645,7 @@ class _EpiBoxInfoFormState extends ConsumerState { controller: hostController, decoration: const InputDecoration(hintText: "Host"), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -679,9 +654,7 @@ class _EpiBoxInfoFormState extends ConsumerState { keyboardType: Util.isDesktop ? null : const TextInputType.numberWithOptions(), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), TextButton( onPressed: () async { try { diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index c689baf30..3917f743e 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -12,11 +12,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/isar/models/wallet_info.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; @@ -40,10 +40,7 @@ import 'rename_wallet_view.dart'; import 'spark_info.dart'; class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget { - const WalletSettingsWalletSettingsView({ - super.key, - required this.walletId, - }); + const WalletSettingsWalletSettingsView({super.key, required this.walletId}); static const String routeName = "/walletSettingsWalletSettings"; @@ -58,6 +55,34 @@ class _WalletSettingsWalletSettingsViewState extends ConsumerState { late final DSBController _switchController; + bool _switchDuressToggleLock = false; // Mutex. + Future _switchDuressToggled() async { + if (_switchDuressToggleLock) { + return; + } + _switchDuressToggleLock = true; // Lock mutex. + + try { + final visibility = ref.read(pWalletInfo(widget.walletId)).isDuressVisible; + + await ref + .read(pWalletInfo(widget.walletId)) + .updateDuressVisibilityStatus( + isDuressVisible: !visibility, + isar: ref.read(mainDBProvider).isar, + ); + } catch (e, s) { + Logging.instance.f( + "Failed to update duress visibility for wallet", + error: e, + stackTrace: s, + ); + } finally { + // ensure _switchDuressToggleLock is set to false no matter what. + _switchDuressToggleLock = false; + } + } + bool _switchReuseAddressToggledLock = false; // Mutex. Future _switchReuseAddressToggled() async { if (_switchReuseAddressToggledLock) { @@ -90,10 +115,7 @@ class _WalletSettingsWalletSettingsViewState style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), + child: Text("Continue", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(true); }, @@ -115,12 +137,12 @@ class _WalletSettingsWalletSettingsViewState } Future _updateAddressReuse(bool shouldReuse) async { - await ref.read(pWalletInfo(widget.walletId)).updateOtherData( - newEntries: { - WalletInfoKeys.reuseAddress: shouldReuse, - }, - isar: ref.read(mainDBProvider).isar, - ); + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.reuseAddress: shouldReuse}, + isar: ref.read(mainDBProvider).isar, + ); if (_switchController.isOn != null) { if (_switchController.isOn!.call() != shouldReuse) { @@ -139,7 +161,8 @@ class _WalletSettingsWalletSettingsViewState Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); - final isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface && + final isViewOnlyNoAddressGen = + wallet is ViewOnlyOptionInterface && wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; @@ -158,11 +181,7 @@ class _WalletSettingsWalletSettingsViewState ), ), body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -199,10 +218,7 @@ class _WalletSettingsWalletSettingsViewState ), ), ), - if (wallet is RbfInterface) - const SizedBox( - height: 8, - ), + if (wallet is RbfInterface) const SizedBox(height: 8), if (wallet is RbfInterface) RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -236,9 +252,7 @@ class _WalletSettingsWalletSettingsViewState ), ), if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen) RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -269,11 +283,13 @@ class _WalletSettingsWalletSettingsViewState width: 40, child: IgnorePointer( child: DraggableSwitchButton( - isOn: ref.watch( - pWalletInfo(widget.walletId).select( - (value) => value.otherData, - ), - )[WalletInfoKeys.reuseAddress] as bool? ?? + isOn: + ref.watch( + pWalletInfo(widget.walletId).select( + (value) => value.otherData, + ), + )[WalletInfoKeys.reuseAddress] + as bool? ?? false, controller: _switchController, ), @@ -284,10 +300,56 @@ class _WalletSettingsWalletSettingsViewState ), ), ), - if (wallet is LelantusInterface && !wallet.isViewOnly) - const SizedBox( - height: 8, + if (!ref.watch(pDuress)) const SizedBox(height: 8), + if (!ref.watch(pDuress)) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: _switchDuressToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Show in duress", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitch( + value: + ref.watch( + pWalletInfo(widget.walletId).select( + (value) => value.otherData, + ), + )[WalletInfoKeys + .duressMarkedVisibleWalletKey] + as bool? ?? + false, + onChanged: (_) => (), + ), + ), + ), + ], + ), + ), + ), ), + if (wallet is LelantusInterface && !wallet.isViewOnly) + const SizedBox(height: 8), if (wallet is LelantusInterface && !wallet.isViewOnly) RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -321,9 +383,7 @@ class _WalletSettingsWalletSettingsViewState ), ), if (wallet is SparkInterface && !wallet.isViewOnly) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (wallet is SparkInterface && !wallet.isViewOnly) RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -356,10 +416,7 @@ class _WalletSettingsWalletSettingsViewState ), ), ), - if (wallet is LibMoneroWallet) - const SizedBox( - height: 8, - ), + if (wallet is LibMoneroWallet) const SizedBox(height: 8), if (wallet is LibMoneroWallet) RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -392,9 +449,7 @@ class _WalletSettingsWalletSettingsViewState ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: RawMaterialButton( @@ -410,59 +465,65 @@ class _WalletSettingsWalletSettingsViewState showDialog( barrierDismissible: true, context: context, - builder: (_) => StackDialog( - title: - "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + builder: + (_) => StackDialog( + title: + "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?", + leftButton: TextButton( + style: Theme.of(context) .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: widget.walletId, - showBackButton: true, - routeOnSuccess: - DeleteWalletWarningView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to delete wallet", - biometricsAuthenticationTitle: - "Delete wallet", - ), - settings: const RouteSettings( - name: "/deleteWalletLockscreen", + .getSecondaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), - ); - }, - child: Text( - "Delete", - style: STextStyles.button(context), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => LockscreenView( + routeOnSuccessArguments: + widget.walletId, + showBackButton: true, + routeOnSuccess: + DeleteWalletWarningView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to delete wallet", + biometricsAuthenticationTitle: + "Delete wallet", + ), + settings: const RouteSettings( + name: "/deleteWalletLockscreen", + ), + ), + ); + }, + child: Text( + "Delete", + style: STextStyles.button(context), + ), + ), ), - ), - ), ); }, child: Padding( diff --git a/lib/providers/global/duress_provider.dart b/lib/providers/global/duress_provider.dart new file mode 100644 index 000000000..f6e84011c --- /dev/null +++ b/lib/providers/global/duress_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final pDuress = StateProvider((ref) => true); + diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 0f2f12343..90e0ced3a 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -19,6 +19,7 @@ export './exchange/exchange_form_state_provider.dart'; export './exchange/exchange_send_from_wallet_id_provider.dart'; export './exchange/trade_note_service_provider.dart'; export './exchange/trade_sent_from_stack_lookup_provider.dart'; +export './global/duress_provider.dart'; export './global/locale_provider.dart'; export './global/node_service_provider.dart'; export './global/notifications_provider.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index fb0e79c86..65f2df473 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -114,6 +114,7 @@ import 'pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_ import 'pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart'; import 'pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart'; +import 'pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart'; import 'pages/settings_views/global_settings_view/security_views/security_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart'; @@ -975,6 +976,13 @@ class RouteGenerator { settings: RouteSettings(name: settings.name), ); + case CreateDuressPinView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const CreateDuressPinView(), + settings: RouteSettings(name: settings.name), + ); + case BaseCurrencySettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 07856e6ee..d47c7b66b 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -152,10 +152,10 @@ class Wallets { } } - Future load(Prefs prefs, MainDB mainDB) async { + Future load(Prefs prefs, MainDB mainDB, bool isDuress) async { // return await _loadV1(prefs, mainDB); // return await _loadV2(prefs, mainDB); - return await _loadV3(prefs, mainDB); + return await _loadV3(prefs, mainDB, isDuress); } Future _loadV1(Prefs prefs, MainDB mainDB) async { @@ -165,18 +165,21 @@ class Wallets { hasLoaded = true; // clear out any wallet hive boxes where the wallet was deleted in previous app run - for (final walletId in DB.instance - .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + for (final walletId in DB.instance.values( + boxName: DB.boxNameWalletsToDeleteOnStart, + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .deleteAll(), + () async => + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll(), ); } // clear list - await DB.instance - .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + await DB.instance.deleteAll( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); if (walletInfoList.isEmpty) { @@ -204,9 +207,10 @@ class Wallets { for (final walletInfo in walletInfoList) { try { final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); - Logging.instance - .d("LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " - "IS VERIFIED: $isVerified"); + Logging.instance.d( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + ); if (isVerified) { // TODO: integrate this into the new wallets somehow? @@ -222,7 +226,8 @@ class Wallets { prefs: prefs, ); - final shouldSetAutoSync = shouldAutoSyncAll || + final shouldSetAutoSync = + shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(walletInfo.walletId); if (wallet is LibMoneroWallet) { @@ -269,18 +274,21 @@ class Wallets { hasLoaded = true; // clear out any wallet hive boxes where the wallet was deleted in previous app run - for (final walletId in DB.instance - .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + for (final walletId in DB.instance.values( + boxName: DB.boxNameWalletsToDeleteOnStart, + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .deleteAll(), + () async => + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll(), ); } // clear list - await DB.instance - .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + await DB.instance.deleteAll( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); if (walletInfoList.isEmpty) { @@ -309,9 +317,10 @@ class Wallets { for (final walletInfo in walletInfoList) { try { final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); - Logging.instance - .d("LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " - "IS VERIFIED: $isVerified"); + Logging.instance.d( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + ); if (isVerified) { // TODO: integrate this into the new wallets somehow? @@ -345,11 +354,7 @@ class Wallets { deleteFutures.add(_deleteWallet(walletInfo.walletId)); } } catch (e, s) { - Logging.instance.w( - "$e $s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e $s", error: e, stackTrace: s); continue; } } @@ -357,17 +362,17 @@ class Wallets { final asyncWalletIds = await Future.wait(walletIDInitFutures); asyncWalletIds.removeWhere((e) => e == "dummy_ignore"); - final List> walletInitFutures = asyncWalletIds - .map( - (id) => _wallets[id]!.init().then( - (_) { - if (shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(id)) { - _wallets[id]!.shouldAutoSync = true; - } - }, - ), - ) - .toList(); + final List> walletInitFutures = + asyncWalletIds + .map( + (id) => _wallets[id]!.init().then((_) { + if (shouldAutoSyncAll || + walletIdsToEnableAutoSync.contains(id)) { + _wallets[id]!.shouldAutoSync = true; + } + }), + ) + .toList(); if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { unawaited( @@ -387,34 +392,43 @@ class Wallets { } /// should be best performance - Future _loadV3(Prefs prefs, MainDB mainDB) async { + Future _loadV3(Prefs prefs, MainDB mainDB, bool isDuress) async { if (hasLoaded) { return; } hasLoaded = true; // clear out any wallet hive boxes where the wallet was deleted in previous app run - for (final walletId in DB.instance - .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + for (final walletId in DB.instance.values( + boxName: DB.boxNameWalletsToDeleteOnStart, + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .deleteAll(), + () async => + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll(), ); } // clear list - await DB.instance - .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); - - final walletInfoList = await mainDB.isar.walletInfo - .where() - .filter() - .anyOf( - AppConfig.coins.map((e) => e.identifier), - (q, element) => q.coinNameMatches(element), - ) - .findAll(); + await DB.instance.deleteAll( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); + + final walletInfoList = + await mainDB.isar.walletInfo + .where() + .filter() + .anyOf( + AppConfig.coins.map((e) => e.identifier), + (q, element) => q.coinNameMatches(element), + ) + .findAll(); + + if (isDuress) { + walletInfoList.retainWhere((e) => e.isDuressVisible); + } + if (walletInfoList.isEmpty) { return; } @@ -441,9 +455,10 @@ class Wallets { for (final walletInfo in walletInfoList) { try { final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); - Logging.instance - .d("LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " - "IS VERIFIED: $isVerified"); + Logging.instance.d( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + ); if (isVerified) { // TODO: integrate this into the new wallets somehow? @@ -477,11 +492,7 @@ class Wallets { deleteFutures.add(_deleteWallet(walletInfo.walletId)); } } catch (e, s) { - Logging.instance.w( - "$e $s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e $s", error: e, stackTrace: s); continue; } } @@ -490,24 +501,21 @@ class Wallets { asyncWalletIds.removeWhere((e) => e == "dummy_ignore"); final List idsToRefresh = []; - final List> walletInitFutures = asyncWalletIds - .map( - (id) => _wallets[id]!.init().then( - (_) { - if (shouldSyncAllOnceOnStartup || - walletIdsToSyncOnceOnStartup.contains(id)) { - idsToRefresh.add(id); - } - }, - ), - ) - .toList(); + final List> walletInitFutures = + asyncWalletIds + .map( + (id) => _wallets[id]!.init().then((_) { + if (shouldSyncAllOnceOnStartup || + walletIdsToSyncOnceOnStartup.contains(id)) { + idsToRefresh.add(id); + } + }), + ) + .toList(); Future _refreshFutures(List idsToRefresh) async { final start = DateTime.now(); - Logging.instance.d( - "Initial refresh start: ${start.toUtc()}", - ); + Logging.instance.d("Initial refresh start: ${start.toUtc()}"); const groupCount = 3; for (int i = 0; i < idsToRefresh.length; i += groupCount) { final List> futures = []; @@ -515,14 +523,13 @@ class Wallets { if (i + j >= idsToRefresh.length) { break; } - futures.add( - _wallets[idsToRefresh[i + j]]!.refresh(), - ); + futures.add(_wallets[idsToRefresh[i + j]]!.refresh()); } await Future.wait(futures); } - Logging.instance - .d("Initial refresh duration: ${DateTime.now().difference(start)}"); + Logging.instance.d( + "Initial refresh duration: ${DateTime.now().difference(start)}", + ); } if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { @@ -530,15 +537,13 @@ class Wallets { Future.wait([ _initLinearly(walletsToInitLinearly), ...walletInitFutures, - ]).then( - (value) => _refreshFutures(idsToRefresh), - ), + ]).then((value) => _refreshFutures(idsToRefresh)), ); } else if (walletInitFutures.isNotEmpty) { unawaited( - Future.wait(walletInitFutures).then( - (value) => _refreshFutures(idsToRefresh), - ), + Future.wait( + walletInitFutures, + ).then((value) => _refreshFutures(idsToRefresh)), ); } else if (walletsToInitLinearly.isNotEmpty) { unawaited(_initLinearly(walletsToInitLinearly)); @@ -578,7 +583,8 @@ class Wallets { ); if (isVerified) { - final shouldSetAutoSync = shouldAutoSyncAll || + final shouldSetAutoSync = + shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(wallet.walletId); if (isDesktop) { diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 395e5ecd6..a436c8483 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -41,6 +41,8 @@ class Prefs extends ChangeNotifier { _randomizePIN = await _getRandomizePIN(); _useBiometrics = await _getUseBiometrics(); _hasPin = await _getHasPin(); + _hasDuressPin = await _getHasDuressPin(); + _biometricsDuress = await _getBiometricsDuress(); _language = await _getPreferredLanguage(); _showFavoriteWallets = await _getShowFavoriteWallets(); _wifiOnly = await _getUseWifiOnly(); @@ -101,9 +103,10 @@ class Prefs extends ChangeNotifier { Future _getLastUnlockedTimeout() async { return (DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastUnlockedTimeout", - )) as int? ?? + boxName: DB.boxNamePrefs, + key: "lastUnlockedTimeout", + )) + as int? ?? 60; } @@ -127,9 +130,10 @@ class Prefs extends ChangeNotifier { Future _getLastUnlocked() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastUnlocked", - ) as int? ?? + boxName: DB.boxNamePrefs, + key: "lastUnlocked", + ) + as int? ?? 0; } @@ -155,9 +159,10 @@ class Prefs extends ChangeNotifier { Future _getCurrentNotificationIndex() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "currentNotificationId", - ) as int? ?? + boxName: DB.boxNamePrefs, + key: "currentNotificationId", + ) + as int? ?? 0; } @@ -180,10 +185,12 @@ class Prefs extends ChangeNotifier { } Future> _getWalletIdsSyncOnStartup() async { - final list = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "walletIdsSyncOnStartup", - ) as List? ?? + final list = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "walletIdsSyncOnStartup", + ) + as List? ?? []; return List.from(list); } @@ -207,10 +214,12 @@ class Prefs extends ChangeNotifier { } Future _getSyncType() async { - final int index = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "syncTypeIndex", - ) as int? ?? + final int index = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "syncTypeIndex", + ) + as int? ?? SyncingType.allWalletsOnStartup.index; return SyncingType.values[index]; } @@ -234,8 +243,11 @@ class Prefs extends ChangeNotifier { } Future _getUseWifiOnly() async { - return await DB.instance - .get(boxName: DB.boxNamePrefs, key: "wifiOnly") as bool? ?? + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "wifiOnly", + ) + as bool? ?? false; } @@ -259,9 +271,10 @@ class Prefs extends ChangeNotifier { Future _getShowFavoriteWallets() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showFavoriteWallets", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "showFavoriteWallets", + ) + as bool? ?? true; } @@ -285,9 +298,10 @@ class Prefs extends ChangeNotifier { Future _getPreferredLanguage() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "language", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "language", + ) + as String? ?? Language.englishUS.description; } @@ -311,9 +325,10 @@ class Prefs extends ChangeNotifier { Future _getPreferredCurrency() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "currency", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "currency", + ) + as String? ?? "USD"; } @@ -378,9 +393,10 @@ class Prefs extends ChangeNotifier { Future _getRandomizePIN() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "randomizePIN", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "randomizePIN", + ) + as bool? ?? false; } @@ -404,9 +420,10 @@ class Prefs extends ChangeNotifier { Future _getUseBiometrics() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useBiometrics", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "useBiometrics", + ) + as bool? ?? false; } @@ -418,16 +435,76 @@ class Prefs extends ChangeNotifier { set hasPin(bool hasPin) { if (_hasPin != hasPin) { - DB.instance - .put(boxName: DB.boxNamePrefs, key: "hasPin", value: hasPin); + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "hasPin", + value: hasPin, + ); _hasPin = hasPin; notifyListeners(); } } Future _getHasPin() async { - return await DB.instance - .get(boxName: DB.boxNamePrefs, key: "hasPin") as bool? ?? + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "hasPin", + ) + as bool? ?? + false; + } + + // has set up pin + + bool _hasDuressPin = false; + + bool get hasDuressPin => _hasDuressPin; + + set hasDuressPin(bool hasDuressPin) { + if (_hasDuressPin != hasDuressPin) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "hasDuressPin", + value: hasDuressPin, + ); + _hasDuressPin = hasDuressPin; + notifyListeners(); + } + } + + Future _getHasDuressPin() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "hasDuressPin", + ) + as bool? ?? + false; + } + + // has toggled biometrics to act for duress instead of normal auth + + bool _biometricsDuress = false; + + bool get biometricsDuress => _biometricsDuress; + + set biometricsDuress(bool biometricsDuress) { + if (_biometricsDuress != biometricsDuress) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "biometricsDuress", + value: biometricsDuress, + ); + _biometricsDuress = biometricsDuress; + notifyListeners(); + } + } + + Future _getBiometricsDuress() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "biometricsDuress", + ) + as bool? ?? false; } @@ -451,9 +528,10 @@ class Prefs extends ChangeNotifier { Future _getHasFamiliarity() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "familiarity", - ) as int? ?? + boxName: DB.boxNamePrefs, + key: "familiarity", + ) + as int? ?? 0; } @@ -477,9 +555,10 @@ class Prefs extends ChangeNotifier { Future _getTorKillswitch() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "torKillswitch", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "torKillswitch", + ) + as bool? ?? true; } @@ -503,9 +582,10 @@ class Prefs extends ChangeNotifier { Future _getShowTestNetCoins() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showTestNetCoins", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "showTestNetCoins", + ) + as bool? ?? false; } @@ -519,22 +599,23 @@ class Prefs extends ChangeNotifier { if (_isAutoBackupEnabled != isAutoBackupEnabled) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "isAutoBackupEnabled", - value: isAutoBackupEnabled, - ) + boxName: DB.boxNamePrefs, + key: "isAutoBackupEnabled", + value: isAutoBackupEnabled, + ) .then((_) { - _isAutoBackupEnabled = isAutoBackupEnabled; - notifyListeners(); - }); + _isAutoBackupEnabled = isAutoBackupEnabled; + notifyListeners(); + }); } } Future _getIsAutoBackupEnabled() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "isAutoBackupEnabled", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "isAutoBackupEnabled", + ) + as bool? ?? false; } @@ -558,9 +639,10 @@ class Prefs extends ChangeNotifier { Future _getAutoBackupLocation() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoBackupLocation", - ) as String?; + boxName: DB.boxNamePrefs, + key: "autoBackupLocation", + ) + as String?; } // auto backup frequency type @@ -601,10 +683,12 @@ class Prefs extends ChangeNotifier { } Future _getBackupFrequencyType() async { - String? rate = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "backupFrequencyType", - ) as String?; + String? rate = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "backupFrequencyType", + ) + as String?; rate ??= "10Min"; switch (rate) { case "10Min": @@ -638,9 +722,10 @@ class Prefs extends ChangeNotifier { Future _getLastAutoBackup() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoBackupFileUri", - ) as DateTime?; + boxName: DB.boxNamePrefs, + key: "autoBackupFileUri", + ) + as DateTime?; } // auto backup @@ -653,22 +738,23 @@ class Prefs extends ChangeNotifier { if (_hideBlockExplorerWarning != hideBlockExplorerWarning) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "hideBlockExplorerWarning", - value: hideBlockExplorerWarning, - ) + boxName: DB.boxNamePrefs, + key: "hideBlockExplorerWarning", + value: hideBlockExplorerWarning, + ) .then((_) { - _hideBlockExplorerWarning = hideBlockExplorerWarning; - notifyListeners(); - }); + _hideBlockExplorerWarning = hideBlockExplorerWarning; + notifyListeners(); + }); } } Future _getHideBlockExplorerWarning() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "hideBlockExplorerWarning", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "hideBlockExplorerWarning", + ) + as bool? ?? false; } @@ -682,22 +768,23 @@ class Prefs extends ChangeNotifier { if (_gotoWalletOnStartup != gotoWalletOnStartup) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "gotoWalletOnStartup", - value: gotoWalletOnStartup, - ) + boxName: DB.boxNamePrefs, + key: "gotoWalletOnStartup", + value: gotoWalletOnStartup, + ) .then((_) { - _gotoWalletOnStartup = gotoWalletOnStartup; - notifyListeners(); - }); + _gotoWalletOnStartup = gotoWalletOnStartup; + notifyListeners(); + }); } } Future _getGotoWalletOnStartup() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "gotoWalletOnStartup", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "gotoWalletOnStartup", + ) + as bool? ?? false; } @@ -721,9 +808,10 @@ class Prefs extends ChangeNotifier { Future _getStartupWalletId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "startupWalletId", - ) as String?; + boxName: DB.boxNamePrefs, + key: "startupWalletId", + ) + as String?; } // incognito mode off by default @@ -736,28 +824,31 @@ class Prefs extends ChangeNotifier { if (_externalCalls != externalCalls) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "externalCalls", - value: externalCalls, - ) + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: externalCalls, + ) .then((_) { - _externalCalls = externalCalls; - notifyListeners(); - }); + _externalCalls = externalCalls; + notifyListeners(); + }); } } Future _getHasExternalCalls() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "externalCalls", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "externalCalls", + ) + as bool? ?? true; } Future isExternalCallsSet() async { - if (await DB.instance - .get(boxName: DB.boxNamePrefs, key: "externalCalls") == + if (await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "externalCalls", + ) == null) { return false; } @@ -768,8 +859,9 @@ class Prefs extends ChangeNotifier { String? get userID => _userId; Future _getUserId() async { - String? userID = await DB.instance - .get(boxName: DB.boxNamePrefs, key: "userID") as String?; + String? userID = + await DB.instance.get(boxName: DB.boxNamePrefs, key: "userID") + as String?; if (userID == null) { userID = const Uuid().v4(); await saveUserID(userID); @@ -779,8 +871,11 @@ class Prefs extends ChangeNotifier { Future saveUserID(String userId) async { _userId = userId; - await DB.instance - .put(boxName: DB.boxNamePrefs, key: "userID", value: _userId); + await DB.instance.put( + boxName: DB.boxNamePrefs, + key: "userID", + value: _userId, + ); // notifyListeners(); } @@ -788,10 +883,15 @@ class Prefs extends ChangeNotifier { int? get signupEpoch => _signupEpoch; Future _getSignupEpoch() async { - int? signupEpoch = await DB.instance - .get(boxName: DB.boxNamePrefs, key: "signupEpoch") as int?; + int? signupEpoch = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "signupEpoch", + ) + as int?; if (signupEpoch == null) { - signupEpoch = DateTime.now().millisecondsSinceEpoch ~/ + signupEpoch = + DateTime.now().millisecondsSinceEpoch ~/ Duration.millisecondsPerSecond; await saveSignupEpoch(signupEpoch); } @@ -828,9 +928,10 @@ class Prefs extends ChangeNotifier { Future _getEnableCoinControl() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "enableCoinControl", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "enableCoinControl", + ) + as bool? ?? false; } @@ -854,9 +955,10 @@ class Prefs extends ChangeNotifier { Future _getEnableSystemBrightness() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "enableSystemBrightness", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "enableSystemBrightness", + ) + as bool? ?? false; } @@ -880,9 +982,10 @@ class Prefs extends ChangeNotifier { Future _getThemeId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "themeId", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "themeId", + ) + as String? ?? "light"; } @@ -906,9 +1009,10 @@ class Prefs extends ChangeNotifier { Future _getSystemBrightnessLightThemeId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "systemBrightnessLightThemeId", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "systemBrightnessLightThemeId", + ) + as String? ?? "light"; } @@ -932,9 +1036,10 @@ class Prefs extends ChangeNotifier { Future _getSystemBrightnessDarkTheme() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "systemBrightnessDarkThemeId", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "systemBrightnessDarkThemeId", + ) + as String? ?? "dark"; } @@ -962,10 +1067,12 @@ class Prefs extends ChangeNotifier { Future _setAmountUnits() async { for (final coin in AppConfig.coins) { - final unitIndex = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "amountUnitFor${coin.identifier}", - ) as int? ?? + final unitIndex = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "amountUnitFor${coin.identifier}", + ) + as int? ?? 0; // 0 is "normal" _amountUnits[coin] = AmountUnit.values[unitIndex]; } @@ -995,10 +1102,12 @@ class Prefs extends ChangeNotifier { Future _setMaxDecimals() async { for (final coin in AppConfig.coins) { - final decimals = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "maxDecimalsFor${coin.identifier}", - ) as int? ?? + final decimals = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "maxDecimalsFor${coin.identifier}", + ) + as int? ?? (coin.fractionDigits > 18 ? 18 : coin.fractionDigits); // use some sane max rather than up to 30 that nano uses _amountDecimals[coin.identifier] = decimals; @@ -1031,9 +1140,10 @@ class Prefs extends ChangeNotifier { Future _getUseTor() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useTor", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "useTor", + ) + as bool? ?? false; } @@ -1054,10 +1164,7 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "fusionServerInfoMap", value: _fusionServerInfo.map( - (key, value) => MapEntry( - key, - value.toJsonString(), - ), + (key, value) => MapEntry(key, value.toJsonString()), ), ); notifyListeners(); @@ -1065,29 +1172,30 @@ class Prefs extends ChangeNotifier { } Future> _getFusionServerInfo() async { - final map = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "fusionServerInfoMap", - ) as Map?; + final map = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "fusionServerInfoMap", + ) + as Map?; if (map == null) { return _fusionServerInfo; } - final actualMap = Map.from(map).map( - (key, value) => MapEntry( - key, - FusionInfo.fromJsonString(value), - ), - ); + final actualMap = Map.from( + map, + ).map((key, value) => MapEntry(key, FusionInfo.fromJsonString(value))); // legacy bch check if (actualMap["bitcoincash"] == null || actualMap["bitcoincashTestnet"] == null) { - final saved = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "fusionServerInfo", - ) as String?; + final saved = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "fusionServerInfo", + ) + as String?; if (saved != null) { final bchInfo = FusionInfo.fromJsonString(saved); @@ -1098,10 +1206,7 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "fusionServerInfoMap", value: actualMap.map( - (key, value) => MapEntry( - key, - value.toJsonString(), - ), + (key, value) => MapEntry(key, value.toJsonString()), ), ), ); @@ -1131,9 +1236,10 @@ class Prefs extends ChangeNotifier { Future _getAutoPin() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoPin", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "autoPin", + ) + as bool? ?? false; } @@ -1157,9 +1263,10 @@ class Prefs extends ChangeNotifier { Future _getEnableExchange() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showExchange", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "showExchange", + ) + as bool? ?? true; } @@ -1180,9 +1287,10 @@ class Prefs extends ChangeNotifier { Future _getAdvancedFiroFeatures() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "advancedFiroFeatures", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "advancedFiroFeatures", + ) + as bool? ?? false; } @@ -1203,9 +1311,10 @@ class Prefs extends ChangeNotifier { Future _getLogsPath() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "logsPath", - ) as String?; + boxName: DB.boxNamePrefs, + key: "logsPath", + ) + as String?; } // log level pref @@ -1224,10 +1333,12 @@ class Prefs extends ChangeNotifier { } Future _getLogLevel() async { - final value = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "logLevel", - ) as int?; + final value = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "logLevel", + ) + as int?; try { return Level.values.firstWhere((e) => e.value == value); diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 779f1dd78..f943ea5aa 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -114,9 +114,10 @@ class WalletInfo implements IsarId { } @ignore - Map get otherData => otherDataJsonString == null - ? {} - : Map.from(jsonDecode(otherDataJsonString!) as Map); + Map get otherData => + otherDataJsonString == null + ? {} + : Map.from(jsonDecode(otherDataJsonString!) as Map); @ignore bool get isViewOnly => @@ -134,9 +135,25 @@ class WalletInfo implements IsarId { ?.isMnemonicVerified == true; + @ignore + bool get isDuressVisible => + otherData[WalletInfoKeys.duressMarkedVisibleWalletKey] as bool? ?? false; + //============================================================================ //============= Updaters ================================================ + Future updateDuressVisibilityStatus({ + required bool isDuressVisible, + required Isar isar, + }) async { + await updateOtherData( + newEntries: { + WalletInfoKeys.duressMarkedVisibleWalletKey: isDuressVisible, + }, + isar: isar, + ); + } + Future updateBalance({ required Balance newBalance, required Isar isar, @@ -151,9 +168,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedBalanceString: newEncoded, - ), + thisInfo.copyWith(cachedBalanceString: newEncoded), ); }); } @@ -173,9 +188,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedBalanceSecondaryString: newEncoded, - ), + thisInfo.copyWith(cachedBalanceSecondaryString: newEncoded), ); }); } @@ -195,9 +208,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedBalanceTertiaryString: newEncoded, - ), + thisInfo.copyWith(cachedBalanceTertiaryString: newEncoded), ); }); } @@ -215,9 +226,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedChainHeight: newHeight, - ), + thisInfo.copyWith(cachedChainHeight: newHeight), ); }); } @@ -235,11 +244,12 @@ class WalletInfo implements IsarId { if (customIndexOverride != null) { index = customIndexOverride; } else if (flag) { - final highest = await isar.walletInfo - .where() - .sortByFavouriteOrderIndexDesc() - .favouriteOrderIndexProperty() - .findFirst(); + final highest = + await isar.walletInfo + .where() + .sortByFavouriteOrderIndexDesc() + .favouriteOrderIndexProperty() + .findFirst(); index = (highest ?? 0) + 1; } else { index = -1; @@ -253,19 +263,14 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - favouriteOrderIndex: index, - ), + thisInfo.copyWith(favouriteOrderIndex: index), ); }); } } /// copies this with a new name and updates the db - Future updateName({ - required String newName, - required Isar isar, - }) async { + Future updateName({required String newName, required Isar isar}) async { // don't allow empty names if (newName.isEmpty) { throw Exception("Empty wallet name not allowed!"); @@ -278,11 +283,7 @@ class WalletInfo implements IsarId { if (thisInfo.name != newName) { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); - await isar.walletInfo.put( - thisInfo.copyWith( - name: newName, - ), - ); + await isar.walletInfo.put(thisInfo.copyWith(name: newName)); }); } } @@ -299,9 +300,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedReceivingAddress: newAddress, - ), + thisInfo.copyWith(cachedReceivingAddress: newAddress), ); }); } @@ -325,37 +324,27 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - otherDataJsonString: encodedNew, - ), + thisInfo.copyWith(otherDataJsonString: encodedNew), ); }); } } /// Can be dangerous. Don't use unless you know the consequences - Future setMnemonicVerified({ - required Isar isar, - }) async { + Future setMnemonicVerified({required Isar isar}) async { final meta = await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst(); if (meta == null) { await isar.writeTxn(() async { await isar.walletInfoMeta.put( - WalletInfoMeta( - walletId: walletId, - isMnemonicVerified: true, - ), + WalletInfoMeta(walletId: walletId, isMnemonicVerified: true), ); }); } else if (meta.isMnemonicVerified == false) { await isar.writeTxn(() async { await isar.walletInfoMeta.deleteByWalletId(walletId); await isar.walletInfoMeta.put( - WalletInfoMeta( - walletId: walletId, - isMnemonicVerified: true, - ), + WalletInfoMeta(walletId: walletId, isMnemonicVerified: true), ); }); } else { @@ -384,9 +373,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - restoreHeight: newRestoreHeight, - ), + thisInfo.copyWith(restoreHeight: newRestoreHeight), ); }); } @@ -423,9 +410,7 @@ class WalletInfo implements IsarId { this.cachedBalanceSecondaryString, this.cachedBalanceTertiaryString, this.otherDataJsonString, - }) : assert( - AppConfig.coins.map((e) => e.identifier).contains(coinName), - ); + }) : assert(AppConfig.coins.map((e) => e.identifier).contains(coinName)); WalletInfo copyWith({ String? name, @@ -481,9 +466,7 @@ class WalletInfo implements IsarId { Map jsonObject, AddressType mainAddressType, ) { - final coin = AppConfig.getCryptoCurrencyFor( - jsonObject["coin"] as String, - )!; + final coin = AppConfig.getCryptoCurrencyFor(jsonObject["coin"] as String)!; return WalletInfo( coinName: coin.identifier, walletId: jsonObject["id"] as String, @@ -494,11 +477,7 @@ class WalletInfo implements IsarId { @Deprecated("Legacy support") Map toMap() { - return { - "name": name, - "id": walletId, - "coin": coin.identifier, - }; + return {"name": name, "id": walletId, "coin": coin.identifier}; } @Deprecated("Legacy support") @@ -527,4 +506,6 @@ abstract class WalletInfoKeys { static const String reuseAddress = "reuseAddressKey"; static const String isViewOnlyKey = "isViewOnlyKey"; static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey"; + static const String duressMarkedVisibleWalletKey = + "duressMarkedVisibleWalletKey"; } diff --git a/lib/wallets/isar/providers/all_wallets_info_provider.dart b/lib/wallets/isar/providers/all_wallets_info_provider.dart index 0acbae46c..2bdb932ca 100644 --- a/lib/wallets/isar/providers/all_wallets_info_provider.dart +++ b/lib/wallets/isar/providers/all_wallets_info_provider.dart @@ -3,20 +3,30 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import '../../../providers/db/main_db_provider.dart'; + import '../../../app_config.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/duress_provider.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../models/wallet_info.dart'; final pAllWalletsInfo = Provider((ref) { - return ref.watch(_pAllWalletsInfo.select((value) => value.value)); + final duress = ref.watch(pDuress); + + final results = ref.watch(_pAllWalletsInfo.select((value) => value.value)); + + if (duress) { + results.retainWhere((e) => e.isDuressVisible); + } + + return results; }); final pAllWalletsInfoByCoin = Provider((ref) { final infos = ref.watch(pAllWalletsInfo); final Map wallets})> - map = {}; + map = {}; for (final info in infos) { if (map[info.coin] == null) { @@ -41,6 +51,7 @@ _WalletInfoWatcher? _globalInstance; final _pAllWalletsInfo = ChangeNotifierProvider((ref) { if (_globalInstance == null) { final isar = ref.watch(mainDBProvider).isar; + _globalInstance = _WalletInfoWatcher( isar.walletInfo .where() @@ -65,21 +76,22 @@ class _WalletInfoWatcher extends ChangeNotifier { List get value => _value; _WalletInfoWatcher(this._value, Isar isar) { - _streamSubscription = - isar.walletInfo.watchLazy(fireImmediately: true).listen((event) { - isar.walletInfo - .where() - .filter() - .anyOf( - AppConfig.coins.map((e) => e.identifier), - (q, element) => q.coinNameMatches(element), - ) - .findAll() - .then((value) { - _value = value; - notifyListeners(); - }); - }); + _streamSubscription = isar.walletInfo + .watchLazy(fireImmediately: true) + .listen((event) { + isar.walletInfo + .where() + .filter() + .anyOf( + AppConfig.coins.map((e) => e.identifier), + (q, element) => q.coinNameMatches(element), + ) + .findAll() + .then((value) { + _value = value; + notifyListeners(); + }); + }); } @override diff --git a/lib/widgets/custom_buttons/draggable_switch_button.dart b/lib/widgets/custom_buttons/draggable_switch_button.dart index 064746c6f..e0ea21ebf 100644 --- a/lib/widgets/custom_buttons/draggable_switch_button.dart +++ b/lib/widgets/custom_buttons/draggable_switch_button.dart @@ -48,10 +48,9 @@ class DraggableSwitchButtonState extends State { Color _colorBG(bool isOn, bool enabled, double alpha) { if (enabled) { return Color.alphaBlend( - Theme.of(context) - .extension()! - .switchBGOn - .withOpacity(alpha), + Theme.of( + context, + ).extension()!.switchBGOn.withOpacity(alpha), Theme.of(context).extension()!.switchBGOff, ); } @@ -61,10 +60,9 @@ class DraggableSwitchButtonState extends State { Color _colorFG(bool isOn, bool enabled, double alpha) { if (enabled) { return Color.alphaBlend( - Theme.of(context) - .extension()! - .switchCircleOn - .withOpacity(alpha), + Theme.of( + context, + ).extension()!.switchCircleOn.withOpacity(alpha), Theme.of(context).extension()!.switchCircleOff, ); } @@ -190,15 +188,11 @@ class DraggableSwitchButtonState extends State { children: [ SizedBox( width: constraint.maxWidth / 2, - child: Center( - child: widget.onItem, - ), + child: Center(child: widget.onItem), ), SizedBox( width: constraint.maxWidth / 2, - child: Center( - child: widget.offItem, - ), + child: Center(child: widget.offItem), ), ], ), @@ -215,3 +209,85 @@ class DSBController { VoidCallback? activate; bool Function()? isOn; } + +// new and improved(ish) version +class DraggableSwitch extends StatefulWidget { + final bool value; + final ValueChanged onChanged; + + const DraggableSwitch({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + State createState() => _DraggableSwitchState(); +} + +class _DraggableSwitchState extends State { + final _duration = const Duration(milliseconds: 150); + + double _dragOffset = 0.0; + + void _handleDragEnd(DragEndDetails details) { + if (_dragOffset.abs() > 10) { + final shouldTurnOn = _dragOffset > 0; + if (widget.value != shouldTurnOn) { + widget.onChanged(shouldTurnOn); + } + } + _dragOffset = 0.0; + } + + void _toggle() { + widget.onChanged(!widget.value); + } + + @override + Widget build(BuildContext context) { + final isOn = widget.value; + + final colors = Theme.of(context).extension()!; + + final colorBG = isOn ? colors.switchBGOn : colors.switchBGOff; + final colorFG = isOn ? colors.switchCircleOn : colors.switchCircleOff; + + return GestureDetector( + onTap: _toggle, + onHorizontalDragUpdate: (details) { + _dragOffset += details.primaryDelta!; + }, + onHorizontalDragEnd: _handleDragEnd, + child: LayoutBuilder( + builder: (context, constraints) { + return AnimatedContainer( + duration: _duration, + curve: Curves.easeInOut, + width: constraints.maxWidth, + height: constraints.maxHeight, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: colorBG, + borderRadius: BorderRadius.circular(constraints.maxHeight / 2), + ), + child: AnimatedAlign( + duration: _duration, + curve: Curves.easeInOut, + alignment: isOn ? Alignment.centerRight : Alignment.centerLeft, + child: AnimatedContainer( + duration: _duration, + height: constraints.maxHeight - 4, + width: constraints.maxWidth / 2 - 4, + decoration: BoxDecoration( + color: colorFG, + shape: BoxShape.circle, + ), + ), + ), + ); + }, + ), + ); + } +} From 6e086611a947c8a5796cbce5452eea6de26dbd15 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 16 May 2025 15:51:57 -0600 Subject: [PATCH 2/6] update mocks --- test/cached_electrumx_test.mocks.dart | 30 +++++++++++++++++ .../pages/send_view/send_view_test.mocks.dart | 32 +++++++++++++++++++ .../exchange/exchange_view_test.mocks.dart | 30 +++++++++++++++++ .../managed_favorite_test.mocks.dart | 32 +++++++++++++++++++ .../node_options_sheet_test.mocks.dart | 32 +++++++++++++++++++ .../table_view/table_view_row_test.mocks.dart | 2 ++ .../transaction_card_test.mocks.dart | 32 +++++++++++++++++++ test/widget_tests/wallet_card_test.mocks.dart | 2 ++ ...et_info_row_balance_future_test.mocks.dart | 2 ++ .../wallet_info_row_test.mocks.dart | 2 ++ 10 files changed, 196 insertions(+) diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 11c558f20..22ab2a712 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -911,6 +911,36 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 476ab3883..c4398cf4d 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -202,6 +202,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -209,6 +210,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -848,6 +850,36 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 1271b96b0..643d6cc6b 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -272,6 +272,36 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 218b9c0c5..6eb09a26e 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -202,6 +202,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -209,6 +210,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -553,6 +555,36 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index cf7098de6..172abecf2 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -203,6 +203,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -210,6 +211,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -428,6 +430,36 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 9393a5730..4d7adcad3 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -171,6 +171,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -178,6 +179,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i8.Future.value(), diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index ac6790693..a090e757e 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -230,6 +230,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i13.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -237,6 +238,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -526,6 +528,36 @@ class MockPrefs extends _i1.Mock implements _i13.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index 80c03af35..1ba8ca158 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -174,6 +174,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -181,6 +182,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i8.Future.value(), diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index d7fea45d4..7ded2cf87 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -170,6 +170,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future load( _i10.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -177,6 +178,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i8.Future.value(), diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 88f854589..3f2a24ac7 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -184,6 +184,7 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { _i9.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -191,6 +192,7 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i9.Future.value(), From d23f50bfc5cd0e1aaf233ea437928746e91251a9 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 16 May 2025 15:56:51 -0600 Subject: [PATCH 3/6] refactor to descriptive function name --- lib/pages/send_view/send_view.dart | 8 ++++---- .../wallet_view/sub_widgets/desktop_send.dart | 4 ++-- lib/utilities/address_utils.dart | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 3c907605e..4047a2064 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -231,7 +231,7 @@ class _SendViewState extends ConsumerState { _applyUri(paymentData); } else { if (coin is Epiccash) { - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } sendToController.text = content; @@ -245,7 +245,7 @@ class _SendViewState extends ConsumerState { } catch (e) { if (coin is Epiccash) { // strip http:// and https:// if content contains @ - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } await _checkSparkNameAndOrSetAddress(content); @@ -977,7 +977,7 @@ class _SendViewState extends ConsumerState { if (coin is Epiccash) { // strip http:// and https:// if content contains @ - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } final trimmed = content.trim(); @@ -1223,7 +1223,7 @@ class _SendViewState extends ConsumerState { _address = _address!.substring(0, _address!.indexOf("\n")); } - sendToController.text = AddressUtils().formatAddress(_address!); + sendToController.text = AddressUtils().formatEpicCashAddress(_address!); } }); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 300bc4d99..7dd007a2e 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -737,7 +737,7 @@ class _DesktopSendState extends ConsumerState { _applyUri(paymentData); } else { if (coin is Epiccash) { - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } sendToController.text = content; @@ -752,7 +752,7 @@ class _DesktopSendState extends ConsumerState { // If parsing fails, treat it as a plain address. if (coin is Epiccash) { // strip http:// and https:// if content contains @ - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } await _checkSparkNameAndOrSetAddress(content); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index d14e4e809..9b2f03758 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -229,7 +229,7 @@ class AddressUtils { } /// Formats an address string to remove any unnecessary prefixes or suffixes. - String formatAddress(String epicAddress) { + String formatEpicCashAddress(String epicAddress) { // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address) if ((epicAddress.startsWith("http://") || epicAddress.startsWith("https://")) && From 7d28ed89342cc7b969e0408c94506f812c38ec02 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 16 May 2025 16:40:39 -0600 Subject: [PATCH 4/6] fix: empty other data error --- .../new_wallet_recovery_phrase_warning_view.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index a35c4563a..2ad110fbd 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -150,10 +150,15 @@ class _NewWalletRecoveryPhraseWarningViewState otherDataJson[WalletInfoKeys.duressMarkedVisibleWalletKey] = true; } + String? otherDataJsonString; + if (otherDataJson != null && otherDataJson.isNotEmpty) { + otherDataJsonString = jsonEncode(otherDataJson); + } + final info = WalletInfo.createNew( coin: widget.coin, name: widget.walletName, - otherDataJsonString: jsonEncode(otherDataJson), + otherDataJsonString: otherDataJsonString, ); var node = ref From d6bc8befa796c2e951dc7c3357a8cdf34fca796a Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 16 May 2025 16:42:29 -0600 Subject: [PATCH 5/6] hack in check for addresses that can contain colon in the uri parsing logic --- lib/utilities/address_utils.dart | 14 ++++++++++++++ test/address_utils_test.dart | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 9b2f03758..9e63370e1 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -134,6 +134,20 @@ class AddressUtils { /// /// Returns null on failure to parse static PaymentUriData? parsePaymentUri(String uri, {Logging? logging}) { + // hacky check its not just a bcash, ecash, or xel address + final parts = uri.split(":"); + if (parts.length == 2) { + if ([ + "xel", + "bitcoincash", + "bchtest", + "ecash", + "ectest", + ].contains(parts.first.toLowerCase())) { + return null; + } + } + try { final Map parsedData = _parseUri(uri); diff --git a/test/address_utils_test.dart b/test/address_utils_test.dart index 599380c31..c3ce3cbad 100644 --- a/test/address_utils_test.dart +++ b/test/address_utils_test.dart @@ -57,6 +57,17 @@ void main() { expect(AddressUtils.parsePaymentUri(uri), isNull); }); + test("parse double prefix type address", () { + const uri = + "bitcoin:xel:$firoAddress?amount=50.1&message=eggs%20are%20good%21"; + final result = AddressUtils.parsePaymentUri(uri); + expect(result, isNotNull); + expect(result!.scheme, "bitcoin"); + expect(result.address, "xel:$firoAddress"); + expect(result.amount, "50.1"); + expect(result.message, "eggs are good!"); + }); + test("encode a list of (mnemonic) words/strings as a json object", () { final List list = [ "hello", @@ -92,7 +103,10 @@ void main() { test("build a uri string with empty params", () { expect( AddressUtils.buildUriString( - Firo(CryptoCurrencyNetwork.main).uriScheme, firoAddress, {}), + Firo(CryptoCurrencyNetwork.main).uriScheme, + firoAddress, + {}, + ), "firo:$firoAddress", ); }); From 2bbe82d4006520f8ce3ed6a0cb5bb8c443598ef6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 19 May 2025 09:47:52 -0600 Subject: [PATCH 6/6] Fix hardcoded android logs dir. Might fix https://github.com/cypherstack/stack_wallet/issues/1134 ? --- lib/utilities/stack_file_system.dart | 37 ++++++++++++------- pubspec.lock | 8 ++-- scripts/app_config/templates/pubspec.template | 2 +- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 14cb0fae0..4f9bff090 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -10,6 +10,7 @@ import 'dart:io'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -40,14 +41,17 @@ abstract class StackFileSystem { if (Util.isArmLinux) { appDirectory = await getApplicationDocumentsDirectory(); appDirectory = Directory( - "${appDirectory.path}/.${AppConfig.appDefaultDataDirName}", + path.join(appDirectory.path, ".${AppConfig.appDefaultDataDirName}"), ); } else if (Platform.isLinux) { if (_overrideDesktopDirPath != null) { appDirectory = Directory(_overrideDesktopDirPath!); } else { appDirectory = Directory( - "${Platform.environment['HOME']}/.${AppConfig.appDefaultDataDirName}", + path.join( + Platform.environment['HOME']!, + ".${AppConfig.appDefaultDataDirName}", + ), ); } } else if (Platform.isWindows) { @@ -62,7 +66,7 @@ abstract class StackFileSystem { } else { appDirectory = await getLibraryDirectory(); appDirectory = Directory( - "${appDirectory.path}/${AppConfig.appDefaultDataDirName}", + path.join(appDirectory.path, AppConfig.appDefaultDataDirName), ); } } else if (Platform.isIOS) { @@ -86,7 +90,7 @@ abstract class StackFileSystem { static Future applicationIsarDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/isar"); + final dir = Directory(path.join(root.path, "isar")); if (!dir.existsSync()) { await dir.create(); } @@ -99,7 +103,7 @@ abstract class StackFileSystem { static Future applicationDriftDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/drift"); + final dir = Directory(path.join(root.path, "drift")); if (!dir.existsSync()) { await dir.create(); } @@ -126,7 +130,7 @@ abstract class StackFileSystem { static Future applicationTorDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/tor"); + final dir = Directory(path.join(root.path, "tor")); if (!dir.existsSync()) { await dir.create(); } @@ -139,7 +143,7 @@ abstract class StackFileSystem { static Future applicationFiroCacheSQLiteDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/sqlite/firo_cache"); + final dir = Directory(path.join(root.path, "sqlite", "firo_cache")); if (!dir.existsSync()) { await dir.create(recursive: true); } @@ -152,7 +156,7 @@ abstract class StackFileSystem { static Future applicationHiveDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/hive"); + final dir = Directory(path.join(root.path, "hive")); if (!dir.existsSync()) { await dir.create(); } @@ -164,7 +168,7 @@ abstract class StackFileSystem { static Future applicationXelisDirectory() async { final root = await applicationRootDirectory(); - final dir = Directory("${root.path}${Platform.pathSeparator}xelis"); + final dir = Directory(path.join(root.path, "xelis")); if (!dir.existsSync()) { await dir.create(); } @@ -173,7 +177,7 @@ abstract class StackFileSystem { static Future applicationXelisTableDirectory() async { final xelis = await applicationXelisDirectory(); - final dir = Directory("${xelis.path}${Platform.pathSeparator}table"); + final dir = Directory(path.join(xelis.path, "table")); if (!dir.existsSync()) { await dir.create(); } @@ -183,7 +187,7 @@ abstract class StackFileSystem { static Future initThemesDir() async { final root = await applicationRootDirectory(); - final dir = Directory("${root.path}/themes"); + final dir = Directory(path.join(root.path, "themes")); if (!dir.existsSync()) { await dir.create(); } @@ -206,13 +210,18 @@ abstract class StackFileSystem { final Directory logsDir; if (Platform.isIOS) { - logsDir = Directory("${appDocsDir.path}/logs"); + logsDir = Directory(path.join(appDocsDir.path, "logs")); } else if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { // TODO check this is correct for macos - logsDir = Directory("${appDocsDir.path}/$logsDirName"); + logsDir = Directory(path.join(appDocsDir.path, logsDirName)); } else if (Platform.isAndroid) { await Permission.storage.request(); - logsDir = Directory("/storage/emulated/0/Documents/$logsDirName"); + final ext = await getExternalStorageDirectory(); + final rootPath = path.dirname( + path.dirname(path.dirname(path.dirname(ext!.path))), + ); + final logsDirPath = path.join(rootPath, "Documents", logsDirName); + logsDir = Directory(logsDirPath); } else { throw Exception("Unsupported Platform"); } diff --git a/pubspec.lock b/pubspec.lock index 16e439300..c5baa5ce0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1574,18 +1574,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.0+1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index d086b0f94..f4959f93d 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -78,7 +78,7 @@ dependencies: # Utility plugins http: ^0.13.0 local_auth: ^2.3.0 - permission_handler: ^11.0.0 + permission_handler: ^12.0.0+1 flutter_local_notifications: ^17.2.2 rxdart: ^0.27.3 zxcvbn: ^1.0.0