CW-1581-trezor-fixes-enhancements (#3462)

* feat: add Trezor auto connect

* chore: update `trezor_flutter`

* fix: correct method name for retrieving Trezor auto-pairing credentials

* fix: update device connection prompt text across all languages [skip ci]

* feat: add Trezor key image synchronization ui and improve hardware wallet UX

* feat: add sync-balance icon asset for settings row visuals [skip ci]

* feat: add sync-balance icon asset for settings row visuals [skip ci]

* fix: remove unused Monero and sync key images settings actions from menus [skip ci]

* fix: re-enable routing for UR QR Animated Page [skip ci]
This commit is contained in:
Konstantin Ullrich
2026-08-06 14:49:18 +02:00
committed by GitHub
parent 76b04580f3
commit feeec2e425
46 changed files with 854 additions and 188 deletions
+3 -1
View File
@@ -434,7 +434,9 @@ Future<void> setup({
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
);
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
getIt.registerLazySingleton(
() => TrezorConnectViewModel(getIt<TrezorConnect>(), getIt<SecureStorage>()),
);
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
+6
View File
@@ -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();
+12 -24
View File
@@ -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<NewSendPage> {
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<NewSendPage> {
}
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<NewSendPage> {
} 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<NewSendPage> {
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<NewSendPage> {
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)) {
+24 -16
View File
@@ -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<SettingsListItem> 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),
]);
@@ -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<SyncKeyImagesSheet> createState() => _HardwareWalletProceedOnDeviceSheetState();
}
class _HardwareWalletProceedOnDeviceSheetState extends State<SyncKeyImagesSheet> {
_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<void> _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;
}
@@ -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<void> Function() onPressed;
final bool disabled;
final SvgPicture? image;
final Color color;
final Color textColor;
final Color borderColor;
final String text;
@override
State<StatefulWidget> createState() => _NewFuturePrimaryButtonState();
}
class _NewFuturePrimaryButtonState extends State<NewFuturePrimaryButton> {
bool isLoading = false;
Future<void> _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,
);
}
+16 -3
View File
@@ -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<dynamic> createRoute(RouteSettings settings) {
case Routes.setup2faInfoPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
case Routes.syncKeyImagesDevices:
final hardwareWalletType = getIt<AppStore>().wallet!.hardwareWalletType;
if (hardwareWalletType == HardwareWalletType.trezor) {
return MaterialPageRoute<void>(
builder: (_) => SyncKeyImagesSheet(
appStore: getIt<AppStore>(),
trezorConnectVM: getIt<TrezorConnectViewModel>(),
),
);
}
return MaterialPageRoute<void>(
builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
case Routes.urqrAnimatedPage:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
+1
View File
@@ -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';
@@ -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<Permission, PermissionStatus> statuses = await [
final Map<Permission, PermissionStatus> 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<HardwareWalletDevice> scanForBleDevices() =>
trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d));
trezorBLE.scan().map(TrezorHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => trezorUSB.devices
.then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList());
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList());
@override
Future<void> 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<bool> connectDevice(HardwareWalletDevice device, WalletType type,
[bool isRetry = false]) async {
if (!(device is TrezorHardwareWalletDevice)) return false;
if (isConnecting) return false;
Future<bool> 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<String> 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<String> get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc";
Future<sdk.ThpState> _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<sdk.ThpState> _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<bool> 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;
}