diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index d0e653907..afd9284be 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -370,6 +370,8 @@ abstract class MoneroWalletBase }; } + bool hasUnknownKeyImages() => currentWallet!.hasUnknownKeyImages(); + bool needExportOutputs(Money amount) { if (int.tryParse(currentWallet!.secretSpendKey()) != 0) { return false; diff --git a/cw_monero/lib/trezor.dart b/cw_monero/lib/trezor.dart index a9a387912..b851859cb 100644 --- a/cw_monero/lib/trezor.dart +++ b/cw_monero/lib/trezor.dart @@ -1,27 +1,28 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:trezor_flutter/trezor_flutter.dart'; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:trezor_flutter/trezor_flutter.dart"; class MoneroTrezorService extends HardwareWalletService { - final TrezorClient client; - MoneroTrezorService(this.client); + + final TrezorClient client; } class MoneroTrezorWatchCredentials { + const MoneroTrezorWatchCredentials(this.watchKey, this.address); + final String watchKey; final String address; - - const MoneroTrezorWatchCredentials(this.watchKey, this.address); } class Trezor { - final MoneroTrezorService service; - Trezor(this.service); + final MoneroTrezorService service; + String? _sessionPassphrase; + Future newPassphraseSession(String? passphrase) async { if (passphrase == null) return; @@ -42,21 +43,23 @@ class Trezor { final txIds = []; for (final tdi in tdisList) { - txIds.add(MoneroKeyImageTxData( - outKey: tdi["out_key"], - txPubKey: tdi["tx_pub_key"], - internalOutputIndex: tdi["internal_output_index"], - subAddrMajor: tdi["sub_addr_major"], - subAddrMinor: tdi["sub_addr_minor"], + txIds.add( + MoneroKeyImageTxData( + outKey: tdi["out_key"] as String, + txPubKey: tdi["tx_pub_key"] as String, + internalOutputIndex: tdi["internal_output_index"] as int, + subAddrMajor: tdi["sub_addr_major"] as int, + subAddrMinor: tdi["sub_addr_minor"] as int, additionalTxPubKeys: - (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [])); + (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [], + ), + ); } final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds); return jsonEncode(keyImages.toMap()); } - Future signTransaction(String json) async { - return TrezorMonero(service.client).signTransaction(jsonDecode(json)); - } + Future signTransaction(String json) => + TrezorMonero(service.client).signTransaction(jsonDecode(json) as Map); } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 275e7d4aa..0cc1a9e19 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -921,8 +921,8 @@ packages: dependency: "direct main" description: path: trezor-flutter - ref: "944080d950a62dd48a199b9736237cbe580351de" - resolved-ref: "944080d950a62dd48a199b9736237cbe580351de" + ref: "7a736320f6d53ad5c763734ab832d6bea560a327" + resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327" url: "https://github.com/cake-tech/trezor-flutter.git" source: git version: "0.1.0" @@ -930,8 +930,8 @@ packages: dependency: transitive description: path: trezor_usb_transport - ref: "944080d950a62dd48a199b9736237cbe580351de" - resolved-ref: "944080d950a62dd48a199b9736237cbe580351de" + ref: "7a736320f6d53ad5c763734ab832d6bea560a327" + resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327" url: "https://github.com/cake-tech/trezor-flutter.git" source: git version: "0.0.1" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 692103eba..3f1c10f7b 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: trezor_flutter: git: url: https://github.com/cake-tech/trezor-flutter.git - ref: 944080d950a62dd48a199b9736237cbe580351de + ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97 path: trezor-flutter dev_dependencies: diff --git a/lib/di.dart b/lib/di.dart index 6bb8bd98c..da00561a5 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -434,7 +434,9 @@ Future setup({ () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"), ); - getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt())); + getIt.registerLazySingleton( + () => TrezorConnectViewModel(getIt(), getIt()), + ); getIt.registerFactory(() => KeyService(getIt.get())); diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 7d2b8ec26..d1e421d30 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -420,6 +420,12 @@ class CWMonero extends Monero { return moneroWallet.needExportOutputs(amount); } + @override + bool hasUnknownKeyImages(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.hasUnknownKeyImages(); + } + @override void monerocCheck() => checkIfMoneroCIsFine(); diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index d25b3440e..1b379df8e 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -19,6 +19,7 @@ import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart"; import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart"; import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart"; import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; import "package:cake_wallet/new-ui/widgets/picker.dart"; import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; @@ -42,7 +43,6 @@ import "package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_shee import "package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart"; import "package:cake_wallet/src/widgets/cake_image_widget.dart"; import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart"; -import "package:cake_wallet/src/widgets/primary_button.dart"; import "package:cake_wallet/src/widgets/standard_checkbox.dart"; import "package:cake_wallet/store/app_store.dart"; import "package:cake_wallet/utils/payment_request.dart"; @@ -618,9 +618,9 @@ class _NewSendPageState extends State { selectedDot: _selectedOutput, ), Observer( - builder: (_) => LoadingPrimaryButton( + builder: (_) => NewFuturePrimaryButton( key: const ValueKey("send_page_send_button_key"), - onPressed: () { + onPressed: () async { //Request dummy node to get the focus out of the text fields FocusScope.of(context).requestFocus(FocusNode()); @@ -629,11 +629,11 @@ class _NewSendPageState extends State { } if (widget.mode == SendPageModes.normal) { - _handleSend(); + await _handleSend(); } else if (widget.mode == SendPageModes.lightningDeposit || widget.mode == SendPageModes.mwebDeposit) { - Navigator.of(context).push( + await Navigator.of(context).push( CupertinoPageRoute( builder: (context) => Material( child: L2ActionWalletSelector( @@ -652,7 +652,7 @@ class _NewSendPageState extends State { } else if (widget.mode == SendPageModes.lightningWithdrawal || widget.mode == SendPageModes.mwebWithdrawal) { - Navigator.of(context).push( + await Navigator.of(context).push( CupertinoPageRoute( builder: (context) => Material( child: L2ActionWalletSelector( @@ -673,13 +673,7 @@ class _NewSendPageState extends State { text: S.of(context).continue_text, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, - isLoading: widget.sendViewModel.state is IsExecutingState || - widget.sendViewModel.state is TransactionCommitting || - widget.sendViewModel.state - is IsAwaitingDeviceResponseState || - widget.sendViewModel.state - is LoadingTemplateExecutingState, - isDisabled: !widget.sendViewModel.isReadyForSend || + disabled: !widget.sendViewModel.isReadyForSend || widget.sendViewModel.state is ExecutedSuccessfullyState, ), ), @@ -858,17 +852,11 @@ class _NewSendPageState extends State { for (final item in widget.sendViewModel.outputs) { amount += item.cryptoAmountMoney; } - if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { - if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) { - await monero!.syncTrezor(widget.sendViewModel.wallet); - } else { - if (mounted) { - await Navigator.of(context).pushNamed( - Routes.urqrAnimatedPage, - arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet), - ); - } - } + if (monero!.hasUnknownKeyImages(widget.sendViewModel.wallet) && mounted) { + await Navigator.of(context).pushNamed( + Routes.syncKeyImagesDevices, + arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet), + ); await Future.delayed(const Duration(seconds: 1)); // wait for monero to refresh the state } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { diff --git a/lib/new-ui/pages/settings_page.dart b/lib/new-ui/pages/settings_page.dart index 2183ec5d4..8aaec520d 100644 --- a/lib/new-ui/pages/settings_page.dart +++ b/lib/new-ui/pages/settings_page.dart @@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import "package:cw_core/wallet_type.dart"; +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; bool _trueFunc(DashboardViewModel _) => true; @@ -26,10 +25,21 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb; bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect; -bool _isCupcake(DashboardViewModel vm) => - vm.wallet.hardwareWalletType == HardwareWalletType.cupcake; +bool _requiresKeyImageSync(DashboardViewModel vm) => + vm.wallet.type == WalletType.monero && + [HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType); class SettingsListItem { + const SettingsListItem( + this.iconPath, + this.title, + this.route, { + this.requireAuth = false, + this.use2fa = _falseFunc, + this.condition = _trueFunc, + this.routeArgs, + }); + final String iconPath; final String title; final String route; @@ -37,21 +47,15 @@ class SettingsListItem { final bool requireAuth; final bool Function(DashboardViewModel) use2fa; final bool Function(DashboardViewModel) condition; - - const SettingsListItem(this.iconPath, this.title, this.route, - {this.routeArgs = null, - this.requireAuth = false, - this.use2fa = _falseFunc, - this.condition = _trueFunc}); } class SettingsSectionData { + const SettingsSectionData(this.title, this.titleIconPath, this.items); + final String title; final String titleIconPath; final List items; - const SettingsSectionData(this.title, this.titleIconPath, this.items); - static SettingsSectionData walletSettings = SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [ SettingsListItem( @@ -71,9 +75,13 @@ class SettingsSectionData { condition: _hasWalletConnect), //SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc), //SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb), - SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs, - Routes.urqrAnimatedPage, - routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake), + SettingsListItem( + "assets/new-ui/settings_row_icons/sync-balance.svg", + S.current.resync_device, + Routes.syncKeyImagesDevices, + routeArgs: {'export-outputs': 'export-outputs'}, + condition: _requiresKeyImageSync, + ), SettingsListItem( "assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage), ]); diff --git a/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart b/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart new file mode 100644 index 000000000..d8febdcbf --- /dev/null +++ b/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart @@ -0,0 +1,224 @@ +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/src/widgets/alert_with_two_actions.dart"; +import "package:cake_wallet/src/widgets/base_alert_dialog.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/utils/show_pop_up.dart"; +import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; + +class SyncKeyImagesSheet extends StatefulWidget { + const SyncKeyImagesSheet({ + required this.appStore, + required this.trezorConnectVM, + super.key, + }); + + final AppStore appStore; + final TrezorConnectViewModelBase trezorConnectVM; + + WalletBase get wallet => appStore.wallet!; + + @override + State createState() => _HardwareWalletProceedOnDeviceSheetState(); +} + +class _HardwareWalletProceedOnDeviceSheetState extends State { + _KeyImageSyncState _state = _KeyImageSyncState.initial; + + @override + Widget build(BuildContext context) => PopScope( + canPop: false, + child: SafeArea( + bottom: false, + child: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).resync_device, + leadingIcon: Icon( + Icons.close, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onLeadingPressed: () => showPopUp( + context: context, + builder: (context) => AlertWithTwoActions( + alertTitle: S.of(context).are_you_sure_exit, + alertContent: S.of(context).resync_device_cancel_warning_desc, + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).yes_exit, + rightAlertButtonStyle: AlertButtonStyle.error(context), + actionLeftButton: Navigator.of(context).pop, + actionRightButton: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ), + ), + ), + Expanded( + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CakeImageWidget( + imageUrl: hardwareWalletIcon, + width: 100, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + DirectionalAnimatedSwitcher( + duration: const Duration(milliseconds: 400), + child: content, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: AnimatedOpacity( + curve: Curves.easeOutQuad, + opacity: _state == _KeyImageSyncState.initial ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: PrimaryButton( + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + onPressed: _onContinuePressed, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + + Widget get content { + if (_state == _KeyImageSyncState.syncing) { + return _syncingKeyImages(); + } + + return _initial(); + } + + String? get hardwareWalletIcon { + switch (widget.wallet.hardwareWalletType) { + case HardwareWalletType.bitbox: + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; + case HardwareWalletType.ledger: + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; + case HardwareWalletType.trezor: + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; + case HardwareWalletType.cupcake: + return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + case null: + return "assets/images/hardware_wallet/device_qr.svg"; + } + } + + Widget _initial() => Column( + key: const ValueKey(0), + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).resync_device_desc, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ); + + Widget _syncingKeyImages() => Column( + key: const ValueKey(1), + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).proceed_on_device, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 20, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).proceed_on_device_description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ], + ); + + Future _onContinuePressed() async { + setState(() => _state = _KeyImageSyncState.syncing); + + if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) { + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: widget.wallet.type, + hardwareWalletType: widget.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (_, __) { + widget.trezorConnectVM.initWallet(widget.wallet); + Navigator.of(context).pop(); + }, + isReconnect: false, + ), + ); + + // Recheck to handle tap-backs + if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) { + setState(() => _state = _KeyImageSyncState.initial); + return; + } + } else { + await widget.trezorConnectVM.initWallet(widget.wallet); + } + + final result = await widget.trezorConnectVM.syncKeyImages(widget.wallet); + if (result && context.mounted) { + Navigator.of(context).pop(); + } + } +} + +enum _KeyImageSyncState { + initial, + syncing; +} diff --git a/lib/new-ui/widgets/new_future_primary_button.dart b/lib/new-ui/widgets/new_future_primary_button.dart new file mode 100644 index 000000000..2440f1938 --- /dev/null +++ b/lib/new-ui/widgets/new_future_primary_button.dart @@ -0,0 +1,57 @@ +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; + +class NewFuturePrimaryButton extends StatefulWidget { + const NewFuturePrimaryButton({ + required this.onPressed, + required this.text, + required this.color, + required this.textColor, + this.borderColor = Colors.transparent, + this.disabled = false, + this.image, + super.key, + }); + + final Future Function() onPressed; + final bool disabled; + final SvgPicture? image; + final Color color; + final Color textColor; + final Color borderColor; + final String text; + + @override + State createState() => _NewFuturePrimaryButtonState(); +} + +class _NewFuturePrimaryButtonState extends State { + bool isLoading = false; + + Future _onPressed() async { + if (isLoading) { + return; + } + setState(() => isLoading = true); + try { + await widget.onPressed.call(); + } finally { + if (mounted) { + setState(() => isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) => NewPrimaryButton( + onPressed: _onPressed, + image: widget.image, + text: widget.text, + color: widget.color, + textColor: widget.textColor, + isLoading: isLoading, + borderColor: widget.borderColor, + disabled: widget.disabled, + ); +} diff --git a/lib/router.dart b/lib/router.dart index 837eab8c8..39a28a42f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/new-ui/new_dashboard.dart'; import 'package:cake_wallet/new-ui/pages/about_page.dart'; -import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart'; @@ -13,6 +12,7 @@ import 'package:cake_wallet/new-ui/pages/addresses_page.dart'; import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart'; import "package:cake_wallet/new-ui/pages/receive_page.dart"; import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; @@ -51,6 +51,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; import 'package:cake_wallet/src/screens/dev/network_requests.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; @@ -126,7 +127,6 @@ import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; -import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; @@ -146,7 +146,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; @@ -908,6 +908,19 @@ Route createRoute(RouteSettings settings) { case Routes.setup2faInfoPage: return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.syncKeyImagesDevices: + final hardwareWalletType = getIt().wallet!.hardwareWalletType; + if (hardwareWalletType == HardwareWalletType.trezor) { + return MaterialPageRoute( + builder: (_) => SyncKeyImagesSheet( + appStore: getIt(), + trezorConnectVM: getIt(), + ), + ); + } + return MaterialPageRoute( + builder: (_) => getIt.get(param1: settings.arguments)); + case Routes.urqrAnimatedPage: return MaterialPageRoute( builder: (_) => getIt.get(param1: settings.arguments)); diff --git a/lib/routes.dart b/lib/routes.dart index 236196bf7..b21e2bc38 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -133,6 +133,7 @@ class Routes { static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; + static const syncKeyImagesDevices = '/device/sync_key_images'; static const urqrAnimatedPage = '/urqr/animated_page'; static const walletGroupsDisplayPage = '/wallet_groups_display_page'; static const walletGroupDescription = '/wallet_group_description'; diff --git a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart index c048edf7c..28116e3cb 100644 --- a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart +++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart @@ -1,43 +1,43 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:convert"; +import "dart:io"; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cw_core/hardware/device_connection_type.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:mobx/mobx.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:trezor_connect/trezor_connect.dart' as connect_sdk; -import 'package:trezor_flutter/trezor_flutter.dart' as sdk; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/core/secure_storage.dart"; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/evm/evm.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cake_wallet/wallet_type_utils.dart"; +import "package:cw_core/encryption_file_utils.dart"; +import "package:cw_core/hardware/device_connection_type.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/key.dart"; +import "package:cw_core/root_dir.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:device_info_plus/device_info_plus.dart"; +import "package:flutter/material.dart"; +import "package:mobx/mobx.dart"; +import "package:permission_handler/permission_handler.dart"; +import "package:trezor_connect/trezor_connect.dart" as connect_sdk; +import "package:trezor_flutter/trezor_flutter.dart" as sdk; -part 'trezor_connect_view_model.g.dart'; +part "trezor_connect_view_model.g.dart"; class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel; abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store { - final connect_sdk.TrezorConnect trezorConnect; - - late final sdk.TrezorInterface trezorBLE; - late final sdk.TrezorInterface trezorUSB; - - sdk.ThpState _state = sdk.ThpState(); - - TrezorConnectViewModelBase(this.trezorConnect) { + TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) { if (_doesSupportHardwareWallets) { reaction((_) => isBleEnabled, (_) { - if (isBleEnabled) _initBLE(); + if (isBleEnabled) { + _initBLE(); + } }); updateBleState(); @@ -47,11 +47,21 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S } } + final connect_sdk.TrezorConnect trezorConnect; + final SecureStorage _secureStorage; + + late final sdk.TrezorInterface trezorBLE; + late final sdk.TrezorInterface trezorUSB; + + sdk.ThpState? _state; + bool get _doesSupportHardwareWallets { if (isMoneroOnly) { return DeviceConnectionType.supportedConnectionTypes( - WalletType.monero, HardwareWalletType.trezor, Platform.isIOS) - .isNotEmpty; + WalletType.monero, + HardwareWalletType.trezor, + Platform.isIOS, + ).isNotEmpty; } return true; @@ -63,9 +73,11 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S if (isBleEnabled && !_bleIsInitialized) { trezorBLE = sdk.TrezorInterface.ble( onPermissionRequest: (_) async { - if (Platform.isMacOS) return true; + if (Platform.isMacOS) { + return true; + } - Map statuses = await [ + final Map statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.bluetoothAdvertise, @@ -73,7 +85,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S return statuses.values.where((status) => status.isDenied).isEmpty; }, - bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)), + bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)), ); _bleIsInitialized = true; } @@ -99,21 +111,27 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S final newState = bleState == sdk.AvailabilityState.poweredOn; - if (newState != isBleEnabled) isBleEnabled = newState; + if (newState != isBleEnabled) { + isBleEnabled = newState; + } } @override Stream scanForBleDevices() => - trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d)); + trezorBLE.scan().map(TrezorHardwareWalletDevice.new); @override - Future> getAllUsbDevices() => trezorUSB.devices - .then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList()); + Future> getAllUsbDevices() => + trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList()); @override Future stopScanning() async { - if (_bleIsInitialized) await trezorBLE.stopScanning(); - if (!Platform.isIOS) await trezorUSB.stopScanning(); + if (_bleIsInitialized) { + await trezorBLE.stopScanning(); + } + if (!Platform.isIOS) { + await trezorUSB.stopScanning(); + } } sdk.TrezorClient? _client; @@ -127,10 +145,17 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S @override @action - Future connectDevice(HardwareWalletDevice device, WalletType type, - [bool isRetry = false]) async { - if (!(device is TrezorHardwareWalletDevice)) return false; - if (isConnecting) return false; + Future connectDevice( + HardwareWalletDevice device, + WalletType type, [ + bool isRetry = false, + ]) async { + if (device is! TrezorHardwareWalletDevice) { + return false; + } + if (isConnecting) { + return false; + } isConnecting = true; paringState = TrezorParingState.initial; @@ -140,16 +165,19 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S final connection = await trezorInterface.connect(device.device); if (!isRetry) { - showModalBottomSheet( - context: navigatorKey.currentContext!, - isScrollControlled: true, - isDismissible: false, - enableDrag: false, - useSafeArea: true, - builder: (context) => HardwareWalletProceedOnDeviceSheet( + unawaited( + showModalBottomSheet( + context: navigatorKey.currentContext!, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + useSafeArea: true, + builder: (_) => HardwareWalletProceedOnDeviceSheet( hardwareWalletType: hardwareWalletType, trezorConnectVM: this, - onRetry: () => connectDevice(device, type, true)), + onRetry: () => connectDevice(device, type, true), + ), + ), ); } @@ -159,15 +187,18 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S final res = await _pinCompleter!.future; paringState = TrezorParingState.verifyingPin; - if (res == null) throw Exception(); + if (res == null) { + throw Exception(); + } return res; } final deviceInfo = await _deviceName; + _state ??= await _getState(); _client = sdk.TrezorClient.getClientForConnection( connection, - _state, + _state!, "Cake Wallet", deviceInfo, onPinCode, @@ -175,6 +206,16 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S await _client!.createChannel(); + if (!_state!.pairingCredentials.any((c) => c.autoconnect == true)) { + if (_client case final sdk.TrezorClientV2 clientV2) { + try { + final auto = await clientV2.getAutoPairingCredentials(); + _state!.setPairingCredentials([auto]); + await _saveState(); + } catch (_) {} + } + } + paringState = TrezorParingState.success; return true; } catch (e) { @@ -192,8 +233,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S Future get _deviceName async { final deviceInfo = DeviceInfoPlugin(); - if (Platform.isAndroid) return (await deviceInfo.androidInfo).model; - if (Platform.isIOS) return (await deviceInfo.iosInfo).name; + if (Platform.isAndroid) { + return (await deviceInfo.androidInfo).model; + } + if (Platform.isIOS) { + return (await deviceInfo.iosInfo).name; + } return "Computer"; } @@ -231,9 +276,61 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S case WalletType.polygon: return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); default: - throw Exception('Unexpected wallet type: ${wallet.type} for trezor'); + throw Exception("Unexpected wallet type: ${wallet.type} for trezor"); } } + + final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true); + static const String _secureStorageKey = "com.cakewallet.trezor/thp_state"; + + Future get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc"; + + Future _getState() async { + final password = await _secureStorage.read(key: _secureStorageKey); + if (password == null) { + return _saveState(); + } + try { + final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password); + return sdk.ThpState.fromJson(state); + } catch (_) { + return _saveState(); + } + } + + Future _saveState() async { + try { + final password = generateKey(); + await _secureStorage.write(key: _secureStorageKey, value: password); + + final file = File(await _thpJsonFile); + if (!file.existsSync()) { + file.createSync(); + } + + final state = _state ?? sdk.ThpState(); + await _encryptionFileUtils.write( + path: file.path, + password: password, + data: jsonEncode(state.toMap()), + ); + _state = state; + return state; + } catch (_) { + throw Exception("Unable to save Trezor State"); + } + } + + Future syncKeyImages(WalletBase wallet) async { + if (wallet.type == WalletType.monero) { + try { + await monero!.syncTrezor(wallet); + } catch (_) { + return false; + } + } + return true; + } } abstract class TrezorParingState { @@ -241,6 +338,7 @@ abstract class TrezorParingState { static TrezorParingState enterPin = EnterPinTrezorParingState(); static TrezorParingState verifyingPin = VerifyingPinTrezorParingState(); static TrezorParingState success = SuccessTrezorParingState(); + static TrezorParingState fail(String message) => FailTrezorParingState(message); } @@ -253,7 +351,7 @@ class VerifyingPinTrezorParingState extends TrezorParingState {} class SuccessTrezorParingState extends TrezorParingState {} class FailTrezorParingState extends TrezorParingState { - final String message; - FailTrezorParingState(this.message); + + final String message; } diff --git a/res/pictures/settings_row_icons/sync-balance.svg b/res/pictures/settings_row_icons/sync-balance.svg new file mode 100644 index 000000000..fc3274a66 --- /dev/null +++ b/res/pictures/settings_row_icons/sync-balance.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 125537f7b..815de2868 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -529,7 +529,6 @@ "i_understand": "أفهم", "icon_style": "نمط الأيقونة", "id": "المعرّف: ", - "if_you_dont_see_your_device": "إذا لم يظهر جهازك أعلاه، يُرجى التأكد من أن Ledger قيد التشغيل ومفتوح القفل!", "ignor": "تجاهل", "import": "استيراد", "importNFTs": "استيراد رموز NFT", @@ -1451,4 +1450,4 @@ "wallet_balance": "رصيد المحفظة", "wc_desc": "اتصل بسهولة بتطبيقات DApps", "yes_exit": "نعم. اخرج" -} \ No newline at end of file +} diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 59d11edb6..45136deab 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -529,7 +529,6 @@ "i_understand": "Разбирам", "icon_style": "Стил на иконата", "id": "ID: ", - "if_you_dont_see_your_device": "Ако не виждате устройството си по-горе, моля, уверете се, че вашият Ledger е буден и отключен!", "ignor": "Игнорирай", "import": "Импортиране", "importNFTs": "Импортиране на NFT", @@ -1451,4 +1450,4 @@ "wallet_balance": "Баланс на портфейла", "wc_desc": "Свържете се лесно с dApps", "yes_exit": "Да. Изход" -} \ No newline at end of file +} diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index c2c5c49c0..c3df26ca8 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -529,7 +529,6 @@ "i_understand": "Rozumím", "icon_style": "Styl ikony", "id": "ID: ", - "if_you_dont_see_your_device": "Pokud své zařízení výše nevidíte, ujistěte se, že je váš Ledger zapnutý a odemčený!", "ignor": "Ignorovat", "import": "Importovat", "importNFTs": "Importovat NFT", @@ -1451,4 +1450,4 @@ "wallet_balance": "Zůstatek peněženky", "wc_desc": "Snadno se připojte k dApps", "yes_exit": "Ano. Ukončit" -} \ No newline at end of file +} diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2b8d0b5e7..9a39f7bda 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -530,7 +530,7 @@ "i_understand": "Ich verstehe", "icon_style": "Symbolstil", "id": "ID: ", - "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Ledger eingeschaltet und entsperrt ist!", + "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Gerät eingeschaltet und entsperrt ist!", "ignore": "Ignorieren", "import": "Importieren", "importNFTs": "NFTs importieren", @@ -882,6 +882,9 @@ "restore_wallet": "Wallet wiederherstellen", "restore_wallet_name": "Wallet-Name", "restore_wallet_restore_description": "Beschreibung der Wallet-Wiederherstellung", + "resync_device": "Gerät resyncen", + "resync_device_desc": "Um das Guthaben aktuell zu halten muss Cake Wallet gelegentlich sich mit dem Gerät synchonisieren.", + "resync_device_cancel_warning_desc": "Dadurch wird der laufende Synchronisierungsprozess mit Ihrem Hardware-Gerät beendet.", "return_address_desc": "Im seltenen Fall eines fehlgeschlagenen Swaps können unsere Partner Ihre Zahlung automatisch an die angegebene Rückerstattungsadresse erstatten.", "return_address_desc_external": "Da Sie „Von extern senden“ ausgewählt haben, legen Sie bitte unten manuell eine Erstattungsadresse fest.", "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrer Debitkarte, Ihrem Bankkonto oder Ihrem Robinhood-Guthaben. Nur USA.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d81f3dd52..30e506aef 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -544,11 +544,11 @@ "how_to_connect": "How to Connect", "how_to_use": "How to use", "how_to_use_card": "How to use this card", - "hww_exit_desc": "This will ed the ongoing pairing process with your hardware device", + "hww_exit_desc": "This will end the ongoing pairing process with your hardware device", "i_understand": "I understand", "icon_style": "Icon style", "id": "ID: ", - "if_you_dont_see_your_device": "If you don't see your device above, please be sure your Ledger is awake and unlocked!", + "if_you_dont_see_your_device": "If you don't see your device above, please be sure your device is awake and unlocked!", "ignor": "Ignore", "import": "Import", "importNFTs": "Import NFTs", @@ -926,6 +926,9 @@ "restore_wallet": "Restore Wallet", "restore_wallet_name": "Wallet name", "restore_wallet_restore_description": "Wallet restore description", + "resync_device": "Resync device", + "resync_device_desc": "Balance updating requires occasional resyncing Cake Wallet with the device.", + "resync_device_cancel_warning_desc": "This will end the ongoing sync process with your hardware device", "return_address_desc": "In the rare case of a failed swap, our partners can automatically refund your payment to the provided refund address.", "return_address_desc_external": "Since you chose Send From External, please set a refund address manually below.", "robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 91c41c6f2..4f3590fb7 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -529,7 +529,6 @@ "i_understand": "Entiendo", "icon_style": "Estilo de icono", "id": "ID: ", - "if_you_dont_see_your_device": "Si no ves tu dispositivo arriba, asegúrate de que tu Ledger esté encendido y desbloqueado.", "ignor": "Ignorar", "import": "Importar", "importNFTs": "Importar NFTs", @@ -1454,4 +1453,4 @@ "wallet_balance": "Saldo de la cartera", "wc_desc": "Conéctate fácilmente a dApps", "yes_exit": "Sí. Salir" -} \ No newline at end of file +} diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index 40b83e118..6a15e0c9a 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -527,7 +527,6 @@ "i_understand": "متوجه شدم", "icon_style": "سبک آیکون", "id": "شناسه: ", - "if_you_dont_see_your_device": "اگر دستگاه خود را در بالا نمی‌بینید، لطفاً مطمئن شوید که Ledger شما روشن است و قفل آن باز شده است!", "ignor": "نادیده گرفتن", "import": "درون‌ریزی", "importNFTs": "وارد کردن NFTها", @@ -1451,4 +1450,4 @@ "wallet_balance": "موجودی کیف پول", "wc_desc": "به‌راحتی به dAppها متصل شوید", "yes_exit": "بله. خروج" -} \ No newline at end of file +} diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 5327d2029..397529fb1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -529,7 +529,6 @@ "i_understand": "Je comprends", "icon_style": "Style d'icône", "id": "ID : ", - "if_you_dont_see_your_device": "Si vous ne voyez pas votre appareil ci‑dessus, assurez‑vous que votre Ledger est allumé et déverrouillé !", "ignor": "Ignorer", "import": "Importer", "importNFTs": "Importer des NFTs", @@ -1451,4 +1450,4 @@ "wallet_balance": "Solde du portefeuille", "wc_desc": "Connectez-vous facilement aux dApps", "yes_exit": "Oui. Quitter" -} \ No newline at end of file +} diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 693fe80b0..7cb149af0 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -1285,7 +1285,6 @@ "hide": "Mokañy", "how_to_connect": "Mba'éichapa Oñembojoaju", "hww_exit_desc": "Kóva omboty pe joaju oikóva ne hardware mba'e'oka ndive", - "if_you_dont_see_your_device": "Ndehecháiramo ne mba'e'oka yvate, ehecha porãke ne Ledger hesãi ha ipe'apyre!", "incorrect_seed_option": "Hekope'ỹ. Eha'ã jey", "incorrect_seed_option_back": "Hekope'ỹ. Ehecha porãke ne seed oñeñongatu hekopete ha eha'ã jey.", "input": "Jeikeha (input)", @@ -1459,4 +1458,4 @@ "you_will_receive_estimated_amount": "Rehupytýta (jehaipyre)", "youCanGoBackToYourDapp": "Ikatúma rejevy ne dApp-pe", "your": "Nde" -} \ No newline at end of file +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index b4c142360..db0b67879 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -529,7 +529,6 @@ "i_understand": "Na fahimta", "icon_style": "Salon icon", "id": "ID: ", - "if_you_dont_see_your_device": "Idan ba ka ga na'urarka a sama ba, da fatan ka tabbatar Ledger ɗinka yana kunne kuma a buɗe!", "ignor": "Yi watsi", "import": "Shigo da", "importNFTs": "Shigo da NFT", @@ -1454,4 +1453,4 @@ "wallet_balance": "Ma'aunin walat", "wc_desc": "Haɗa da DApps cikin sauƙi", "yes_exit": "Eh. Fita" -} \ No newline at end of file +} diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 6f21d5d86..413a19131 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -529,7 +529,6 @@ "i_understand": "मैं समझता/समझती हूँ", "icon_style": "चिह्न शैली", "id": "ID: ", - "if_you_dont_see_your_device": "यदि आपको ऊपर अपना डिवाइस नहीं दिख रहा है, तो कृपया सुनिश्चित करें कि आपका Ledger चालू है और अनलॉक है!", "ignor": "नज़रअंदाज़ करें", "import": "इम्पोर्ट", "importNFTs": "NFTs आयात करें", @@ -1453,4 +1452,4 @@ "wallet_balance": "वॉलेट बैलेंस", "wc_desc": "DApps से आसानी से कनेक्ट करें", "yes_exit": "हाँ। बाहर निकलें" -} \ No newline at end of file +} diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index eb66c8e61..11a9ffe88 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -529,7 +529,6 @@ "i_understand": "Razumijem", "icon_style": "Stil ikone", "id": "ID: ", - "if_you_dont_see_your_device": "Ako gore ne vidite svoj uređaj, provjerite je li vaš Ledger uključen i otključan!", "ignor": "Zanemari", "import": "Uvoz", "importNFTs": "Uvezi NFT-ove", @@ -1451,4 +1450,4 @@ "wallet_balance": "Stanje novčanika", "wc_desc": "Jednostavno se povežite s DApp-ovima", "yes_exit": "Da. Izađi" -} \ No newline at end of file +} diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index c81c4e463..0d0c47ef5 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -529,7 +529,6 @@ "i_understand": "Հասկացա", "icon_style": "Սրբապատկերների ոճը", "id": "ID: ", - "if_you_dont_see_your_device": "Եթե վերևում չեք տեսնում ձեր սարքը, համոզվեք, որ ձեր Ledger-ը միացված է և ապակողպված է։", "ignor": "Անտեսել", "import": "Ներմուծում", "importNFTs": "Ներմուծել NFT-ներ", @@ -1451,4 +1450,4 @@ "wc_action_approve": "Հաստատել", "wc_desc": "Հեշտությամբ միացեք DApps-երին", "yes_exit": "Այո։ Դուրս գալ" -} \ No newline at end of file +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 877fac35f..ae2e5ca6b 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -529,7 +529,6 @@ "i_understand": "Saya mengerti", "icon_style": "Gaya ikon", "id": "ID: ", - "if_you_dont_see_your_device": "Jika Anda tidak melihat perangkat Anda di atas, pastikan Ledger Anda menyala dan tidak terkunci!", "ignor": "Abaikan", "import": "Impor", "importNFTs": "Impor NFT", @@ -1454,4 +1453,4 @@ "wc_action_approve": "Setujui", "wc_desc": "Hubungkan dengan mudah ke DApps", "yes_exit": "Ya. Keluar" -} \ No newline at end of file +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 2bd228544..7997f91d2 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -529,7 +529,6 @@ "i_understand": "Ho capito", "icon_style": "Stile dell'icona", "id": "ID: ", - "if_you_dont_see_your_device": "Se non vedi il tuo dispositivo qui sopra, assicurati che il tuo Ledger sia acceso e sbloccato!", "ignor": "Ignora", "import": "Importa", "importNFTs": "Importa NFT", @@ -1454,4 +1453,4 @@ "wc_action_approve": "Approva", "wc_desc": "Connettiti facilmente alle dApp", "yes_exit": "Sì. Esci" -} \ No newline at end of file +} diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 87f1193ca..f8ab953de 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -530,7 +530,6 @@ "i_understand": "理解しました", "icon_style": "アイコンのスタイル", "id": "ID: ", - "if_you_dont_see_your_device": "上記にデバイスが表示されない場合は、Ledgerが起動していてロック解除されていることを確認してください!", "ignor": "無視", "import": "インポート", "importNFTs": "NFTをインポート", @@ -1452,4 +1451,4 @@ "wc_action_approve": "承認", "wc_desc": "dApp に簡単に接続", "yes_exit": "はい。終了する" -} \ No newline at end of file +} diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index b756745bb..299814e60 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -529,7 +529,6 @@ "i_understand": "이해했습니다", "icon_style": "아이콘 스타일", "id": "ID: ", - "if_you_dont_see_your_device": "위에서 기기가 보이지 않으면 Ledger가 켜져 있고 잠금 해제되어 있는지 확인하세요!", "ignor": "무시", "import": "가져오기", "importNFTs": "NFT 가져오기", @@ -1451,4 +1450,4 @@ "wc_action_approve": "승인", "wc_desc": "DApp에 손쉽게 연결", "yes_exit": "예. 종료" -} \ No newline at end of file +} diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 51f7c231f..d58b1add9 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -529,7 +529,6 @@ "i_understand": "နားလည်ပါတယ်", "icon_style": "အိုင်ကွန်စတိုင်", "id": "ID: ", - "if_you_dont_see_your_device": "အထက်မှာ သင့်စက်ကို မတွေ့ပါက သင့် Ledger ကို ဖွင့်ထားပြီး လော့ခ်ဖွင့်ထားကြောင်း သေချာပါစေ။", "ignor": "လျစ်လျူရှုပါ", "import": "တင်သွင်း", "importNFTs": "NFT များကို တင်သွင်းပါ", @@ -1451,4 +1450,4 @@ "wc_action_approve": "အတည်ပြု", "wc_desc": "dApp များသို့ လွယ်ကူစွာ ချိတ်ဆက်ပါ", "yes_exit": "ဟုတ်ကဲ့။ ထွက်ပါ" -} \ No newline at end of file +} diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9285015d8..d538ef215 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -527,7 +527,6 @@ "i_understand": "Ik begrijp het", "icon_style": "Pictogramstijl", "id": "ID: ", - "if_you_dont_see_your_device": "Als je je apparaat hierboven niet ziet, zorg er dan voor dat je Ledger ontgrendeld is!", "ignor": "Negeren", "import": "Importeren", "importNFTs": "NFT's importeren", @@ -1452,4 +1451,4 @@ "wc_action_approve": "Goedkeuren", "wc_desc": "Maak eenvoudig verbinding met dApps", "yes_exit": "Ja. Afsluiten" -} \ No newline at end of file +} diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 16045cd4b..530163022 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -528,7 +528,6 @@ "i_understand": "Rozumiem", "icon_style": "Styl ikony", "id": "ID: ", - "if_you_dont_see_your_device": "Jeśli nie widzisz swojego urządzenia powyżej, upewnij się, że Twój Ledger jest aktywny i odblokowany!", "ignor": "Ignoruj", "import": "Importuj", "importNFTs": "Importuj NFT", @@ -1451,4 +1450,4 @@ "wc_action_approve": "Zatwierdź", "wc_desc": "Łatwo łącz się z aplikacjami dApp", "yes_exit": "Tak. Wyjdź" -} \ No newline at end of file +} diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 5ef972b66..e357dc89d 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -529,7 +529,6 @@ "i_understand": "Eu entendo", "icon_style": "Estilo do ícone", "id": "ID: ", - "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, verifique se o seu Ledger está ligado e desbloqueado!", "ignor": "Ignorar", "import": "Importar", "importNFTs": "Importar NFTs", @@ -1454,4 +1453,4 @@ "wc_action_approve": "Aprovar", "wc_desc": "Ligue-se facilmente a DApps", "yes_exit": "Sim. Sair" -} \ No newline at end of file +} diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb index ea1a3a4ab..215136300 100644 --- a/res/values/strings_pt_BR.arb +++ b/res/values/strings_pt_BR.arb @@ -524,7 +524,6 @@ "i_understand": "Eu entendo", "icon_style": "Estilo do ícone", "id": "ID: ", - "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, certifique-se de que seu Ledger esteja ligado e desbloqueado!", "ignor": "Ignorar", "import": "Importar", "importNFTs": "Importar NFTs", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index aaac422dd..9ce39e077 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -529,7 +529,6 @@ "i_understand": "Я понимаю", "icon_style": "Стиль иконок", "id": "ID: ", - "if_you_dont_see_your_device": "Если вы не видите свое устройство выше, пожалуйста, убедитесь, что ваш Ledger включен и разблокирован!", "ignor": "Игнорировать", "import": "Импорт", "importNFTs": "Импортировать NFT", @@ -1452,4 +1451,4 @@ "wc_action_approve": "Одобрить", "wc_desc": "Легко подключайтесь к dApps", "yes_exit": "Да. Выйти" -} \ No newline at end of file +} diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 669153910..9c575933f 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -529,7 +529,6 @@ "i_understand": "ฉันเข้าใจ", "icon_style": "สไตล์ไอคอน", "id": "ID: ", - "if_you_dont_see_your_device": "หากคุณไม่เห็นอุปกรณ์ของคุณด้านบน โปรดตรวจสอบให้แน่ใจว่า Ledger ของคุณเปิดอยู่และปลดล็อกแล้ว!", "ignor": "เพิกเฉย", "import": "นำเข้า", "importNFTs": "นำเข้า NFT", @@ -1451,4 +1450,4 @@ "wc_action_approve": "อนุมัติ", "wc_desc": "เชื่อมต่อกับ DApps ได้อย่างง่ายดาย", "yes_exit": "ใช่ ออก" -} \ No newline at end of file +} diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 8e23566a4..91b49c2e8 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -529,7 +529,6 @@ "i_understand": "Naiintindihan ko", "icon_style": "Estilo ng icon", "id": "ID: ", - "if_you_dont_see_your_device": "Kung hindi mo nakikita ang iyong device sa itaas, pakitiyak na gising at naka-unlock ang iyong Ledger!", "ignor": "Huwag pansinin", "import": "I-import", "importNFTs": "Mag-import ng mga NFT", @@ -1451,4 +1450,4 @@ "wc_action_approve": "Aprubahan", "wc_desc": "Madaling kumonekta sa mga DApp", "yes_exit": "Oo. Lumabas" -} \ No newline at end of file +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index beb1e6f02..765fa2f15 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -529,7 +529,6 @@ "i_understand": "Anladım", "icon_style": "Simge stili", "id": "ID: ", - "if_you_dont_see_your_device": "Cihazınızı yukarıda görmüyorsanız, lütfen Ledger'ınızın açık, uyanık ve kilidinin açılmış olduğundan emin olun!", "ignor": "Yoksay", "import": "İçe aktar", "importNFTs": "NFT'leri İçe Aktar", @@ -1451,4 +1450,4 @@ "wc_action_approve": "Onayla", "wc_desc": "DApp'lere kolayca bağlanın", "yes_exit": "Evet. Çık" -} \ No newline at end of file +} diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 53fee6efb..f9421be02 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -529,7 +529,6 @@ "i_understand": "Я розумію", "icon_style": "Стиль значка", "id": "ID: ", - "if_you_dont_see_your_device": "Якщо ви не бачите свій пристрій вище, будь ласка, переконайтеся, що ваш Ledger увімкнений і розблокований!", "ignor": "Ігнорувати", "import": "Імпорт", "importNFTs": "Імпорт NFT", @@ -1452,4 +1451,4 @@ "wc_action_approve": "Схвалити", "wc_desc": "Легко підключайтеся до dApp", "yes_exit": "Так. Вийти" -} \ No newline at end of file +} diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index b28375292..89c767ea8 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -529,7 +529,6 @@ "i_understand": "میں سمجھ گیا/گئی ہوں", "icon_style": "آئیکن اسٹائل", "id": "ID: ", - "if_you_dont_see_your_device": "اگر آپ کو اوپر اپنا ڈیوائس نظر نہیں آ رہا، تو براہِ کرم یقینی بنائیں کہ آپ کا Ledger آن ہے اور اَن لاک ہے!", "ignor": "نظر انداز کریں", "import": "درآمد کریں", "importNFTs": "NFTs درآمد کریں", @@ -1453,4 +1452,4 @@ "wc_action_approve": "منظور کریں", "wc_desc": "DApps سے آسانی سے کنیکٹ کریں", "yes_exit": "ہاں۔ باہر جائیں" -} \ No newline at end of file +} diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index c5eb9647b..6963c9a44 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -528,7 +528,6 @@ "i_understand": "Tôi hiểu", "icon_style": "Kiểu biểu tượng", "id": "ID: ", - "if_you_dont_see_your_device": "Nếu bạn không thấy thiết bị của mình ở trên, hãy đảm bảo rằng Ledger của bạn đang bật và đã được mở khóa!", "ignor": "Bỏ qua", "import": "Nhập", "importNFTs": "Nhập NFT", @@ -1452,4 +1451,4 @@ "wc_action_approve": "Phê duyệt", "wc_desc": "Dễ dàng kết nối với các dApp", "yes_exit": "Có. Thoát" -} \ No newline at end of file +} diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 089e19207..8a073f320 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -530,7 +530,6 @@ "i_understand": "Mo ye mi", "icon_style": "Aami ara", "id": "ID: ", - "if_you_dont_see_your_device": "Ti o ko ba ri ẹrọ rẹ loke, jọwọ rii daju pe Ledger rẹ wa ni titan ati pe o ti ṣí i silẹ!", "ignor": "Foju kọ", "import": "Gbe wọle", "importNFTs": "Gbe NFTs wọle", @@ -1452,4 +1451,4 @@ "wc_action_approve": "Fọwọ́sí", "wc_desc": "So pọ̀ mọ́ àwọn DApp ní rọrùn", "yes_exit": "Bẹ́ẹ̀ni. Jáde" -} \ No newline at end of file +} diff --git a/test/new-ui/widgets/new_future_primary_button_test.dart b/test/new-ui/widgets/new_future_primary_button_test.dart new file mode 100644 index 000000000..858dc5fda --- /dev/null +++ b/test/new-ui/widgets/new_future_primary_button_test.dart @@ -0,0 +1,265 @@ +import "dart:async"; + +import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:flutter_test/flutter_test.dart"; + +const _svg = ''; + +void main() { + Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); + + NewPrimaryButton inner(WidgetTester tester) => + tester.widget(find.byType(NewPrimaryButton)); + + Future? fire(WidgetTester tester) { + final result = (inner(tester).onPressed as dynamic)(); + return result is Future ? result : null; + } + + Widget build({ + required Future Function() onPressed, + bool disabled = false, + SvgPicture? image, + Color color = Colors.blue, + Color textColor = Colors.white, + Color borderColor = Colors.transparent, + String text = "Send", + }) => + wrap( + NewFuturePrimaryButton( + onPressed: onPressed, + text: text, + color: color, + textColor: textColor, + borderColor: borderColor, + disabled: disabled, + image: image, + ), + ); + + group("NewFuturePrimaryButton", () { + group("prop forwarding", () { + testWidgets("passes every prop through to NewPrimaryButton", (tester) async { + final image = SvgPicture.string(_svg); + + await tester.pumpWidget( + build( + onPressed: () async {}, + text: "Confirm", + color: Colors.green, + textColor: Colors.black, + borderColor: Colors.red, + disabled: true, + image: image, + ), + ); + + final button = inner(tester); + expect(button.text, "Confirm"); + expect(button.color, Colors.green); + expect(button.textColor, Colors.black); + expect(button.borderColor, Colors.red); + expect(button.disabled, isTrue); + expect(button.image, same(image)); + }); + + testWidgets("defaults: transparent border, not disabled, no image", (tester) async { + await tester.pumpWidget( + wrap( + NewFuturePrimaryButton( + onPressed: () async {}, + text: "Send", + color: Colors.blue, + textColor: Colors.white, + ), + ), + ); + + final button = inner(tester); + expect(button.borderColor, Colors.transparent); + expect(button.disabled, isFalse); + expect(button.image, isNull); + }); + + testWidgets("starts out not loading", (tester) async { + await tester.pumpWidget(build(onPressed: () async {})); + + expect(inner(tester).isLoading, isFalse); + }); + }); + + group("loading lifecycle", () { + testWidgets("flips isLoading true while pending, false once resolved", (tester) async { + final completer = Completer(); + await tester.pumpWidget(build(onPressed: () => completer.future)); + + final pressed = fire(tester); + await tester.pump(); + expect( + inner(tester).isLoading, + isTrue, + reason: "should be loading while the future is in flight", + ); + + completer.complete(); + await pressed; + await tester.pump(); + expect(inner(tester).isLoading, isFalse); + }); + + testWidgets("synchronous-ish callback still settles back to not loading", (tester) async { + var calls = 0; + await tester.pumpWidget(build(onPressed: () async => calls++)); + + await fire(tester); + await tester.pump(); + + expect(calls, 1); + expect(inner(tester).isLoading, isFalse); + }); + + testWidgets("invokes the callback exactly once per press", (tester) async { + var calls = 0; + await tester.pumpWidget(build(onPressed: () async => calls++)); + + await fire(tester); + await tester.pump(); + await fire(tester); + await tester.pump(); + + expect(calls, 2); + }); + }); + + testWidgets("ignores presses while a previous future is pending", (tester) async { + final completer = Completer(); + var calls = 0; + await tester.pumpWidget( + build( + onPressed: () { + calls++; + return completer.future; + }, + ), + ); + + final pressed = fire(tester); + await tester.pump(); + + unawaited(fire(tester)); + unawaited(fire(tester)); + unawaited(fire(tester)); + await tester.pump(); + + expect(calls, 1, reason: "extra presses must be swallowed"); + + completer.complete(); + await pressed; + await tester.pump(); + + await fire(tester); + await tester.pump(); + expect(calls, 2); + }); + + group("handle errors", () { + testWidgets("clears the loading flag when the callback throws", (tester) async { + await tester.pumpWidget( + build(onPressed: () async => throw StateError("boom")), + ); + + await expectLater(fire(tester), throwsA(isA())); + await tester.pump(); + + expect(inner(tester).isLoading, isFalse, reason: "the finally block must reset the flag"); + }); + + testWidgets("rethrows instead of swallowing the failure", (tester) async { + await tester.pumpWidget( + build(onPressed: () async => throw StateError("boom")), + ); + + await expectLater(fire(tester), throwsA(isA())); + }); + + testWidgets("stays pressable after the callback throws", (tester) async { + var calls = 0; + await tester.pumpWidget( + build( + onPressed: () { + calls++; + throw StateError("boom"); + }, + ), + ); + + await expectLater(fire(tester), throwsA(isA())); + await tester.pump(); + await expectLater(fire(tester), throwsA(isA())); + await tester.pump(); + + expect(calls, 2, reason: "the re-entrancy guard must not latch on failure"); + expect(inner(tester).isLoading, isFalse); + }); + }); + + group("disposal mid-flight", () { + testWidgets("does not setState after being disposed", (tester) async { + final completer = Completer(); + await tester.pumpWidget(build(onPressed: () => completer.future)); + + final pressed = fire(tester); + await tester.pump(); + + await tester.pumpWidget(wrap(const SizedBox.shrink())); + expect(find.byType(NewPrimaryButton), findsNothing); + + completer.complete(); + await pressed; + await tester.pump(); + + expect( + tester.takeException(), + isNull, + reason: "the mounted check must guard the second setState", + ); + }); + + testWidgets("still propagates a throw when disposed", (tester) async { + final completer = Completer(); + await tester.pumpWidget(build(onPressed: () => completer.future)); + + final pressed = fire(tester); + await tester.pump(); + await tester.pumpWidget(wrap(const SizedBox.shrink())); + + completer.completeError(StateError("boom")); + + await expectLater(pressed, throwsA(isA())); + expect( + tester.takeException(), + isNull, + reason: "no setState-after-dispose FlutterError on the error path", + ); + }); + }); + + testWidgets("disabled is forwarded but not enforced by this wrapper", (tester) async { + // Press-blocking lives in NewPrimaryButton; this widget only relays the + // flag. If that ever moves up here, this expectation should change. + var calls = 0; + await tester.pumpWidget( + build(onPressed: () async => calls++, disabled: true), + ); + + expect(inner(tester).disabled, isTrue); + + await fire(tester); + await tester.pump(); + expect(calls, 1); + }); + }); +} diff --git a/tool/configure.dart b/tool/configure.dart index 66c302799..022eaa2cd 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -442,6 +442,8 @@ abstract class Monero { Map exportOutputsUR(Object wallet); bool needExportOutputs(Object wallet, Money amount); + + bool hasUnknownKeyImages(Object wallet); bool importKeyImagesUR(Object wallet, String ur);