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]

Konstantin Ullrich committed Aug 6, 2026 at 14:49 UTC feeec2e42501104140b985b6871f7bc3c68eb65f
46 files changed +855 -189
cw_monero/lib/monero_wallet.dart
+2
@@ -370,6 +370,8 @@ abstract class MoneroWalletBase
370 };
371 }
372
373 + bool hasUnknownKeyImages() => currentWallet!.hasUnknownKeyImages();
374 +
375 bool needExportOutputs(Money amount) {
376 if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
377 return false;
cw_monero/lib/trezor.dart
+22 -19
@@ -1,27 +1,28 @@
1 -import 'dart:convert';
1 +import "dart:convert";
2
3 -import 'package:cw_core/hardware/hardware_wallet_service.dart';
4 -import 'package:trezor_flutter/trezor_flutter.dart';
3 +import "package:cw_core/hardware/hardware_wallet_service.dart";
4 +import "package:trezor_flutter/trezor_flutter.dart";
5
6 class MoneroTrezorService extends HardwareWalletService {
7 - final TrezorClient client;
8 -
7 MoneroTrezorService(this.client);
8 +
9 + final TrezorClient client;
10 }
11
12 class MoneroTrezorWatchCredentials {
13 + const MoneroTrezorWatchCredentials(this.watchKey, this.address);
14 +
15 final String watchKey;
16 final String address;
15 -
16 - const MoneroTrezorWatchCredentials(this.watchKey, this.address);
17 }
18
19 class Trezor {
20 - final MoneroTrezorService service;
21 -
20 Trezor(this.service);
21
22 + final MoneroTrezorService service;
23 +
24 String? _sessionPassphrase;
25 +
26 Future<void> newPassphraseSession(String? passphrase) async {
27 if (passphrase == null) return;
28
@@ -42,21 +43,23 @@ class Trezor {
43
44 final txIds = <MoneroKeyImageTxData>[];
45 for (final tdi in tdisList) {
45 - txIds.add(MoneroKeyImageTxData(
46 - outKey: tdi["out_key"],
47 - txPubKey: tdi["tx_pub_key"],
48 - internalOutputIndex: tdi["internal_output_index"],
49 - subAddrMajor: tdi["sub_addr_major"],
50 - subAddrMinor: tdi["sub_addr_minor"],
46 + txIds.add(
47 + MoneroKeyImageTxData(
48 + outKey: tdi["out_key"] as String,
49 + txPubKey: tdi["tx_pub_key"] as String,
50 + internalOutputIndex: tdi["internal_output_index"] as int,
51 + subAddrMajor: tdi["sub_addr_major"] as int,
52 + subAddrMinor: tdi["sub_addr_minor"] as int,
53 additionalTxPubKeys:
52 - (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? []));
54 + (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [],
55 + ),
56 + );
57 }
58 final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds);
59
60 return jsonEncode(keyImages.toMap());
61 }
62
59 - Future<String> signTransaction(String json) async {
60 - return TrezorMonero(service.client).signTransaction(jsonDecode(json));
61 - }
63 + Future<String> signTransaction(String json) =>
64 + TrezorMonero(service.client).signTransaction(jsonDecode(json) as Map<String, dynamic>);
65 }
cw_monero/pubspec.lock
+4 -4
@@ -921,8 +921,8 @@ packages:
921 dependency: "direct main"
922 description:
923 path: trezor-flutter
924 - ref: "944080d950a62dd48a199b9736237cbe580351de"
925 - resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
924 + ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
925 + resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
926 url: "https://github.com/cake-tech/trezor-flutter.git"
927 source: git
928 version: "0.1.0"
@@ -930,8 +930,8 @@ packages:
930 dependency: transitive
931 description:
932 path: trezor_usb_transport
933 - ref: "944080d950a62dd48a199b9736237cbe580351de"
934 - resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
933 + ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
934 + resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
935 url: "https://github.com/cake-tech/trezor-flutter.git"
936 source: git
937 version: "0.0.1"
cw_monero/pubspec.yaml
+1 -1
@@ -33,7 +33,7 @@ dependencies:
33 trezor_flutter:
34 git:
35 url: https://github.com/cake-tech/trezor-flutter.git
36 - ref: 944080d950a62dd48a199b9736237cbe580351de
36 + ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97
37 path: trezor-flutter
38
39 dev_dependencies:
lib/di.dart
+3 -1
@@ -434,7 +434,9 @@ Future<void> setup({
434 () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
435 );
436
437 - getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
437 + getIt.registerLazySingleton(
438 + () => TrezorConnectViewModel(getIt<TrezorConnect>(), getIt<SecureStorage>()),
439 + );
440
441 getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
442
lib/monero/cw_monero.dart
+6
@@ -420,6 +420,12 @@ class CWMonero extends Monero {
420 return moneroWallet.needExportOutputs(amount);
421 }
422
423 + @override
424 + bool hasUnknownKeyImages(Object wallet) {
425 + final moneroWallet = wallet as MoneroWallet;
426 + return moneroWallet.hasUnknownKeyImages();
427 + }
428 +
429 @override
430 void monerocCheck() => checkIfMoneroCIsFine();
431
lib/new-ui/pages/send_page.dart
+12 -24
@@ -19,6 +19,7 @@ import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet
19 import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
20 import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
21 import "package:cake_wallet/new-ui/widgets/modern_button.dart";
22 +import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart";
23 import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
24 import "package:cake_wallet/new-ui/widgets/picker.dart";
25 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
43 import "package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart";
44 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
45 import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart";
45 -import "package:cake_wallet/src/widgets/primary_button.dart";
46 import "package:cake_wallet/src/widgets/standard_checkbox.dart";
47 import "package:cake_wallet/store/app_store.dart";
48 import "package:cake_wallet/utils/payment_request.dart";
@@ -618,9 +618,9 @@ class _NewSendPageState extends State<NewSendPage> {
618 selectedDot: _selectedOutput,
619 ),
620 Observer(
621 - builder: (_) => LoadingPrimaryButton(
621 + builder: (_) => NewFuturePrimaryButton(
622 key: const ValueKey("send_page_send_button_key"),
623 - onPressed: () {
623 + onPressed: () async {
624 //Request dummy node to get the focus out of the text fields
625 FocusScope.of(context).requestFocus(FocusNode());
626
@@ -629,11 +629,11 @@ class _NewSendPageState extends State<NewSendPage> {
629 }
630
631 if (widget.mode == SendPageModes.normal) {
632 - _handleSend();
632 + await _handleSend();
633 } else if (widget.mode ==
634 SendPageModes.lightningDeposit ||
635 widget.mode == SendPageModes.mwebDeposit) {
636 - Navigator.of(context).push(
636 + await Navigator.of(context).push(
637 CupertinoPageRoute(
638 builder: (context) => Material(
639 child: L2ActionWalletSelector(
@@ -652,7 +652,7 @@ class _NewSendPageState extends State<NewSendPage> {
652 } else if (widget.mode ==
653 SendPageModes.lightningWithdrawal ||
654 widget.mode == SendPageModes.mwebWithdrawal) {
655 - Navigator.of(context).push(
655 + await Navigator.of(context).push(
656 CupertinoPageRoute(
657 builder: (context) => Material(
658 child: L2ActionWalletSelector(
@@ -673,13 +673,7 @@ class _NewSendPageState extends State<NewSendPage> {
673 text: S.of(context).continue_text,
674 color: Theme.of(context).colorScheme.primary,
675 textColor: Theme.of(context).colorScheme.onPrimary,
676 - isLoading: widget.sendViewModel.state is IsExecutingState ||
677 - widget.sendViewModel.state is TransactionCommitting ||
678 - widget.sendViewModel.state
679 - is IsAwaitingDeviceResponseState ||
680 - widget.sendViewModel.state
681 - is LoadingTemplateExecutingState,
682 - isDisabled: !widget.sendViewModel.isReadyForSend ||
676 + disabled: !widget.sendViewModel.isReadyForSend ||
677 widget.sendViewModel.state is ExecutedSuccessfullyState,
678 ),
679 ),
@@ -858,17 +852,11 @@ class _NewSendPageState extends State<NewSendPage> {
852 for (final item in widget.sendViewModel.outputs) {
853 amount += item.cryptoAmountMoney;
854 }
861 - if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
862 - if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) {
863 - await monero!.syncTrezor(widget.sendViewModel.wallet);
864 - } else {
865 - if (mounted) {
866 - await Navigator.of(context).pushNamed(
867 - Routes.urqrAnimatedPage,
868 - arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet),
869 - );
870 - }
871 - }
855 + if (monero!.hasUnknownKeyImages(widget.sendViewModel.wallet) && mounted) {
856 + await Navigator.of(context).pushNamed(
857 + Routes.syncKeyImagesDevices,
858 + arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet),
859 + );
860 await Future.delayed(const Duration(seconds: 1)); // wait for monero to refresh the state
861 }
862 if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
lib/new-ui/pages/settings_page.dart
+23 -15
@@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
9 import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
10 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
11 import 'package:cw_core/wallet_info.dart';
12 +import "package:cw_core/wallet_type.dart";
13 import 'package:flutter/material.dart';
13 -import 'package:flutter_svg/svg.dart';
14 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
15 -import "package:cw_core/wallet_type.dart";
15
16 bool _trueFunc(DashboardViewModel _) => true;
17
@@ -26,10 +25,21 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
25
26 bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
27
29 -bool _isCupcake(DashboardViewModel vm) =>
30 - vm.wallet.hardwareWalletType == HardwareWalletType.cupcake;
28 +bool _requiresKeyImageSync(DashboardViewModel vm) =>
29 + vm.wallet.type == WalletType.monero &&
30 + [HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
31
32 class SettingsListItem {
33 + const SettingsListItem(
34 + this.iconPath,
35 + this.title,
36 + this.route, {
37 + this.requireAuth = false,
38 + this.use2fa = _falseFunc,
39 + this.condition = _trueFunc,
40 + this.routeArgs,
41 + });
42 +
43 final String iconPath;
44 final String title;
45 final String route;
@@ -37,21 +47,15 @@ class SettingsListItem {
47 final bool requireAuth;
48 final bool Function(DashboardViewModel) use2fa;
49 final bool Function(DashboardViewModel) condition;
40 -
41 - const SettingsListItem(this.iconPath, this.title, this.route,
42 - {this.routeArgs = null,
43 - this.requireAuth = false,
44 - this.use2fa = _falseFunc,
45 - this.condition = _trueFunc});
50 }
51
52 class SettingsSectionData {
53 + const SettingsSectionData(this.title, this.titleIconPath, this.items);
54 +
55 final String title;
56 final String titleIconPath;
57 final List<SettingsListItem> items;
58
53 - const SettingsSectionData(this.title, this.titleIconPath, this.items);
54 -
59 static SettingsSectionData walletSettings =
60 SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
61 SettingsListItem(
@@ -71,9 +75,13 @@ class SettingsSectionData {
75 condition: _hasWalletConnect),
76 //SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
77 //SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
74 - SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs,
75 - Routes.urqrAnimatedPage,
76 - routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake),
78 + SettingsListItem(
79 + "assets/new-ui/settings_row_icons/sync-balance.svg",
80 + S.current.resync_device,
81 + Routes.syncKeyImagesDevices,
82 + routeArgs: {'export-outputs': 'export-outputs'},
83 + condition: _requiresKeyImageSync,
84 + ),
85 SettingsListItem(
86 "assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
87 ]);
lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart new
+224
@@ -0,0 +1,224 @@
1 +import "package:cake_wallet/generated/i18n.dart";
2 +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
3 +import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart";
4 +import "package:cake_wallet/routes.dart";
5 +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
6 +import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
7 +import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
8 +import "package:cake_wallet/src/widgets/cake_image_widget.dart";
9 +import "package:cake_wallet/src/widgets/primary_button.dart";
10 +import "package:cake_wallet/store/app_store.dart";
11 +import "package:cake_wallet/utils/show_pop_up.dart";
12 +import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart";
13 +import "package:cw_core/wallet_base.dart";
14 +import "package:cw_core/wallet_info.dart";
15 +import "package:flutter/cupertino.dart";
16 +import "package:flutter/material.dart";
17 +
18 +class SyncKeyImagesSheet extends StatefulWidget {
19 + const SyncKeyImagesSheet({
20 + required this.appStore,
21 + required this.trezorConnectVM,
22 + super.key,
23 + });
24 +
25 + final AppStore appStore;
26 + final TrezorConnectViewModelBase trezorConnectVM;
27 +
28 + WalletBase get wallet => appStore.wallet!;
29 +
30 + @override
31 + State<SyncKeyImagesSheet> createState() => _HardwareWalletProceedOnDeviceSheetState();
32 +}
33 +
34 +class _HardwareWalletProceedOnDeviceSheetState extends State<SyncKeyImagesSheet> {
35 + _KeyImageSyncState _state = _KeyImageSyncState.initial;
36 +
37 + @override
38 + Widget build(BuildContext context) => PopScope(
39 + canPop: false,
40 + child: SafeArea(
41 + bottom: false,
42 + child: Container(
43 + width: MediaQuery.of(context).size.width,
44 + decoration: BoxDecoration(
45 + color: Theme.of(context).colorScheme.surface,
46 + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
47 + ),
48 + child: SafeArea(
49 + child: Padding(
50 + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
51 + child: Column(
52 + children: [
53 + ModalTopBar(
54 + title: S.of(context).resync_device,
55 + leadingIcon: Icon(
56 + Icons.close,
57 + color: Theme.of(context).colorScheme.onSurfaceVariant,
58 + ),
59 + onLeadingPressed: () => showPopUp(
60 + context: context,
61 + builder: (context) => AlertWithTwoActions(
62 + alertTitle: S.of(context).are_you_sure_exit,
63 + alertContent: S.of(context).resync_device_cancel_warning_desc,
64 + leftButtonText: S.of(context).cancel,
65 + rightButtonText: S.of(context).yes_exit,
66 + rightAlertButtonStyle: AlertButtonStyle.error(context),
67 + actionLeftButton: Navigator.of(context).pop,
68 + actionRightButton: () {
69 + Navigator.of(context).pop();
70 + Navigator.of(context).pop();
71 + },
72 + ),
73 + ),
74 + ),
75 + Expanded(
76 + child: Column(
77 + spacing: 12,
78 + mainAxisAlignment: MainAxisAlignment.center,
79 + children: [
80 + CakeImageWidget(
81 + imageUrl: hardwareWalletIcon,
82 + width: 100,
83 + colorFilter: ColorFilter.mode(
84 + Theme.of(context).colorScheme.onSurfaceVariant,
85 + BlendMode.srcIn,
86 + ),
87 + ),
88 + DirectionalAnimatedSwitcher(
89 + duration: const Duration(milliseconds: 400),
90 + child: content,
91 + ),
92 + ],
93 + ),
94 + ),
95 + Padding(
96 + padding: const EdgeInsets.all(24),
97 + child: AnimatedOpacity(
98 + curve: Curves.easeOutQuad,
99 + opacity: _state == _KeyImageSyncState.initial ? 1 : 0,
100 + duration: const Duration(milliseconds: 300),
101 + child: PrimaryButton(
102 + text: S.of(context).continue_text,
103 + color: Theme.of(context).colorScheme.primary,
104 + textColor: Theme.of(context).colorScheme.onPrimary,
105 + onPressed: _onContinuePressed,
106 + ),
107 + ),
108 + ),
109 + ],
110 + ),
111 + ),
112 + ),
113 + ),
114 + ),
115 + );
116 +
117 + Widget get content {
118 + if (_state == _KeyImageSyncState.syncing) {
119 + return _syncingKeyImages();
120 + }
121 +
122 + return _initial();
123 + }
124 +
125 + String? get hardwareWalletIcon {
126 + switch (widget.wallet.hardwareWalletType) {
127 + case HardwareWalletType.bitbox:
128 + return "assets/new-ui/hardware_wallets/device_bitbox.svg";
129 + case HardwareWalletType.ledger:
130 + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
131 + case HardwareWalletType.trezor:
132 + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
133 + case HardwareWalletType.cupcake:
134 + return "assets/images/cupcake.svg";
135 + case HardwareWalletType.coldcard:
136 + case HardwareWalletType.seedsigner:
137 + case HardwareWalletType.keystone:
138 + case null:
139 + return "assets/images/hardware_wallet/device_qr.svg";
140 + }
141 + }
142 +
143 + Widget _initial() => Column(
144 + key: const ValueKey(0),
145 + spacing: 12,
146 + mainAxisAlignment: MainAxisAlignment.center,
147 + children: [
148 + Padding(
149 + padding: const EdgeInsets.symmetric(horizontal: 20),
150 + child: Text(
151 + S.of(context).resync_device_desc,
152 + textAlign: TextAlign.center,
153 + style: TextStyle(
154 + fontSize: 16,
155 + color: Theme.of(context).colorScheme.onSurfaceVariant,
156 + ),
157 + ),
158 + ),
159 + ],
160 + );
161 +
162 + Widget _syncingKeyImages() => Column(
163 + key: const ValueKey(1),
164 + spacing: 12,
165 + mainAxisAlignment: MainAxisAlignment.center,
166 + children: [
167 + Text(
168 + S.of(context).proceed_on_device,
169 + style: TextStyle(
170 + fontWeight: FontWeight.w500,
171 + fontSize: 20,
172 + ),
173 + ),
174 + Padding(
175 + padding: EdgeInsets.symmetric(horizontal: 20),
176 + child: Text(
177 + S.of(context).proceed_on_device_description,
178 + textAlign: TextAlign.center,
179 + style: TextStyle(
180 + fontSize: 16,
181 + color: Theme.of(context).colorScheme.onSurfaceVariant,
182 + ),
183 + ),
184 + )
185 + ],
186 + );
187 +
188 + Future<void> _onContinuePressed() async {
189 + setState(() => _state = _KeyImageSyncState.syncing);
190 +
191 + if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
192 + await Navigator.of(context).pushNamed(
193 + Routes.connectDevices,
194 + arguments: ConnectDevicePageParams(
195 + walletType: widget.wallet.type,
196 + hardwareWalletType: widget.wallet.walletInfo.hardwareWalletType!,
197 + onConnectDevice: (_, __) {
198 + widget.trezorConnectVM.initWallet(widget.wallet);
199 + Navigator.of(context).pop();
200 + },
201 + isReconnect: false,
202 + ),
203 + );
204 +
205 + // Recheck to handle tap-backs
206 + if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
207 + setState(() => _state = _KeyImageSyncState.initial);
208 + return;
209 + }
210 + } else {
211 + await widget.trezorConnectVM.initWallet(widget.wallet);
212 + }
213 +
214 + final result = await widget.trezorConnectVM.syncKeyImages(widget.wallet);
215 + if (result && context.mounted) {
216 + Navigator.of(context).pop();
217 + }
218 + }
219 +}
220 +
221 +enum _KeyImageSyncState {
222 + initial,
223 + syncing;
224 +}
lib/new-ui/widgets/new_future_primary_button.dart new
+57
@@ -0,0 +1,57 @@
1 +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
2 +import "package:flutter/material.dart";
3 +import "package:flutter_svg/flutter_svg.dart";
4 +
5 +class NewFuturePrimaryButton extends StatefulWidget {
6 + const NewFuturePrimaryButton({
7 + required this.onPressed,
8 + required this.text,
9 + required this.color,
10 + required this.textColor,
11 + this.borderColor = Colors.transparent,
12 + this.disabled = false,
13 + this.image,
14 + super.key,
15 + });
16 +
17 + final Future<void> Function() onPressed;
18 + final bool disabled;
19 + final SvgPicture? image;
20 + final Color color;
21 + final Color textColor;
22 + final Color borderColor;
23 + final String text;
24 +
25 + @override
26 + State<StatefulWidget> createState() => _NewFuturePrimaryButtonState();
27 +}
28 +
29 +class _NewFuturePrimaryButtonState extends State<NewFuturePrimaryButton> {
30 + bool isLoading = false;
31 +
32 + Future<void> _onPressed() async {
33 + if (isLoading) {
34 + return;
35 + }
36 + setState(() => isLoading = true);
37 + try {
38 + await widget.onPressed.call();
39 + } finally {
40 + if (mounted) {
41 + setState(() => isLoading = false);
42 + }
43 + }
44 + }
45 +
46 + @override
47 + Widget build(BuildContext context) => NewPrimaryButton(
48 + onPressed: _onPressed,
49 + image: widget.image,
50 + text: widget.text,
51 + color: widget.color,
52 + textColor: widget.textColor,
53 + isLoading: isLoading,
54 + borderColor: widget.borderColor,
55 + disabled: widget.disabled,
56 + );
57 +}
lib/router.dart
+16 -3
@@ -4,7 +4,6 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
4 import 'package:cake_wallet/core/new_wallet_arguments.dart';
5 import 'package:cake_wallet/new-ui/new_dashboard.dart';
6 import 'package:cake_wallet/new-ui/pages/about_page.dart';
7 -import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
7 import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
8 import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
9 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';
12 import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
13 import "package:cake_wallet/new-ui/pages/receive_page.dart";
14 import 'package:cake_wallet/new-ui/pages/send_page.dart';
15 +import 'package:cake_wallet/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart';
16 import 'package:cake_wallet/order/order.dart';
17 import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
18 import 'package:cake_wallet/core/totp_request_details.dart';
@@ -51,6 +51,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
51 import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
52 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
53 import 'package:cake_wallet/src/screens/dev/network_requests.dart';
54 +import 'package:cake_wallet/store/app_store.dart';
55 import 'package:cake_wallet/utils/feature_flag.dart';
56 import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart';
57 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';
127 import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
128 import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart';
129 import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
129 -import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
130 import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
131 import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
132 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';
146 import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
147 import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
148 import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
149 -import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
149 +import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
150 import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
151 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
152 import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
@@ -908,6 +908,19 @@ Route<dynamic> createRoute(RouteSettings settings) {
908 case Routes.setup2faInfoPage:
909 return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
910
911 + case Routes.syncKeyImagesDevices:
912 + final hardwareWalletType = getIt<AppStore>().wallet!.hardwareWalletType;
913 + if (hardwareWalletType == HardwareWalletType.trezor) {
914 + return MaterialPageRoute<void>(
915 + builder: (_) => SyncKeyImagesSheet(
916 + appStore: getIt<AppStore>(),
917 + trezorConnectVM: getIt<TrezorConnectViewModel>(),
918 + ),
919 + );
920 + }
921 + return MaterialPageRoute<void>(
922 + builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
923 +
924 case Routes.urqrAnimatedPage:
925 return MaterialPageRoute<void>(
926 builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
lib/routes.dart
+1
@@ -133,6 +133,7 @@ class Routes {
133
134 static const signPage = '/sign_page';
135 static const connectDevices = '/device/connect';
136 + static const syncKeyImagesDevices = '/device/sync_key_images';
137 static const urqrAnimatedPage = '/urqr/animated_page';
138 static const walletGroupsDisplayPage = '/wallet_groups_display_page';
139 static const walletGroupDescription = '/wallet_group_description';
lib/view_model/hardware_wallet/trezor_connect_view_model.dart
+162 -64
@@ -1,43 +1,43 @@
1 -import 'dart:async';
2 -import 'dart:io';
3 -
4 -import 'package:cake_wallet/bitcoin/bitcoin.dart';
5 -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
6 -import 'package:cake_wallet/evm/evm.dart';
7 -import 'package:cake_wallet/main.dart';
8 -import 'package:cake_wallet/monero/monero.dart';
9 -import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart';
10 -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
11 -import 'package:cake_wallet/wallet_type_utils.dart';
12 -import 'package:cw_core/hardware/device_connection_type.dart';
13 -import 'package:cw_core/hardware/hardware_wallet_service.dart';
14 -import 'package:cw_core/utils/print_verbose.dart';
15 -import 'package:cw_core/wallet_base.dart';
16 -import 'package:cw_core/wallet_info.dart';
17 -import 'package:cw_core/wallet_type.dart';
18 -import 'package:device_info_plus/device_info_plus.dart';
19 -import 'package:flutter/material.dart';
20 -import 'package:mobx/mobx.dart';
21 -import 'package:permission_handler/permission_handler.dart';
22 -import 'package:trezor_connect/trezor_connect.dart' as connect_sdk;
23 -import 'package:trezor_flutter/trezor_flutter.dart' as sdk;
24 -
25 -part 'trezor_connect_view_model.g.dart';
1 +import "dart:async";
2 +import "dart:convert";
3 +import "dart:io";
4 +
5 +import "package:cake_wallet/bitcoin/bitcoin.dart";
6 +import "package:cake_wallet/core/secure_storage.dart";
7 +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
8 +import "package:cake_wallet/evm/evm.dart";
9 +import "package:cake_wallet/main.dart";
10 +import "package:cake_wallet/monero/monero.dart";
11 +import "package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart";
12 +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
13 +import "package:cake_wallet/wallet_type_utils.dart";
14 +import "package:cw_core/encryption_file_utils.dart";
15 +import "package:cw_core/hardware/device_connection_type.dart";
16 +import "package:cw_core/hardware/hardware_wallet_service.dart";
17 +import "package:cw_core/key.dart";
18 +import "package:cw_core/root_dir.dart";
19 +import "package:cw_core/utils/print_verbose.dart";
20 +import "package:cw_core/wallet_base.dart";
21 +import "package:cw_core/wallet_info.dart";
22 +import "package:cw_core/wallet_type.dart";
23 +import "package:device_info_plus/device_info_plus.dart";
24 +import "package:flutter/material.dart";
25 +import "package:mobx/mobx.dart";
26 +import "package:permission_handler/permission_handler.dart";
27 +import "package:trezor_connect/trezor_connect.dart" as connect_sdk;
28 +import "package:trezor_flutter/trezor_flutter.dart" as sdk;
29 +
30 +part "trezor_connect_view_model.g.dart";
31
32 class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
33
34 abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
30 - final connect_sdk.TrezorConnect trezorConnect;
31 -
32 - late final sdk.TrezorInterface trezorBLE;
33 - late final sdk.TrezorInterface trezorUSB;
34 -
35 - sdk.ThpState _state = sdk.ThpState();
36 -
37 - TrezorConnectViewModelBase(this.trezorConnect) {
35 + TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) {
36 if (_doesSupportHardwareWallets) {
37 reaction((_) => isBleEnabled, (_) {
40 - if (isBleEnabled) _initBLE();
38 + if (isBleEnabled) {
39 + _initBLE();
40 + }
41 });
42 updateBleState();
43
@@ -47,11 +47,21 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
47 }
48 }
49
50 + final connect_sdk.TrezorConnect trezorConnect;
51 + final SecureStorage _secureStorage;
52 +
53 + late final sdk.TrezorInterface trezorBLE;
54 + late final sdk.TrezorInterface trezorUSB;
55 +
56 + sdk.ThpState? _state;
57 +
58 bool get _doesSupportHardwareWallets {
59 if (isMoneroOnly) {
60 return DeviceConnectionType.supportedConnectionTypes(
53 - WalletType.monero, HardwareWalletType.trezor, Platform.isIOS)
54 - .isNotEmpty;
61 + WalletType.monero,
62 + HardwareWalletType.trezor,
63 + Platform.isIOS,
64 + ).isNotEmpty;
65 }
66
67 return true;
@@ -63,9 +73,11 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
73 if (isBleEnabled && !_bleIsInitialized) {
74 trezorBLE = sdk.TrezorInterface.ble(
75 onPermissionRequest: (_) async {
66 - if (Platform.isMacOS) return true;
76 + if (Platform.isMacOS) {
77 + return true;
78 + }
79
68 - Map<Permission, PermissionStatus> statuses = await [
80 + final Map<Permission, PermissionStatus> statuses = await [
81 Permission.bluetoothScan,
82 Permission.bluetoothConnect,
83 Permission.bluetoothAdvertise,
@@ -73,7 +85,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
85
86 return statuses.values.where((status) => status.isDenied).isEmpty;
87 },
76 - bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
88 + bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
89 );
90 _bleIsInitialized = true;
91 }
@@ -99,21 +111,27 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
111
112 final newState = bleState == sdk.AvailabilityState.poweredOn;
113
102 - if (newState != isBleEnabled) isBleEnabled = newState;
114 + if (newState != isBleEnabled) {
115 + isBleEnabled = newState;
116 + }
117 }
118
119 @override
120 Stream<HardwareWalletDevice> scanForBleDevices() =>
107 - trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d));
121 + trezorBLE.scan().map(TrezorHardwareWalletDevice.new);
122
123 @override
110 - Future<List<HardwareWalletDevice>> getAllUsbDevices() => trezorUSB.devices
111 - .then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList());
124 + Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
125 + trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList());
126
127 @override
128 Future<void> stopScanning() async {
115 - if (_bleIsInitialized) await trezorBLE.stopScanning();
116 - if (!Platform.isIOS) await trezorUSB.stopScanning();
129 + if (_bleIsInitialized) {
130 + await trezorBLE.stopScanning();
131 + }
132 + if (!Platform.isIOS) {
133 + await trezorUSB.stopScanning();
134 + }
135 }
136
137 sdk.TrezorClient? _client;
@@ -127,10 +145,17 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
145
146 @override
147 @action
130 - Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
131 - [bool isRetry = false]) async {
132 - if (!(device is TrezorHardwareWalletDevice)) return false;
133 - if (isConnecting) return false;
148 + Future<bool> connectDevice(
149 + HardwareWalletDevice device,
150 + WalletType type, [
151 + bool isRetry = false,
152 + ]) async {
153 + if (device is! TrezorHardwareWalletDevice) {
154 + return false;
155 + }
156 + if (isConnecting) {
157 + return false;
158 + }
159 isConnecting = true;
160 paringState = TrezorParingState.initial;
161
@@ -140,16 +165,19 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
165 final connection = await trezorInterface.connect(device.device);
166
167 if (!isRetry) {
143 - showModalBottomSheet(
144 - context: navigatorKey.currentContext!,
145 - isScrollControlled: true,
146 - isDismissible: false,
147 - enableDrag: false,
148 - useSafeArea: true,
149 - builder: (context) => HardwareWalletProceedOnDeviceSheet(
168 + unawaited(
169 + showModalBottomSheet(
170 + context: navigatorKey.currentContext!,
171 + isScrollControlled: true,
172 + isDismissible: false,
173 + enableDrag: false,
174 + useSafeArea: true,
175 + builder: (_) => HardwareWalletProceedOnDeviceSheet(
176 hardwareWalletType: hardwareWalletType,
177 trezorConnectVM: this,
152 - onRetry: () => connectDevice(device, type, true)),
178 + onRetry: () => connectDevice(device, type, true),
179 + ),
180 + ),
181 );
182 }
183
@@ -159,15 +187,18 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
187
188 final res = await _pinCompleter!.future;
189 paringState = TrezorParingState.verifyingPin;
162 - if (res == null) throw Exception();
190 + if (res == null) {
191 + throw Exception();
192 + }
193 return res;
194 }
195
196 final deviceInfo = await _deviceName;
197
198 + _state ??= await _getState();
199 _client = sdk.TrezorClient.getClientForConnection(
200 connection,
170 - _state,
201 + _state!,
202 "Cake Wallet",
203 deviceInfo,
204 onPinCode,
@@ -175,6 +206,16 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
206
207 await _client!.createChannel();
208
209 + if (!_state!.pairingCredentials.any((c) => c.autoconnect == true)) {
210 + if (_client case final sdk.TrezorClientV2 clientV2) {
211 + try {
212 + final auto = await clientV2.getAutoPairingCredentials();
213 + _state!.setPairingCredentials([auto]);
214 + await _saveState();
215 + } catch (_) {}
216 + }
217 + }
218 +
219 paringState = TrezorParingState.success;
220 return true;
221 } catch (e) {
@@ -192,8 +233,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
233
234 Future<String> get _deviceName async {
235 final deviceInfo = DeviceInfoPlugin();
195 - if (Platform.isAndroid) return (await deviceInfo.androidInfo).model;
196 - if (Platform.isIOS) return (await deviceInfo.iosInfo).name;
236 + if (Platform.isAndroid) {
237 + return (await deviceInfo.androidInfo).model;
238 + }
239 + if (Platform.isIOS) {
240 + return (await deviceInfo.iosInfo).name;
241 + }
242 return "Computer";
243 }
244
@@ -231,8 +276,60 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
276 case WalletType.polygon:
277 return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
278 default:
234 - throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
279 + throw Exception("Unexpected wallet type: ${wallet.type} for trezor");
280 + }
281 + }
282 +
283 + final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true);
284 + static const String _secureStorageKey = "com.cakewallet.trezor/thp_state";
285 +
286 + Future<String> get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc";
287 +
288 + Future<sdk.ThpState> _getState() async {
289 + final password = await _secureStorage.read(key: _secureStorageKey);
290 + if (password == null) {
291 + return _saveState();
292 + }
293 + try {
294 + final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password);
295 + return sdk.ThpState.fromJson(state);
296 + } catch (_) {
297 + return _saveState();
298 + }
299 + }
300 +
301 + Future<sdk.ThpState> _saveState() async {
302 + try {
303 + final password = generateKey();
304 + await _secureStorage.write(key: _secureStorageKey, value: password);
305 +
306 + final file = File(await _thpJsonFile);
307 + if (!file.existsSync()) {
308 + file.createSync();
309 + }
310 +
311 + final state = _state ?? sdk.ThpState();
312 + await _encryptionFileUtils.write(
313 + path: file.path,
314 + password: password,
315 + data: jsonEncode(state.toMap()),
316 + );
317 + _state = state;
318 + return state;
319 + } catch (_) {
320 + throw Exception("Unable to save Trezor State");
321 + }
322 + }
323 +
324 + Future<bool> syncKeyImages(WalletBase wallet) async {
325 + if (wallet.type == WalletType.monero) {
326 + try {
327 + await monero!.syncTrezor(wallet);
328 + } catch (_) {
329 + return false;
330 + }
331 }
332 + return true;
333 }
334 }
335
@@ -241,6 +338,7 @@ abstract class TrezorParingState {
338 static TrezorParingState enterPin = EnterPinTrezorParingState();
339 static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
340 static TrezorParingState success = SuccessTrezorParingState();
341 +
342 static TrezorParingState fail(String message) => FailTrezorParingState(message);
343 }
344
@@ -253,7 +351,7 @@ class VerifyingPinTrezorParingState extends TrezorParingState {}
351 class SuccessTrezorParingState extends TrezorParingState {}
352
353 class FailTrezorParingState extends TrezorParingState {
256 - final String message;
257 -
354 FailTrezorParingState(this.message);
355 +
356 + final String message;
357 }
res/pictures/settings_row_icons/sync-balance.svg new
+19
@@ -0,0 +1,19 @@
1 +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2 +<g clip-path="url(#clip0_23056_316844)">
3 +<path d="M-0.046875 7.78076C-0.046875 4.9805 -0.046875 3.58037 0.498092 2.51081C0.977459 1.57 1.74236 0.805096 2.68317 0.325729C3.75273 -0.219238 5.15286 -0.219238 7.95312 -0.219238H15.9531C18.7534 -0.219238 20.1535 -0.219238 21.2231 0.325729C22.1639 0.805096 22.9288 1.57 23.4082 2.51081C23.9531 3.58037 23.9531 4.9805 23.9531 7.78076V15.7808C23.9531 18.581 23.9531 19.9812 23.4082 21.0507C22.9288 21.9915 22.1639 22.7564 21.2231 23.2358C20.1535 23.7808 18.7534 23.7808 15.9531 23.7808H7.95312C5.15286 23.7808 3.75273 23.7808 2.68317 23.2358C1.74236 22.7564 0.977459 21.9915 0.498092 21.0507C-0.046875 19.9812 -0.046875 18.581 -0.046875 15.7808V7.78076Z" fill="url(#paint0_linear_23056_316844)"/>
4 +<path d="M6.53906 11.6423C6.53906 12.2751 6.65859 12.8903 6.89766 13.488C7.13672 14.0856 7.50938 14.6376 8.01563 15.1438L8.22656 15.3548V14.1313C8.22656 13.8923 8.30742 13.6919 8.46914 13.5302C8.63086 13.3685 8.83125 13.2876 9.07031 13.2876C9.30938 13.2876 9.50977 13.3685 9.67148 13.5302C9.8332 13.6919 9.91406 13.8923 9.91406 14.1313V17.5063C9.91406 17.7454 9.8332 17.9458 9.67148 18.1075C9.50977 18.2692 9.30938 18.3501 9.07031 18.3501H5.69531C5.45625 18.3501 5.25586 18.2692 5.09414 18.1075C4.93242 17.9458 4.85156 17.7454 4.85156 17.5063C4.85156 17.2673 4.93242 17.0669 5.09414 16.9052C5.25586 16.7435 5.45625 16.6626 5.69531 16.6626H7.17188L6.83438 16.3673C6.10312 15.7204 5.58984 14.9821 5.29453 14.1524C4.99922 13.3228 4.85156 12.486 4.85156 11.6423C4.85156 10.3204 5.18906 9.12158 5.86406 8.0458C6.53906 6.97002 7.44609 6.14385 8.58516 5.56729C8.78203 5.45479 8.98945 5.44775 9.20742 5.54619C9.42539 5.64463 9.56953 5.80635 9.63984 6.03135C9.71016 6.24229 9.70664 6.45322 9.6293 6.66416C9.55195 6.8751 9.41484 7.03682 9.21797 7.14932C8.40234 7.59932 7.75195 8.22158 7.2668 9.01611C6.78164 9.81064 6.53906 10.686 6.53906 11.6423ZM16.6641 11.5579C16.6641 10.9251 16.5445 10.3099 16.3055 9.71221C16.0664 9.11455 15.6938 8.5626 15.1875 8.05635L14.9766 7.84541V9.06885C14.9766 9.30791 14.8957 9.5083 14.734 9.67002C14.5723 9.83174 14.3719 9.9126 14.1328 9.9126C13.8938 9.9126 13.6934 9.83174 13.5316 9.67002C13.3699 9.5083 13.2891 9.30791 13.2891 9.06885V5.69385C13.2891 5.45479 13.3699 5.25439 13.5316 5.09268C13.6934 4.93096 13.8938 4.8501 14.1328 4.8501H17.5078C17.7469 4.8501 17.9473 4.93096 18.109 5.09268C18.2707 5.25439 18.3516 5.45479 18.3516 5.69385C18.3516 5.93291 18.2707 6.1333 18.109 6.29502C17.9473 6.45674 17.7469 6.5376 17.5078 6.5376H16.0313L16.3688 6.83291C17.0578 7.52197 17.5605 8.2708 17.877 9.07939C18.1934 9.88799 18.3516 10.7142 18.3516 11.5579C18.3516 12.8798 18.0141 14.0786 17.3391 15.1544C16.6641 16.2302 15.757 17.0563 14.618 17.6329C14.4211 17.7454 14.2137 17.7524 13.9957 17.654C13.7777 17.5556 13.6336 17.3938 13.5633 17.1688C13.493 16.9579 13.4965 16.747 13.5738 16.536C13.6512 16.3251 13.7883 16.1634 13.9852 16.0509C14.8008 15.6009 15.4512 14.9786 15.9363 14.1841C16.4215 13.3896 16.6641 12.5142 16.6641 11.5579Z" fill="url(#paint1_linear_23056_316844)"/>
5 +</g>
6 +<defs>
7 +<linearGradient id="paint0_linear_23056_316844" x1="11.9531" y1="-0.219238" x2="11.9531" y2="23.7808" gradientUnits="userSpaceOnUse">
8 +<stop stop-color="#F2C738"/>
9 +<stop offset="1" stop-color="#745C0E"/>
10 +</linearGradient>
11 +<linearGradient id="paint1_linear_23056_316844" x1="11.6016" y1="4.8501" x2="11.6016" y2="18.3501" gradientUnits="userSpaceOnUse">
12 +<stop stop-color="white"/>
13 +<stop offset="1" stop-color="#E3D376"/>
14 +</linearGradient>
15 +<clipPath id="clip0_23056_316844">
16 +<rect width="24" height="24" rx="5" fill="white"/>
17 +</clipPath>
18 +</defs>
19 +</svg>
res/values/strings_ar.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "أفهم",
530 "icon_style": "نمط الأيقونة",
531 "id": "المعرّف: ",
532 - "if_you_dont_see_your_device": "إذا لم يظهر جهازك أعلاه، يُرجى التأكد من أن Ledger قيد التشغيل ومفتوح القفل!",
532 "ignor": "تجاهل",
533 "import": "استيراد",
534 "importNFTs": "استيراد رموز NFT",
@@ -1451,4 +1450,4 @@
1450 "wallet_balance": "رصيد المحفظة",
1451 "wc_desc": "اتصل بسهولة بتطبيقات DApps",
1452 "yes_exit": "نعم. اخرج"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_bg.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Разбирам",
530 "icon_style": "Стил на иконата",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Ако не виждате устройството си по-горе, моля, уверете се, че вашият Ledger е буден и отключен!",
532 "ignor": "Игнорирай",
533 "import": "Импортиране",
534 "importNFTs": "Импортиране на NFT",
@@ -1451,4 +1450,4 @@
1450 "wallet_balance": "Баланс на портфейла",
1451 "wc_desc": "Свържете се лесно с dApps",
1452 "yes_exit": "Да. Изход"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_cs.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Rozumím",
530 "icon_style": "Styl ikony",
531 "id": "ID: ",
532 - "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ý!",
532 "ignor": "Ignorovat",
533 "import": "Importovat",
534 "importNFTs": "Importovat NFT",
@@ -1451,4 +1450,4 @@
1450 "wallet_balance": "Zůstatek peněženky",
1451 "wc_desc": "Snadno se připojte k dApps",
1452 "yes_exit": "Ano. Ukončit"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_de.arb
+4 -1
@@ -530,7 +530,7 @@
530 "i_understand": "Ich verstehe",
531 "icon_style": "Symbolstil",
532 "id": "ID: ",
533 - "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!",
533 + "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!",
534 "ignore": "Ignorieren",
535 "import": "Importieren",
536 "importNFTs": "NFTs importieren",
@@ -882,6 +882,9 @@
882 "restore_wallet": "Wallet wiederherstellen",
883 "restore_wallet_name": "Wallet-Name",
884 "restore_wallet_restore_description": "Beschreibung der Wallet-Wiederherstellung",
885 + "resync_device": "Gerät resyncen",
886 + "resync_device_desc": "Um das Guthaben aktuell zu halten muss Cake Wallet gelegentlich sich mit dem Gerät synchonisieren.",
887 + "resync_device_cancel_warning_desc": "Dadurch wird der laufende Synchronisierungsprozess mit Ihrem Hardware-Gerät beendet.",
888 "return_address_desc": "Im seltenen Fall eines fehlgeschlagenen Swaps können unsere Partner Ihre Zahlung automatisch an die angegebene Rückerstattungsadresse erstatten.",
889 "return_address_desc_external": "Da Sie „Von extern senden“ ausgewählt haben, legen Sie bitte unten manuell eine Erstattungsadresse fest.",
890 "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrer Debitkarte, Ihrem Bankkonto oder Ihrem Robinhood-Guthaben. Nur USA.",
res/values/strings_en.arb
+5 -2
@@ -544,11 +544,11 @@
544 "how_to_connect": "How to Connect",
545 "how_to_use": "How to use",
546 "how_to_use_card": "How to use this card",
547 - "hww_exit_desc": "This will ed the ongoing pairing process with your hardware device",
547 + "hww_exit_desc": "This will end the ongoing pairing process with your hardware device",
548 "i_understand": "I understand",
549 "icon_style": "Icon style",
550 "id": "ID: ",
551 - "if_you_dont_see_your_device": "If you don't see your device above, please be sure your Ledger is awake and unlocked!",
551 + "if_you_dont_see_your_device": "If you don't see your device above, please be sure your device is awake and unlocked!",
552 "ignor": "Ignore",
553 "import": "Import",
554 "importNFTs": "Import NFTs",
@@ -926,6 +926,9 @@
926 "restore_wallet": "Restore Wallet",
927 "restore_wallet_name": "Wallet name",
928 "restore_wallet_restore_description": "Wallet restore description",
929 + "resync_device": "Resync device",
930 + "resync_device_desc": "Balance updating requires occasional resyncing Cake Wallet with the device.",
931 + "resync_device_cancel_warning_desc": "This will end the ongoing sync process with your hardware device",
932 "return_address_desc": "In the rare case of a failed swap, our partners can automatically refund your payment to the provided refund address.",
933 "return_address_desc_external": "Since you chose Send From External, please set a refund address manually below.",
934 "robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.",
res/values/strings_es.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Entiendo",
530 "icon_style": "Estilo de icono",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Si no ves tu dispositivo arriba, asegúrate de que tu Ledger esté encendido y desbloqueado.",
532 "ignor": "Ignorar",
533 "import": "Importar",
534 "importNFTs": "Importar NFTs",
@@ -1454,4 +1453,4 @@
1453 "wallet_balance": "Saldo de la cartera",
1454 "wc_desc": "Conéctate fácilmente a dApps",
1455 "yes_exit": "Sí. Salir"
1457 -}
\ No newline at end of file
1456 +}
res/values/strings_fa.arb
+1 -2
@@ -527,7 +527,6 @@
527 "i_understand": "متوجه شدم",
528 "icon_style": "سبک آیکون",
529 "id": "شناسه: ",
530 - "if_you_dont_see_your_device": "اگر دستگاه خود را در بالا نمی‌بینید، لطفاً مطمئن شوید که Ledger شما روشن است و قفل آن باز شده است!",
530 "ignor": "نادیده گرفتن",
531 "import": "درون‌ریزی",
532 "importNFTs": "وارد کردن NFTها",
@@ -1451,4 +1450,4 @@
1450 "wallet_balance": "موجودی کیف پول",
1451 "wc_desc": "به‌راحتی به dAppها متصل شوید",
1452 "yes_exit": "بله. خروج"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_fr.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Je comprends",
530 "icon_style": "Style d'icône",
531 "id": "ID : ",
532 - "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é !",
532 "ignor": "Ignorer",
533 "import": "Importer",
534 "importNFTs": "Importer des NFTs",
@@ -1451,4 +1450,4 @@
1450 "wallet_balance": "Solde du portefeuille",
1451 "wc_desc": "Connectez-vous facilement aux dApps",
1452 "yes_exit": "Oui. Quitter"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_gn.arb
+1 -2
@@ -1285,7 +1285,6 @@
1285 "hide": "Mokañy",
1286 "how_to_connect": "Mba'éichapa Oñembojoaju",
1287 "hww_exit_desc": "Kóva omboty pe joaju oikóva ne hardware mba'e'oka ndive",
1288 - "if_you_dont_see_your_device": "Ndehecháiramo ne mba'e'oka yvate, ehecha porãke ne Ledger hesãi ha ipe'apyre!",
1288 "incorrect_seed_option": "Hekope'ỹ. Eha'ã jey",
1289 "incorrect_seed_option_back": "Hekope'ỹ. Ehecha porãke ne seed oñeñongatu hekopete ha eha'ã jey.",
1290 "input": "Jeikeha (input)",
@@ -1459,4 +1458,4 @@
1458 "you_will_receive_estimated_amount": "Rehupytýta (jehaipyre)",
1459 "youCanGoBackToYourDapp": "Ikatúma rejevy ne dApp-pe",
1460 "your": "Nde"
1462 -}
\ No newline at end of file
1461 +}
res/values/strings_ha.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Na fahimta",
530 "icon_style": "Salon icon",
531 "id": "ID: ",
532 - "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!",
532 "ignor": "Yi watsi",
533 "import": "Shigo da",
534 "importNFTs": "Shigo da NFT",
@@ -1454,4 +1453,4 @@
1453 "wallet_balance": "Ma'aunin walat",
1454 "wc_desc": "Haɗa da DApps cikin sauƙi",
1455 "yes_exit": "Eh. Fita"
1457 -}
\ No newline at end of file
1456 +}
res/values/strings_hi.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "मैं समझता/समझती हूँ",
530 "icon_style": "चिह्न शैली",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "यदि आपको ऊपर अपना डिवाइस नहीं दिख रहा है, तो कृपया सुनिश्चित करें कि आपका Ledger चालू है और अनलॉक है!",
532 "ignor": "नज़रअंदाज़ करें",
533 "import": "इम्पोर्ट",
534 "importNFTs": "NFTs आयात करें",
@@ -1453,4 +1452,4 @@
1452 "wallet_balance": "वॉलेट बैलेंस",
1453 "wc_desc": "DApps से आसानी से कनेक्ट करें",
1454 "yes_exit": "हाँ। बाहर निकलें"
1456 -}
\ No newline at end of file
1455 +}
res/values/strings_hr.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Razumijem",
530 "icon_style": "Stil ikone",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Ako gore ne vidite svoj uređaj, provjerite je li vaš Ledger uključen i otključan!",
532 "ignor": "Zanemari",
533 "import": "Uvoz",
534 "importNFTs": "Uvezi NFT-ove",
@@ -1451,4 +1450,4 @@
1450 "wallet_balance": "Stanje novčanika",
1451 "wc_desc": "Jednostavno se povežite s DApp-ovima",
1452 "yes_exit": "Da. Izađi"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_hy.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Հասկացա",
530 "icon_style": "Սրբապատկերների ոճը",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Եթե վերևում չեք տեսնում ձեր սարքը, համոզվեք, որ ձեր Ledger-ը միացված է և ապակողպված է։",
532 "ignor": "Անտեսել",
533 "import": "Ներմուծում",
534 "importNFTs": "Ներմուծել NFT-ներ",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "Հաստատել",
1451 "wc_desc": "Հեշտությամբ միացեք DApps-երին",
1452 "yes_exit": "Այո։ Դուրս գալ"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_id.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Saya mengerti",
530 "icon_style": "Gaya ikon",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Jika Anda tidak melihat perangkat Anda di atas, pastikan Ledger Anda menyala dan tidak terkunci!",
532 "ignor": "Abaikan",
533 "import": "Impor",
534 "importNFTs": "Impor NFT",
@@ -1454,4 +1453,4 @@
1453 "wc_action_approve": "Setujui",
1454 "wc_desc": "Hubungkan dengan mudah ke DApps",
1455 "yes_exit": "Ya. Keluar"
1457 -}
\ No newline at end of file
1456 +}
res/values/strings_it.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Ho capito",
530 "icon_style": "Stile dell'icona",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Se non vedi il tuo dispositivo qui sopra, assicurati che il tuo Ledger sia acceso e sbloccato!",
532 "ignor": "Ignora",
533 "import": "Importa",
534 "importNFTs": "Importa NFT",
@@ -1454,4 +1453,4 @@
1453 "wc_action_approve": "Approva",
1454 "wc_desc": "Connettiti facilmente alle dApp",
1455 "yes_exit": "Sì. Esci"
1457 -}
\ No newline at end of file
1456 +}
res/values/strings_ja.arb
+1 -2
@@ -530,7 +530,6 @@
530 "i_understand": "理解しました",
531 "icon_style": "アイコンのスタイル",
532 "id": "ID: ",
533 - "if_you_dont_see_your_device": "上記にデバイスが表示されない場合は、Ledgerが起動していてロック解除されていることを確認してください!",
533 "ignor": "無視",
534 "import": "インポート",
535 "importNFTs": "NFTをインポート",
@@ -1452,4 +1451,4 @@
1451 "wc_action_approve": "承認",
1452 "wc_desc": "dApp に簡単に接続",
1453 "yes_exit": "はい。終了する"
1455 -}
\ No newline at end of file
1454 +}
res/values/strings_ko.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "이해했습니다",
530 "icon_style": "아이콘 스타일",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "위에서 기기가 보이지 않으면 Ledger가 켜져 있고 잠금 해제되어 있는지 확인하세요!",
532 "ignor": "무시",
533 "import": "가져오기",
534 "importNFTs": "NFT 가져오기",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "승인",
1451 "wc_desc": "DApp에 손쉽게 연결",
1452 "yes_exit": "예. 종료"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_my.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "နားလည်ပါတယ်",
530 "icon_style": "အိုင်ကွန်စတိုင်",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "အထက်မှာ သင့်စက်ကို မတွေ့ပါက သင့် Ledger ကို ဖွင့်ထားပြီး လော့ခ်ဖွင့်ထားကြောင်း သေချာပါစေ။",
532 "ignor": "လျစ်လျူရှုပါ",
533 "import": "တင်သွင်း",
534 "importNFTs": "NFT များကို တင်သွင်းပါ",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "အတည်ပြု",
1451 "wc_desc": "dApp များသို့ လွယ်ကူစွာ ချိတ်ဆက်ပါ",
1452 "yes_exit": "ဟုတ်ကဲ့။ ထွက်ပါ"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_nl.arb
+1 -2
@@ -527,7 +527,6 @@
527 "i_understand": "Ik begrijp het",
528 "icon_style": "Pictogramstijl",
529 "id": "ID: ",
530 - "if_you_dont_see_your_device": "Als je je apparaat hierboven niet ziet, zorg er dan voor dat je Ledger ontgrendeld is!",
530 "ignor": "Negeren",
531 "import": "Importeren",
532 "importNFTs": "NFT's importeren",
@@ -1452,4 +1451,4 @@
1451 "wc_action_approve": "Goedkeuren",
1452 "wc_desc": "Maak eenvoudig verbinding met dApps",
1453 "yes_exit": "Ja. Afsluiten"
1455 -}
\ No newline at end of file
1454 +}
res/values/strings_pl.arb
+1 -2
@@ -528,7 +528,6 @@
528 "i_understand": "Rozumiem",
529 "icon_style": "Styl ikony",
530 "id": "ID: ",
531 - "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!",
531 "ignor": "Ignoruj",
532 "import": "Importuj",
533 "importNFTs": "Importuj NFT",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "Zatwierdź",
1451 "wc_desc": "Łatwo łącz się z aplikacjami dApp",
1452 "yes_exit": "Tak. Wyjdź"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_pt.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Eu entendo",
530 "icon_style": "Estilo do ícone",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, verifique se o seu Ledger está ligado e desbloqueado!",
532 "ignor": "Ignorar",
533 "import": "Importar",
534 "importNFTs": "Importar NFTs",
@@ -1454,4 +1453,4 @@
1453 "wc_action_approve": "Aprovar",
1454 "wc_desc": "Ligue-se facilmente a DApps",
1455 "yes_exit": "Sim. Sair"
1457 -}
\ No newline at end of file
1456 +}
res/values/strings_pt_BR.arb
-1
@@ -524,7 +524,6 @@
524 "i_understand": "Eu entendo",
525 "icon_style": "Estilo do ícone",
526 "id": "ID: ",
527 - "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, certifique-se de que seu Ledger esteja ligado e desbloqueado!",
527 "ignor": "Ignorar",
528 "import": "Importar",
529 "importNFTs": "Importar NFTs",
res/values/strings_ru.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Я понимаю",
530 "icon_style": "Стиль иконок",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Если вы не видите свое устройство выше, пожалуйста, убедитесь, что ваш Ledger включен и разблокирован!",
532 "ignor": "Игнорировать",
533 "import": "Импорт",
534 "importNFTs": "Импортировать NFT",
@@ -1452,4 +1451,4 @@
1451 "wc_action_approve": "Одобрить",
1452 "wc_desc": "Легко подключайтесь к dApps",
1453 "yes_exit": "Да. Выйти"
1455 -}
\ No newline at end of file
1454 +}
res/values/strings_th.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "ฉันเข้าใจ",
530 "icon_style": "สไตล์ไอคอน",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "หากคุณไม่เห็นอุปกรณ์ของคุณด้านบน โปรดตรวจสอบให้แน่ใจว่า Ledger ของคุณเปิดอยู่และปลดล็อกแล้ว!",
532 "ignor": "เพิกเฉย",
533 "import": "นำเข้า",
534 "importNFTs": "นำเข้า NFT",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "อนุมัติ",
1451 "wc_desc": "เชื่อมต่อกับ DApps ได้อย่างง่ายดาย",
1452 "yes_exit": "ใช่ ออก"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_tl.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Naiintindihan ko",
530 "icon_style": "Estilo ng icon",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Kung hindi mo nakikita ang iyong device sa itaas, pakitiyak na gising at naka-unlock ang iyong Ledger!",
532 "ignor": "Huwag pansinin",
533 "import": "I-import",
534 "importNFTs": "Mag-import ng mga NFT",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "Aprubahan",
1451 "wc_desc": "Madaling kumonekta sa mga DApp",
1452 "yes_exit": "Oo. Lumabas"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_tr.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Anladım",
530 "icon_style": "Simge stili",
531 "id": "ID: ",
532 - "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!",
532 "ignor": "Yoksay",
533 "import": "İçe aktar",
534 "importNFTs": "NFT'leri İçe Aktar",
@@ -1451,4 +1450,4 @@
1450 "wc_action_approve": "Onayla",
1451 "wc_desc": "DApp'lere kolayca bağlanın",
1452 "yes_exit": "Evet. Çık"
1454 -}
\ No newline at end of file
1453 +}
res/values/strings_uk.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "Я розумію",
530 "icon_style": "Стиль значка",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "Якщо ви не бачите свій пристрій вище, будь ласка, переконайтеся, що ваш Ledger увімкнений і розблокований!",
532 "ignor": "Ігнорувати",
533 "import": "Імпорт",
534 "importNFTs": "Імпорт NFT",
@@ -1452,4 +1451,4 @@
1451 "wc_action_approve": "Схвалити",
1452 "wc_desc": "Легко підключайтеся до dApp",
1453 "yes_exit": "Так. Вийти"
1455 -}
\ No newline at end of file
1454 +}
res/values/strings_ur.arb
+1 -2
@@ -529,7 +529,6 @@
529 "i_understand": "میں سمجھ گیا/گئی ہوں",
530 "icon_style": "آئیکن اسٹائل",
531 "id": "ID: ",
532 - "if_you_dont_see_your_device": "اگر آپ کو اوپر اپنا ڈیوائس نظر نہیں آ رہا، تو براہِ کرم یقینی بنائیں کہ آپ کا Ledger آن ہے اور اَن لاک ہے!",
532 "ignor": "نظر انداز کریں",
533 "import": "درآمد کریں",
534 "importNFTs": "NFTs درآمد کریں",
@@ -1453,4 +1452,4 @@
1452 "wc_action_approve": "منظور کریں",
1453 "wc_desc": "DApps سے آسانی سے کنیکٹ کریں",
1454 "yes_exit": "ہاں۔ باہر جائیں"
1456 -}
\ No newline at end of file
1455 +}
res/values/strings_vi.arb
+1 -2
@@ -528,7 +528,6 @@
528 "i_understand": "Tôi hiểu",
529 "icon_style": "Kiểu biểu tượng",
530 "id": "ID: ",
531 - "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!",
531 "ignor": "Bỏ qua",
532 "import": "Nhập",
533 "importNFTs": "Nhập NFT",
@@ -1452,4 +1451,4 @@
1451 "wc_action_approve": "Phê duyệt",
1452 "wc_desc": "Dễ dàng kết nối với các dApp",
1453 "yes_exit": "Có. Thoát"
1455 -}
\ No newline at end of file
1454 +}
res/values/strings_yo.arb
+1 -2
@@ -530,7 +530,6 @@
530 "i_understand": "Mo ye mi",
531 "icon_style": "Aami ara",
532 "id": "ID: ",
533 - "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ẹ!",
533 "ignor": "Foju kọ",
534 "import": "Gbe wọle",
535 "importNFTs": "Gbe NFTs wọle",
@@ -1452,4 +1451,4 @@
1451 "wc_action_approve": "Fọwọ́sí",
1452 "wc_desc": "So pọ̀ mọ́ àwọn DApp ní rọrùn",
1453 "yes_exit": "Bẹ́ẹ̀ni. Jáde"
1455 -}
\ No newline at end of file
1454 +}
test/new-ui/widgets/new_future_primary_button_test.dart new
+265
@@ -0,0 +1,265 @@
1 +import "dart:async";
2 +
3 +import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart";
4 +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
5 +import "package:flutter/material.dart";
6 +import "package:flutter_svg/flutter_svg.dart";
7 +import "package:flutter_test/flutter_test.dart";
8 +
9 +const _svg = '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"/>';
10 +
11 +void main() {
12 + Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child));
13 +
14 + NewPrimaryButton inner(WidgetTester tester) =>
15 + tester.widget<NewPrimaryButton>(find.byType(NewPrimaryButton));
16 +
17 + Future<void>? fire(WidgetTester tester) {
18 + final result = (inner(tester).onPressed as dynamic)();
19 + return result is Future<void> ? result : null;
20 + }
21 +
22 + Widget build({
23 + required Future<void> Function() onPressed,
24 + bool disabled = false,
25 + SvgPicture? image,
26 + Color color = Colors.blue,
27 + Color textColor = Colors.white,
28 + Color borderColor = Colors.transparent,
29 + String text = "Send",
30 + }) =>
31 + wrap(
32 + NewFuturePrimaryButton(
33 + onPressed: onPressed,
34 + text: text,
35 + color: color,
36 + textColor: textColor,
37 + borderColor: borderColor,
38 + disabled: disabled,
39 + image: image,
40 + ),
41 + );
42 +
43 + group("NewFuturePrimaryButton", () {
44 + group("prop forwarding", () {
45 + testWidgets("passes every prop through to NewPrimaryButton", (tester) async {
46 + final image = SvgPicture.string(_svg);
47 +
48 + await tester.pumpWidget(
49 + build(
50 + onPressed: () async {},
51 + text: "Confirm",
52 + color: Colors.green,
53 + textColor: Colors.black,
54 + borderColor: Colors.red,
55 + disabled: true,
56 + image: image,
57 + ),
58 + );
59 +
60 + final button = inner(tester);
61 + expect(button.text, "Confirm");
62 + expect(button.color, Colors.green);
63 + expect(button.textColor, Colors.black);
64 + expect(button.borderColor, Colors.red);
65 + expect(button.disabled, isTrue);
66 + expect(button.image, same(image));
67 + });
68 +
69 + testWidgets("defaults: transparent border, not disabled, no image", (tester) async {
70 + await tester.pumpWidget(
71 + wrap(
72 + NewFuturePrimaryButton(
73 + onPressed: () async {},
74 + text: "Send",
75 + color: Colors.blue,
76 + textColor: Colors.white,
77 + ),
78 + ),
79 + );
80 +
81 + final button = inner(tester);
82 + expect(button.borderColor, Colors.transparent);
83 + expect(button.disabled, isFalse);
84 + expect(button.image, isNull);
85 + });
86 +
87 + testWidgets("starts out not loading", (tester) async {
88 + await tester.pumpWidget(build(onPressed: () async {}));
89 +
90 + expect(inner(tester).isLoading, isFalse);
91 + });
92 + });
93 +
94 + group("loading lifecycle", () {
95 + testWidgets("flips isLoading true while pending, false once resolved", (tester) async {
96 + final completer = Completer<void>();
97 + await tester.pumpWidget(build(onPressed: () => completer.future));
98 +
99 + final pressed = fire(tester);
100 + await tester.pump();
101 + expect(
102 + inner(tester).isLoading,
103 + isTrue,
104 + reason: "should be loading while the future is in flight",
105 + );
106 +
107 + completer.complete();
108 + await pressed;
109 + await tester.pump();
110 + expect(inner(tester).isLoading, isFalse);
111 + });
112 +
113 + testWidgets("synchronous-ish callback still settles back to not loading", (tester) async {
114 + var calls = 0;
115 + await tester.pumpWidget(build(onPressed: () async => calls++));
116 +
117 + await fire(tester);
118 + await tester.pump();
119 +
120 + expect(calls, 1);
121 + expect(inner(tester).isLoading, isFalse);
122 + });
123 +
124 + testWidgets("invokes the callback exactly once per press", (tester) async {
125 + var calls = 0;
126 + await tester.pumpWidget(build(onPressed: () async => calls++));
127 +
128 + await fire(tester);
129 + await tester.pump();
130 + await fire(tester);
131 + await tester.pump();
132 +
133 + expect(calls, 2);
134 + });
135 + });
136 +
137 + testWidgets("ignores presses while a previous future is pending", (tester) async {
138 + final completer = Completer<void>();
139 + var calls = 0;
140 + await tester.pumpWidget(
141 + build(
142 + onPressed: () {
143 + calls++;
144 + return completer.future;
145 + },
146 + ),
147 + );
148 +
149 + final pressed = fire(tester);
150 + await tester.pump();
151 +
152 + unawaited(fire(tester));
153 + unawaited(fire(tester));
154 + unawaited(fire(tester));
155 + await tester.pump();
156 +
157 + expect(calls, 1, reason: "extra presses must be swallowed");
158 +
159 + completer.complete();
160 + await pressed;
161 + await tester.pump();
162 +
163 + await fire(tester);
164 + await tester.pump();
165 + expect(calls, 2);
166 + });
167 +
168 + group("handle errors", () {
169 + testWidgets("clears the loading flag when the callback throws", (tester) async {
170 + await tester.pumpWidget(
171 + build(onPressed: () async => throw StateError("boom")),
172 + );
173 +
174 + await expectLater(fire(tester), throwsA(isA<StateError>()));
175 + await tester.pump();
176 +
177 + expect(inner(tester).isLoading, isFalse, reason: "the finally block must reset the flag");
178 + });
179 +
180 + testWidgets("rethrows instead of swallowing the failure", (tester) async {
181 + await tester.pumpWidget(
182 + build(onPressed: () async => throw StateError("boom")),
183 + );
184 +
185 + await expectLater(fire(tester), throwsA(isA<StateError>()));
186 + });
187 +
188 + testWidgets("stays pressable after the callback throws", (tester) async {
189 + var calls = 0;
190 + await tester.pumpWidget(
191 + build(
192 + onPressed: () {
193 + calls++;
194 + throw StateError("boom");
195 + },
196 + ),
197 + );
198 +
199 + await expectLater(fire(tester), throwsA(isA<StateError>()));
200 + await tester.pump();
201 + await expectLater(fire(tester), throwsA(isA<StateError>()));
202 + await tester.pump();
203 +
204 + expect(calls, 2, reason: "the re-entrancy guard must not latch on failure");
205 + expect(inner(tester).isLoading, isFalse);
206 + });
207 + });
208 +
209 + group("disposal mid-flight", () {
210 + testWidgets("does not setState after being disposed", (tester) async {
211 + final completer = Completer<void>();
212 + await tester.pumpWidget(build(onPressed: () => completer.future));
213 +
214 + final pressed = fire(tester);
215 + await tester.pump();
216 +
217 + await tester.pumpWidget(wrap(const SizedBox.shrink()));
218 + expect(find.byType(NewPrimaryButton), findsNothing);
219 +
220 + completer.complete();
221 + await pressed;
222 + await tester.pump();
223 +
224 + expect(
225 + tester.takeException(),
226 + isNull,
227 + reason: "the mounted check must guard the second setState",
228 + );
229 + });
230 +
231 + testWidgets("still propagates a throw when disposed", (tester) async {
232 + final completer = Completer<void>();
233 + await tester.pumpWidget(build(onPressed: () => completer.future));
234 +
235 + final pressed = fire(tester);
236 + await tester.pump();
237 + await tester.pumpWidget(wrap(const SizedBox.shrink()));
238 +
239 + completer.completeError(StateError("boom"));
240 +
241 + await expectLater(pressed, throwsA(isA<StateError>()));
242 + expect(
243 + tester.takeException(),
244 + isNull,
245 + reason: "no setState-after-dispose FlutterError on the error path",
246 + );
247 + });
248 + });
249 +
250 + testWidgets("disabled is forwarded but not enforced by this wrapper", (tester) async {
251 + // Press-blocking lives in NewPrimaryButton; this widget only relays the
252 + // flag. If that ever moves up here, this expectation should change.
253 + var calls = 0;
254 + await tester.pumpWidget(
255 + build(onPressed: () async => calls++, disabled: true),
256 + );
257 +
258 + expect(inner(tester).disabled, isTrue);
259 +
260 + await fire(tester);
261 + await tester.pump();
262 + expect(calls, 1);
263 + });
264 + });
265 +}
tool/configure.dart
+2
@@ -442,6 +442,8 @@ abstract class Monero {
442 Map<String, String> exportOutputsUR(Object wallet);
443
444 bool needExportOutputs(Object wallet, Money amount);
445 +
446 + bool hasUnknownKeyImages(Object wallet);
447
448 bool importKeyImagesUR(Object wallet, String ur);
449