From ed540bae04e3f0c7e911de14a33402ae5d8b36ee Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 16 Jul 2026 20:39:49 +0200 Subject: [PATCH] CW-1291-ledger-fixes (#3390) * feat: add connecting indicator in hardware wallet connect screen * chore: bump ledger related dependencies * chore: bump ledger related dependencies * feat: add additional guard against gLedger being null * auto-reformat * feat: improve ledger connection and state management * fix: connect_device_page padding on iOS * chore: update universal_ble dependency reference --------- Co-authored-by: Robert Malikowski Co-authored-by: Omar Hatem --- lib/di.dart | 11 +- lib/monero/cw_monero.dart | 3 + .../on_authentication_state_change.dart | 35 +- .../connect_device/connect_device_page.dart | 357 ++++++++++-------- lib/store/app_store.dart | 47 +-- .../hardware_wallet/bitbox_view_model.dart | 15 +- .../hardware_wallet_view_model.dart | 13 +- .../hardware_wallet/ledger_view_model.dart | 188 +++++---- .../trezor_connect_view_model.dart | 12 +- .../wallet_list/wallet_list_view_model.dart | 3 +- pubspec_base.yaml | 14 +- tool/configure.dart | 1 + 12 files changed, 417 insertions(+), 282 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 1415bf045..6bb8bd98c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -421,16 +421,19 @@ Future setup({ case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: throw Exception( - "This should not have happened, because airgapped Wallets don't need View Models"); + "This should not have happened, because airgapped Wallets don't need View Models", + ); } }); - getIt.registerLazySingleton(() => LedgerViewModel()); + getIt.registerLazySingleton(() => LedgerViewModel(getIt())); - getIt.registerLazySingleton(() => BitboxViewModel()); + getIt.registerLazySingleton(BitboxViewModel.new); getIt.registerLazySingleton( - () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet")); + () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"), + ); + getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt())); getIt.registerFactory(() => KeyService(getIt.get())); diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index e6ddabaec..1551b95de 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -433,6 +433,9 @@ class CWMonero extends Monero { @override void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection; + @override + bool hasGlobalLedgerConnection() => gLedger != null; + @override String? getLastLedgerCommand() => latestLedgerCommand; diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 49a3c61c9..02ddd5639 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -75,6 +75,23 @@ void startAuthenticationStateChange( ); return; } + + ledgerVM.suppressMoneroReconnectPrompt = true; + + if (!monero!.hasGlobalLedgerConnection()) { + await showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: "Unexpected Error", + alertContent: + "Unexpected Error while trying to connect to the device (has no global LedgerConnection)", + buttonText: S.of(context).try_again, + buttonAction: Navigator.of(context).pop, + ), + ); + return; + } + showPopUp( context: context, builder: (context) => AlertWithOneAction( @@ -85,14 +102,17 @@ void startAuthenticationStateChange( buttonAction: () => Navigator.of(context).pop(), ), ); - bool tryOpening = true; + + var isConnected = false; + var tryOpening = true; while (tryOpening) { try { await loadCurrentWallet(); + isConnected = true; tryOpening = false; } on Exception catch (e) { final ledgerErrorMessage = ledgerVM.interpretErrorCode(e.toString()); - if (ledgerErrorMessage != null) { + if (ledgerErrorMessage != null && context.mounted) { await showPopUp( context: context, builder: (context) => AlertWithTwoActions( @@ -116,16 +136,17 @@ void startAuthenticationStateChange( } getIt.get().showNext(); - await navigatorKey.currentState! - .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + if (isConnected) { + ledgerVM.suppressMoneroReconnectPrompt = false; + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + } }, allowChangeWallet: true, ), ); - - // await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice)); } else { - navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); } WidgetsBinding.instance.addPostFrameCallback((_) { final linkViewModel = getIt.get(); diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index c96ec27bc..21cbd0292 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -1,54 +1,54 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:io"; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/themes/core/material_base_theme.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/base_page.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/themes/core/material_base_theme.dart"; +import "package:cake_wallet/utils/responsive_layout_util.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel); class ConnectDevicePageParams { - final WalletType walletType; - final OnConnectDevice onConnectDevice; - final bool allowChangeWallet; - final bool isReconnect; - final HardwareWalletType hardwareWalletType; - - ConnectDevicePageParams({ + const ConnectDevicePageParams({ required this.walletType, required this.hardwareWalletType, required this.onConnectDevice, this.allowChangeWallet = false, this.isReconnect = true, }); -} -class ConnectDevicePage extends BasePage { final WalletType walletType; final OnConnectDevice onConnectDevice; final bool allowChangeWallet; final bool isReconnect; - final HardwareWalletViewModel hardwareWalletVM; + final HardwareWalletType hardwareWalletType; +} +class ConnectDevicePage extends BasePage { ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM) : walletType = params.walletType, onConnectDevice = params.onConnectDevice, allowChangeWallet = params.allowChangeWallet, isReconnect = params.isReconnect; + final WalletType walletType; + final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; + final bool isReconnect; + final HardwareWalletViewModel hardwareWalletVM; @override String get title => isReconnect @@ -60,31 +60,32 @@ class ConnectDevicePage extends BasePage { @override Widget body(BuildContext context) => PopScope( - canPop: !isReconnect, - child: ConnectDevicePageBody( - walletType, - onConnectDevice, - allowChangeWallet, - hardwareWalletVM, - currentTheme, - )); + canPop: !isReconnect, + child: ConnectDevicePageBody( + walletType, + onConnectDevice, + hardwareWalletVM, + currentTheme, + allowChangeWallet: allowChangeWallet, + ), + ); } class ConnectDevicePageBody extends StatefulWidget { + const ConnectDevicePageBody( + this.walletType, + this.onConnectDevice, + this.hardwareWalletVM, + this.currentTheme, { + this.allowChangeWallet = false, + }); + final WalletType walletType; final OnConnectDevice onConnectDevice; final bool allowChangeWallet; final HardwareWalletViewModel hardwareWalletVM; final MaterialThemeBase currentTheme; - const ConnectDevicePageBody( - this.walletType, - this.onConnectDevice, - this.allowChangeWallet, - this.hardwareWalletVM, - this.currentTheme, - ); - @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); } @@ -95,10 +96,10 @@ class ConnectDevicePageBodyState extends State { List get allDevices => [...bleDevices, ...usbDevices]; - late Timer? _usbRefreshTimer = null; - late Timer? _bleRefreshTimer = null; - late Timer? _bleStateTimer = null; - late StreamSubscription? _bleRefresh = null; + Timer? _usbRefreshTimer; + Timer? _bleRefreshTimer; + Timer? _bleStateTimer; + StreamSubscription? _bleRefresh; bool longWait = false; Timer? _longWaitTimer; @@ -107,19 +108,22 @@ class ConnectDevicePageBodyState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _bleStateTimer = - Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState()); + _bleStateTimer = Timer.periodic( + const Duration(seconds: 1), + (_) => widget.hardwareWalletVM.updateBleState(), + ); - _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + _bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices()); if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + _usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices()); } if (widget.hardwareWalletVM.hasBluetooth) { - _longWaitTimer = Timer(Duration(seconds: 10), () { - if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) + _longWaitTimer = Timer(const Duration(seconds: 10), () { + if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) { setState(() => longWait = true); + } }); } }); @@ -134,22 +138,29 @@ class ConnectDevicePageBodyState extends State { _longWaitTimer?.cancel(); widget.hardwareWalletVM.stopScanning(); + _isConnectPressed = false; super.dispose(); } Future _refreshUsbDevices() async { final dev = await widget.hardwareWalletVM.getAllUsbDevices(); - if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + + if (usbDevices.length != dev.length) { + setState(() => usbDevices = dev); + } } Future _refreshBleDevices() async { try { if (widget.hardwareWalletVM.isBleEnabled) { - _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() { - bleDevices.add(device); - if (longWait) longWait = false; - })) - ..onError((Object e) { + _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen( + (device) => setState(() { + bleDevices.add(device); + if (longWait) { + longWait = false; + } + }), + )..onError((e) { printV(e); }); _bleRefreshTimer?.cancel(); @@ -160,48 +171,66 @@ class ConnectDevicePageBodyState extends State { } } - Future _connectToDevice(HardwareWalletDevice device) async { - final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); + var _isConnectPressed = false; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (isConnected) - widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); - }); + Future _connectToDevice(HardwareWalletDevice device) async { + if (_isConnectPressed) { + return; + } + + _isConnectPressed = true; + try { + final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); + _isConnectPressed = false; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isConnected) { + widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); + } + }); + } catch (_) { + _isConnectPressed = false; + } } String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) { + printV(deviceType); switch (deviceType) { case HardwareWalletDeviceType.ledgerNanoX: - return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; case HardwareWalletDeviceType.ledgerNanoGen5: - return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg"; case HardwareWalletDeviceType.ledgerStax: - return 'assets/new-ui/hardware_wallets/ledger_stax.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_stax.svg"; case HardwareWalletDeviceType.ledgerFlex: - return 'assets/new-ui/hardware_wallets/ledger_flex.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_flex.svg"; case HardwareWalletDeviceType.BitBox02: case HardwareWalletDeviceType.BitBox02Nova: - return 'assets/new-ui/hardware_wallets/device_bitbox.svg'; + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; case HardwareWalletDeviceType.trezorModelOne: - return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg"; case HardwareWalletDeviceType.trezorModelT: - return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg"; case HardwareWalletDeviceType.trezorSafe3: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg"; case HardwareWalletDeviceType.trezorSafe5: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletDeviceType.trezorSafe7: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; default: - return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; } } String get description { - if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble; + if (!Platform.isAndroid) { + return S.of(context).connect_your_hardware_wallet_ble; + } - if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet; + if (widget.hardwareWalletVM.hasBluetooth) { + return S.of(context).connect_your_hardware_wallet; + } return S.of(context).connect_your_hardware_wallet_usb; } @@ -210,7 +239,7 @@ class ConnectDevicePageBodyState extends State { Widget build(BuildContext context) => Center( child: Container( width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, - padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24), child: Column( children: [ Expanded( @@ -218,9 +247,7 @@ class ConnectDevicePageBodyState extends State { child: Column( spacing: 24, children: [ - CakeImageWidget( - imageUrl: "assets/new-ui/hardware_wallet.svg", - ), + const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"), Text( description, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), @@ -231,7 +258,7 @@ class ConnectDevicePageBodyState extends State { offstage: widget.hardwareWalletVM.isBleEnabled || !widget.hardwareWalletVM.hasBluetooth, child: Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), child: Text( S.of(context).ledger_please_enable_bluetooth, style: Theme.of(context).textTheme.titleMedium, @@ -242,67 +269,89 @@ class ConnectDevicePageBodyState extends State { ), Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(18)), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), child: ListView.separated( - shrinkWrap: true, - itemBuilder: (context, index) { - final item = allDevices[index]; - return GestureDetector( - onTap: () => _connectToDevice(item), - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: 48, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - spacing: 12, - children: [ - CakeImageWidget( - imageUrl: _getDeviceTileLeading(item.type), - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn), - ), - Text(item.name) - ], - ), - Row( - spacing: 12, - children: [ - if (item.connectionType == - HardwareWalletConnectionType.ble) - CakeImageWidget( - imageUrl: "assets/new-ui/bluetooth.svg", + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + final item = allDevices[index]; + return GestureDetector( + onTap: () => _connectToDevice(item), + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + Observer( + builder: (_) { + if (widget.hardwareWalletVM.isConnecting) { + return CupertinoActivityIndicator( + animating: true, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + radius: 12, + ); + } + + return CakeImageWidget( + imageUrl: _getDeviceTileLeading(item.type), width: 24, height: 24, colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn), - ), + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ); + }, + ), + Text(item.name), + ], + ), + Row( + spacing: 12, + children: [ + if (item.connectionType == + HardwareWalletConnectionType.ble) CakeImageWidget( - imageUrl: "assets/new-ui/arrow_forward.svg", - height: 16, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn)) - ], - ) - ], - ), + imageUrl: "assets/new-ui/bluetooth.svg", + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_forward.svg", + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ], + ), + ], ), ), - ); - }, - separatorBuilder: (context, index) => Container( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh), - itemCount: allDevices.length), + ), + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + itemCount: allDevices.length, + ), ), if (widget.allowChangeWallet) ...[ PrimaryButton( @@ -310,7 +359,7 @@ class ConnectDevicePageBodyState extends State { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, onPressed: _onChangeWallet, - ) + ), ], ], ), @@ -321,7 +370,7 @@ class ConnectDevicePageBodyState extends State { color: Theme.of(context).colorScheme.surfaceContainer, textColor: Theme.of(context).colorScheme.primary, onPressed: () => _onHowToConnect(context), - ) + ), ], ), ), @@ -339,17 +388,25 @@ class ConnectDevicePageBodyState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet( + builder: (_) => InfoStepsBottomSheet( titleText: S.of(context).how_to_connect, steps: [ - InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg', - S.of(context).connect_hw_info_step_1), - InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg', - S.of(context).connect_hw_info_step_2), - InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg', - S.of(context).connect_hw_info_step_3), - InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg', - S.of(context).connect_hw_info_step_4), + InfoStep( + "assets/images/wallet_connect_step_icons/step1_power.svg", + S.of(context).connect_hw_info_step_1, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step2_connect.svg", + S.of(context).connect_hw_info_step_2, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step3_unlock.svg", + S.of(context).connect_hw_info_step_3, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step4_select.svg", + S.of(context).connect_hw_info_step_4, + ), ], ), ); diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index b3ac158d9..85a9bbba5 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,25 +1,25 @@ -import 'dart:async'; +import "dart:async"; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; -import 'package:cake_wallet/themes/core/theme_store.dart'; -import 'package:cake_wallet/utils/exception_handler.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/balance.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cake_wallet/store/wallet_list_store.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:cake_wallet/core/amount_parsing_proxy.dart"; +import "package:cake_wallet/di.dart"; +import "package:cake_wallet/entities/preferences_key.dart"; +import "package:cake_wallet/reactions/wallet_connect.dart"; +import "package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart"; +import "package:cake_wallet/store/authentication_store.dart"; +import "package:cake_wallet/store/settings_store.dart"; +import "package:cake_wallet/store/wallet_list_store.dart"; +import "package:cake_wallet/themes/core/theme_store.dart"; +import "package:cake_wallet/utils/exception_handler.dart"; +import "package:cw_core/balance.dart"; +import "package:cw_core/transaction_history.dart"; +import "package:cw_core/transaction_info.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:mobx/mobx.dart"; +import "package:shared_preferences/shared_preferences.dart"; -part 'app_store.g.dart'; +part "app_store.g.dart"; class AppStore = AppStoreBase with _$AppStore; @@ -59,8 +59,9 @@ abstract class AppStoreBase with Store { @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { - bool changingToSameWalletType = this.wallet?.type == wallet.type; - this.wallet?.close(shouldCleanup: !changingToSameWalletType); + final changingToSameWalletType = this.wallet?.type == wallet.type; + + await this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); @@ -80,7 +81,7 @@ abstract class AppStoreBase with Store { wcService.create(); await wcService.init(); } catch (e, s) { - printV('WalletConnect setup failed: $e\n$s'); + printV("WalletConnect setup failed: $e\n$s"); } } } diff --git a/lib/view_model/hardware_wallet/bitbox_view_model.dart b/lib/view_model/hardware_wallet/bitbox_view_model.dart index 39c4b5123..a07b63cfb 100644 --- a/lib/view_model/hardware_wallet/bitbox_view_model.dart +++ b/lib/view_model/hardware_wallet/bitbox_view_model.dart @@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { @observable bool isBleEnabled = false; + @override + @observable + bool isConnecting = false; + @override bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth @@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { Future stopScanning() async {} @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type) async { if (!(device is BitboxHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (isConnecting) return false; + isConnecting = true; try { final appDocDir = await getAppDir(); @@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt'); await bitboxManager.channelHashVerify(); printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt'); - _isConnecting = false; + isConnecting = false; return true; } catch (e) { printV(e); } - _isConnecting = false; + isConnecting = false; return false; } - bool _isConnecting = false; - @override bool isConnected(WalletType type) => false; diff --git a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart index 1b87abff2..ce5473914 100644 --- a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart +++ b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart @@ -1,13 +1,14 @@ -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; abstract class HardwareWalletViewModel { HardwareWalletType get hardwareWalletType; bool get isBleEnabled; bool get hasBluetooth; + bool get isConnecting; bool isConnected(WalletType type); @@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel { Future initWallet(WalletBase wallet); String? interpretErrorCode(String error) => null; + + Future close() async {} } diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index dc608d8d3..f5efaa52e 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,50 +1,47 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +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/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/connect_device/connect_device_page.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:flutter/widgets.dart'; -import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; -import 'package:mobx/mobx.dart'; -import 'package:permission_handler/permission_handler.dart'; +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/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/store/app_store.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:flutter/widgets.dart"; +import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk; +import "package:mobx/mobx.dart"; +import "package:permission_handler/permission_handler.dart"; -part 'ledger_view_model.g.dart'; +part "ledger_view_model.g.dart"; class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel; abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { - late final sdk.LedgerInterface ledgerPlusBLE; - late final sdk.LedgerInterface ledgerPlusUSB; - - bool get _doesSupportHardwareWallets { - if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes( - WalletType.monero, HardwareWalletType.ledger, Platform.isIOS) - .isNotEmpty; - } - - return true; - } - - LedgerViewModelBase() { + LedgerViewModelBase(AppStore appStore) { if (_doesSupportHardwareWallets) { reaction((_) => isBleEnabled, (_) { - if (isBleEnabled) _initBLE(); + if (isBleEnabled) { + _initBLE(); + } }); + + reaction((_) => appStore.wallet, (wallet) async { + if (wallet != null && wallet.type != _connectingWalletType) { + await close(); + } + }); + updateBleState(); if (!Platform.isIOS) { @@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } } + late final sdk.LedgerInterface ledgerPlusBLE; + late final sdk.LedgerInterface ledgerPlusUSB; + + bool get _doesSupportHardwareWallets { + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, + HardwareWalletType.ledger, + Platform.isIOS, + ).isNotEmpty; + } + + return true; + } + @override HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger; @@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { @observable bool isBleEnabled = false; + @override + @observable + bool isConnecting = false; + @override bool get hasBluetooth => true; @@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { if (isBleEnabled && !_bleIsInitialized) { ledgerPlusBLE = sdk.LedgerInterface.ble( onPermissionRequest: (_) async { - if (Platform.isMacOS) return true; + if (Platform.isMacOS) { + return true; + } - Map statuses = await [ + final statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.bluetoothAdvertise, @@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { 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; } @@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { final newState = bleState == sdk.AvailabilityState.poweredOn; - if (newState != isBleEnabled) isBleEnabled = newState; + if (newState != isBleEnabled) { + isBleEnabled = newState; + } } @override Stream scanForBleDevices() => - ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d)); + ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new); @override - Future> getAllUsbDevices() => ledgerPlusUSB.devices - .then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList()); + Future> getAllUsbDevices() => + ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList()); @override Future stopScanning() async { - if (_bleIsInitialized) await ledgerPlusBLE.stopScanning(); - if (!Platform.isIOS) await ledgerPlusUSB.stopScanning(); + if (_bleIsInitialized) { + await ledgerPlusBLE.stopScanning(); + } + if (!Platform.isIOS) { + await ledgerPlusUSB.stopScanning(); + } } @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type) async { - if (!(device is LedgerHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (device is! LedgerHardwareWalletDevice) { + return false; + } + + if (isConnecting) { + return false; + } + + isConnecting = true; _connectingWalletType = type; if (isConnected(type)) { try { @@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { final ledger = device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB; - if (_connectionChangeSubscription == null) { - _connectionChangeSubscription = - ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener); - } + _connectionChangeSubscription ??= + ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener); try { _connection = await ledger.connect(device.device); - _isConnecting = false; + isConnecting = false; return true; } catch (e) { printV(e); } - _isConnecting = false; + isConnecting = false; return false; } StreamSubscription? _connectionChangeSubscription; sdk.LedgerConnection? _connection; - bool _isConnecting = false; WalletType? _connectingWalletType; + bool suppressMoneroReconnectPrompt = false; void _connectionChangeListener(sdk.BleConnectionState event) { - printV('Ledger Device State Changed: $event'); - if (event == sdk.BleConnectionState.disconnected && !_isConnecting) { + printV("Ledger Device State Changed: $event"); + if (event == sdk.BleConnectionState.disconnected && !isConnecting) { _connection = null; if (_connectingWalletType == WalletType.monero) { monero!.resetLedgerConnection(); + if (suppressMoneroReconnectPrompt) { + return; + } + Navigator.of(navigatorKey.currentContext!).pushNamed( Routes.connectDevices, arguments: ConnectDevicePageParams( @@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { hardwareWalletType: HardwareWalletType.ledger, allowChangeWallet: true, isReconnect: true, - onConnectDevice: (context, ledgerVM) async { + onConnectDevice: (context, ledgerVM) { if (context.mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } @@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } @override - bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected); + bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected; sdk.LedgerConnection get connection => _connection!; @override - Future initWallet(WalletBase wallet) async { + Future initWallet(WalletBase wallet) { switch (wallet.type) { case WalletType.monero: return monero!.setLedgerConnection(wallet, connection); case WalletType.bitcoin: - return bitcoin! - .setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.litecoin: - return bitcoin! - .setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.ethereum: case WalletType.polygon: - return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); default: - throw Exception('Unexpected wallet type: ${wallet.type} for ledger'); + throw Exception("Unexpected wallet type: ${wallet.type} for ledger"); } } @@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return error; } - var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString()); + final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString()); - String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? ""; + final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? ""; if (errorCode.contains("6985")) { return S.current.ledger_error_tx_rejected_by_user; } else if (errorCode.contains("5515")) { return S.current.ledger_error_device_locked; - } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) { + } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) { return S.current.ledger_error_wrong_app; } return null; } + + @override + Future close() async { + try { + printV("Resetting ledger VM"); + await _connectionChangeSubscription?.cancel(); + await _connection?.disconnect().catchError((_) {}); + + _connectingWalletType = null; + _connectionChangeSubscription = null; + _connection = null; + isConnecting = false; + await stopScanning(); + } catch (_) {} + } } 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 7115cdf5a..c048edf7c 100644 --- a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart +++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart @@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S @override HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor; + @override + @observable + bool isConnecting = false; + @override @observable bool isBleEnabled = false; @@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S if (!Platform.isIOS) await trezorUSB.stopScanning(); } - bool _isConnecting = false; sdk.TrezorClient? _client; Completer? _pinCompleter; @@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S void setParingPin(String pin) => _pinCompleter?.complete(pin); @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type, [bool isRetry = false]) async { if (!(device is TrezorHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (isConnecting) return false; + isConnecting = true; paringState = TrezorParingState.initial; try { @@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S printV(e); return false; } finally { - _isConnecting = false; + isConnecting = false; } } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index d8ae44e53..78b6793dd 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store { if (walletItem.type == WalletType.haven) { return; } - // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; - // await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType); + final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); await _appStore.changeCurrentWallet(wallet); updateList(); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 90c72af2b..ee0c0f861 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -125,7 +125,7 @@ dependencies: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9 + ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28 trezor_connect: git: url: https://github.com/cake-tech/trezor_connect @@ -201,14 +201,8 @@ dependency_overrides: ffi: 2.1.0 ledger_flutter_plus: git: - url: https://github.com/cake-tech/ledger-flutter-plus - ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2 - ledger_usb_plus: - git: - # url: https://github.com/konstantinullrich/ledger-usb-plus - # ref: dad482c847c0d0eedceabbb40d3478e64b65e630 - url: https://github.com/cake-tech/ledger-usb-plus - ref: 734c3d6f8267ba52ad73b6e1e78e570c07298890 + url: https://github.com/vespr-wallet/ledger-flutter-plus + ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28 socks5_proxy: git: url: https://github.com/LacticWhale/socks_dart.git @@ -231,7 +225,7 @@ dependency_overrides: universal_ble: git: url: https://github.com/cake-tech/universal_ble.git - ref: f7044558ad111a156987614971e08c583c80175c + ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/tool/configure.dart b/tool/configure.dart index 54febefbb..d6b76d756 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -475,6 +475,7 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir Future setLedgerConnection(Object wallet, ledger.LedgerConnection connection); void resetLedgerConnection(); void setGlobalLedgerConnection(ledger.LedgerConnection connection); + bool hasGlobalLedgerConnection(); String? getLastLedgerCommand(); void setHardwareWalletService(Object wallet, HardwareWalletService service); HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection);