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
+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,
);
}