CW-1581-trezor-fixes-enhancements (#3462)
What changed, and why it matters
This commit improves how Cake Wallet connects to Trezor hardware wallets and synchronizes Monero 'key images' (data that proves coins haven't already been spent). It adds automatic reconnection, a new user interface for syncing key images, and stores an encrypted Trezor pairing state on the device. The changes are mostly user-experience and reliability fixes rather than a clear security patch, though they touch on sensitive areas such as encrypted storage, hardware wallet pairing, and transaction signing flow.
Reviewers should verify that the encrypted Trezor state file cannot be downgraded or replayed, that auto-pairing credentials are validated before reuse, and that the new send-page routing does not bypass the existing needExportOutputs check. The trezor_flutter dependency bump should be audited for security-relevant changes, and the SecureStorage key handling should be checked for correct key rotation and deletion on wallet removal.
Security signals we found
New encrypted on-disk storage of Trezor pairing state (thp_state.json.enc) keyed by SecureStorage
Auto-pairing credential retrieval from TrezorClientV2 and persistence without explicit user confirmation
Changes to Monero transaction send flow routing based on hasUnknownKeyImages()
Dependency bump for trezor_flutter and trezor_usb_transport to a new git ref
Addition of async button wrapper that prevents re-entrant presses during pending futures
Evidence from the diff
The patch updates the trezor_flutter dependency, introduces Trezor auto-pairing credentials, persists an encrypted sdk.ThpState using SecureStorage plus EncryptionFileUtils, adds hasUnknownKeyImages() plumbing through cw_monero/lib/monero_wallet.dart and tool/configure.dart, and changes the send flow to route to a new sync-key-images sheet when unknown key images exist for a Trezor/Monero wallet. It also refactors the send button to use a new NewFuturePrimaryButton wrapper that manages async loading state. A large portion of the diff is localization string updates and a new SVG icon.
Changed components
cw_monero/lib/monero_wallet.dartcw_monero/lib/trezor.dartlib/view_model/hardware_wallet/trezor_connect_view_model.dartlib/new-ui/pages/send_page.dartlib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dartlib/router.dartlib/di.dartcw_monero/pubspec.yaml / pubspec.lock (trezor_flutter ref)Inspect captured patch +855 / −189
diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart
index d0e6539..afd9284 100644
--- a/cw_monero/lib/monero_wallet.dart
+++ b/cw_monero/lib/monero_wallet.dart
@@ -370,6 +370,8 @@ abstract class MoneroWalletBase
};
}
+ bool hasUnknownKeyImages() => currentWallet!.hasUnknownKeyImages();
+
bool needExportOutputs(Money amount) {
if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
return false;
diff --git a/cw_monero/lib/trezor.dart b/cw_monero/lib/trezor.dart
index a9a3879..b851859 100644
--- a/cw_monero/lib/trezor.dart
+++ b/cw_monero/lib/trezor.dart
@@ -1,27 +1,28 @@
-import 'dart:convert';
+import "dart:convert";
-import 'package:cw_core/hardware/hardware_wallet_service.dart';
-import 'package:trezor_flutter/trezor_flutter.dart';
+import "package:cw_core/hardware/hardware_wallet_service.dart";
+import "package:trezor_flutter/trezor_flutter.dart";
class MoneroTrezorService extends HardwareWalletService {
- final TrezorClient client;
-
MoneroTrezorService(this.client);
+
+ final TrezorClient client;
}
class MoneroTrezorWatchCredentials {
+ const MoneroTrezorWatchCredentials(this.watchKey, this.address);
+
final String watchKey;
final String address;
-
- const MoneroTrezorWatchCredentials(this.watchKey, this.address);
}
class Trezor {
- final MoneroTrezorService service;
-
Trezor(this.service);
+ final MoneroTrezorService service;
+
String? _sessionPassphrase;
+
Future<void> newPassphraseSession(String? passphrase) async {
if (passphrase == null) return;
@@ -42,21 +43,23 @@ class Trezor {
final txIds = <MoneroKeyImageTxData>[];
for (final tdi in tdisList) {
- txIds.add(MoneroKeyImageTxData(
- outKey: tdi["out_key"],
- txPubKey: tdi["tx_pub_key"],
- internalOutputIndex: tdi["internal_output_index"],
- subAddrMajor: tdi["sub_addr_major"],
- subAddrMinor: tdi["sub_addr_minor"],
+ txIds.add(
+ MoneroKeyImageTxData(
+ outKey: tdi["out_key"] as String,
+ txPubKey: tdi["tx_pub_key"] as String,
+ internalOutputIndex: tdi["internal_output_index"] as int,
+ subAddrMajor: tdi["sub_addr_major"] as int,
+ subAddrMinor: tdi["sub_addr_minor"] as int,
additionalTxPubKeys:
- (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? []));
+ (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [],
+ ),
+ );
}
final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds);
return jsonEncode(keyImages.toMap());
}
- Future<String> signTransaction(String json) async {
- return TrezorMonero(service.client).signTransaction(jsonDecode(json));
- }
+ Future<String> signTransaction(String json) =>
+ TrezorMonero(service.client).signTransaction(jsonDecode(json) as Map<String, dynamic>);
}
diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock
index 275e7d4..0cc1a9e 100644
--- a/cw_monero/pubspec.lock
+++ b/cw_monero/pubspec.lock
@@ -921,8 +921,8 @@ packages:
dependency: "direct main"
description:
path: trezor-flutter
- ref: "944080d950a62dd48a199b9736237cbe580351de"
- resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
+ ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
+ resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.1.0"
@@ -930,8 +930,8 @@ packages:
dependency: transitive
description:
path: trezor_usb_transport
- ref: "944080d950a62dd48a199b9736237cbe580351de"
- resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
+ ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
+ resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.0.1"
diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml
index 692103e..3f1c10f 100644
--- a/cw_monero/pubspec.yaml
+++ b/cw_monero/pubspec.yaml
@@ -33,7 +33,7 @@ dependencies:
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter.git
- ref: 944080d950a62dd48a199b9736237cbe580351de
+ ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97
path: trezor-flutter
dev_dependencies:
diff --git a/lib/di.dart b/lib/di.dart
index 6bb8bd9..da00561 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -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>()));
diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart
index 7d2b8ec..d1e421d 100644
--- a/lib/monero/cw_monero.dart
+++ b/lib/monero/cw_monero.dart
@@ -420,6 +420,12 @@ class CWMonero extends Monero {
return moneroWallet.needExportOutputs(amount);
}
+ @override
+ bool hasUnknownKeyImages(Object wallet) {
+ final moneroWallet = wallet as MoneroWallet;
+ return moneroWallet.hasUnknownKeyImages();
+ }
+
@override
void monerocCheck() => checkIfMoneroCIsFine();
diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart
index d25b344..1b379df 100644
--- a/lib/new-ui/pages/send_page.dart
+++ b/lib/new-ui/pages/send_page.dart
@@ -19,6 +19,7 @@ import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
+import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/picker.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
@@ -42,7 +43,6 @@ import "package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_shee
import "package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart";
-import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/src/widgets/standard_checkbox.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/utils/payment_request.dart";
@@ -618,9 +618,9 @@ class _NewSendPageState extends State<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)) {
diff --git a/lib/new-ui/pages/settings_page.dart b/lib/new-ui/pages/settings_page.dart
index 2183ec5..8aaec52 100644
--- a/lib/new-ui/pages/settings_page.dart
+++ b/lib/new-ui/pages/settings_page.dart
@@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_info.dart';
+import "package:cw_core/wallet_type.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";
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),
]);
diff --git a/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart b/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart
new file mode 100644
index 0000000..d8febdc
--- /dev/null
+++ b/lib/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart
@@ -0,0 +1,224 @@
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
+import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart";
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
+import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
+import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/src/widgets/primary_button.dart";
+import "package:cake_wallet/store/app_store.dart";
+import "package:cake_wallet/utils/show_pop_up.dart";
+import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart";
+import "package:cw_core/wallet_base.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:flutter/cupertino.dart";
+import "package:flutter/material.dart";
+
+class SyncKeyImagesSheet extends StatefulWidget {
+ const SyncKeyImagesSheet({
+ required this.appStore,
+ required this.trezorConnectVM,
+ super.key,
+ });
+
+ final AppStore appStore;
+ final TrezorConnectViewModelBase trezorConnectVM;
+
+ WalletBase get wallet => appStore.wallet!;
+
+ @override
+ State<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;
+}
diff --git a/lib/new-ui/widgets/new_future_primary_button.dart b/lib/new-ui/widgets/new_future_primary_button.dart
new file mode 100644
index 0000000..2440f19
--- /dev/null
+++ b/lib/new-ui/widgets/new_future_primary_button.dart
@@ -0,0 +1,57 @@
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
+import "package:flutter/material.dart";
+import "package:flutter_svg/flutter_svg.dart";
+
+class NewFuturePrimaryButton extends StatefulWidget {
+ const NewFuturePrimaryButton({
+ required this.onPressed,
+ required this.text,
+ required this.color,
+ required this.textColor,
+ this.borderColor = Colors.transparent,
+ this.disabled = false,
+ this.image,
+ super.key,
+ });
+
+ final Future<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,
+ );
+}
diff --git a/lib/router.dart b/lib/router.dart
index 837eab8..39a28a4 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -4,7 +4,6 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
-import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
@@ -13,6 +12,7 @@ import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
import "package:cake_wallet/new-ui/pages/receive_page.dart";
import 'package:cake_wallet/new-ui/pages/send_page.dart';
+import 'package:cake_wallet/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
@@ -51,6 +51,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
+import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
@@ -126,7 +127,6 @@ import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
-import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
@@ -146,7 +146,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
-import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
+import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
@@ -908,6 +908,19 @@ Route<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));
diff --git a/lib/routes.dart b/lib/routes.dart
index 236196b..b21e2bc 100644
--- a/lib/routes.dart
+++ b/lib/routes.dart
@@ -133,6 +133,7 @@ class Routes {
static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
+ static const syncKeyImagesDevices = '/device/sync_key_images';
static const urqrAnimatedPage = '/urqr/animated_page';
static const walletGroupsDisplayPage = '/wallet_groups_display_page';
static const walletGroupDescription = '/wallet_group_description';
diff --git a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart
index c048edf..28116e3 100644
--- a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart
+++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart
@@ -1,43 +1,43 @@
-import 'dart:async';
-import 'dart:io';
-
-import '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;
-
-part 'trezor_connect_view_model.g.dart';
+import "dart:async";
+import "dart:convert";
+import "dart:io";
+
+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";
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,8 +276,60 @@ 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;
}
}
@@ -241,6 +338,7 @@ abstract class TrezorParingState {
static TrezorParingState enterPin = EnterPinTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState success = SuccessTrezorParingState();
+
static TrezorParingState fail(String message) => FailTrezorParingState(message);
}
@@ -253,7 +351,7 @@ class VerifyingPinTrezorParingState extends TrezorParingState {}
class SuccessTrezorParingState extends TrezorParingState {}
class FailTrezorParingState extends TrezorParingState {
- final String message;
-
FailTrezorParingState(this.message);
+
+ final String message;
}
diff --git a/res/pictures/settings_row_icons/sync-balance.svg b/res/pictures/settings_row_icons/sync-balance.svg
new file mode 100644
index 0000000..fc3274a
--- /dev/null
+++ b/res/pictures/settings_row_icons/sync-balance.svg
@@ -0,0 +1,19 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_23056_316844)">
+<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)"/>
+<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)"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_23056_316844" x1="11.9531" y1="-0.219238" x2="11.9531" y2="23.7808" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F2C738"/>
+<stop offset="1" stop-color="#745C0E"/>
+</linearGradient>
+<linearGradient id="paint1_linear_23056_316844" x1="11.6016" y1="4.8501" x2="11.6016" y2="18.3501" gradientUnits="userSpaceOnUse">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#E3D376"/>
+</linearGradient>
+<clipPath id="clip0_23056_316844">
+<rect width="24" height="24" rx="5" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb
index 125537f..815de28 100644
--- a/res/values/strings_ar.arb
+++ b/res/values/strings_ar.arb
@@ -529,7 +529,6 @@
"i_understand": "أفهم",
"icon_style": "نمط الأيقونة",
"id": "المعرّف: ",
- "if_you_dont_see_your_device": "إذا لم يظهر جهازك أعلاه، يُرجى التأكد من أن Ledger قيد التشغيل ومفتوح القفل!",
"ignor": "تجاهل",
"import": "استيراد",
"importNFTs": "استيراد رموز NFT",
@@ -1451,4 +1450,4 @@
"wallet_balance": "رصيد المحفظة",
"wc_desc": "اتصل بسهولة بتطبيقات DApps",
"yes_exit": "نعم. اخرج"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb
index 59d11ed..45136de 100644
--- a/res/values/strings_bg.arb
+++ b/res/values/strings_bg.arb
@@ -529,7 +529,6 @@
"i_understand": "Разбирам",
"icon_style": "Стил на иконата",
"id": "ID: ",
- "if_you_dont_see_your_device": "Ако не виждате устройството си по-горе, моля, уверете се, че вашият Ledger е буден и отключен!",
"ignor": "Игнорирай",
"import": "Импортиране",
"importNFTs": "Импортиране на NFT",
@@ -1451,4 +1450,4 @@
"wallet_balance": "Баланс на портфейла",
"wc_desc": "Свържете се лесно с dApps",
"yes_exit": "Да. Изход"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb
index c2c5c49..c3df26c 100644
--- a/res/values/strings_cs.arb
+++ b/res/values/strings_cs.arb
@@ -529,7 +529,6 @@
"i_understand": "Rozumím",
"icon_style": "Styl ikony",
"id": "ID: ",
- "if_you_dont_see_your_device": "Pokud své zařízení výše nevidíte, ujistěte se, že je váš Ledger zapnutý a odemčený!",
"ignor": "Ignorovat",
"import": "Importovat",
"importNFTs": "Importovat NFT",
@@ -1451,4 +1450,4 @@
"wallet_balance": "Zůstatek peněženky",
"wc_desc": "Snadno se připojte k dApps",
"yes_exit": "Ano. Ukončit"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb
index 2b8d0b5..9a39f7b 100644
--- a/res/values/strings_de.arb
+++ b/res/values/strings_de.arb
@@ -530,7 +530,7 @@
"i_understand": "Ich verstehe",
"icon_style": "Symbolstil",
"id": "ID: ",
- "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Ledger eingeschaltet und entsperrt ist!",
+ "if_you_dont_see_your_device": "Wenn Sie Ihr Gerät oben nicht sehen, stellen Sie bitte sicher, dass Ihr Gerät eingeschaltet und entsperrt ist!",
"ignore": "Ignorieren",
"import": "Importieren",
"importNFTs": "NFTs importieren",
@@ -882,6 +882,9 @@
"restore_wallet": "Wallet wiederherstellen",
"restore_wallet_name": "Wallet-Name",
"restore_wallet_restore_description": "Beschreibung der Wallet-Wiederherstellung",
+ "resync_device": "Gerät resyncen",
+ "resync_device_desc": "Um das Guthaben aktuell zu halten muss Cake Wallet gelegentlich sich mit dem Gerät synchonisieren.",
+ "resync_device_cancel_warning_desc": "Dadurch wird der laufende Synchronisierungsprozess mit Ihrem Hardware-Gerät beendet.",
"return_address_desc": "Im seltenen Fall eines fehlgeschlagenen Swaps können unsere Partner Ihre Zahlung automatisch an die angegebene Rückerstattungsadresse erstatten.",
"return_address_desc_external": "Da Sie „Von extern senden“ ausgewählt haben, legen Sie bitte unten manuell eine Erstattungsadresse fest.",
"robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrer Debitkarte, Ihrem Bankkonto oder Ihrem Robinhood-Guthaben. Nur USA.",
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index d81f3dd..30e506a 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -544,11 +544,11 @@
"how_to_connect": "How to Connect",
"how_to_use": "How to use",
"how_to_use_card": "How to use this card",
- "hww_exit_desc": "This will ed the ongoing pairing process with your hardware device",
+ "hww_exit_desc": "This will end the ongoing pairing process with your hardware device",
"i_understand": "I understand",
"icon_style": "Icon style",
"id": "ID: ",
- "if_you_dont_see_your_device": "If you don't see your device above, please be sure your Ledger is awake and unlocked!",
+ "if_you_dont_see_your_device": "If you don't see your device above, please be sure your device is awake and unlocked!",
"ignor": "Ignore",
"import": "Import",
"importNFTs": "Import NFTs",
@@ -926,6 +926,9 @@
"restore_wallet": "Restore Wallet",
"restore_wallet_name": "Wallet name",
"restore_wallet_restore_description": "Wallet restore description",
+ "resync_device": "Resync device",
+ "resync_device_desc": "Balance updating requires occasional resyncing Cake Wallet with the device.",
+ "resync_device_cancel_warning_desc": "This will end the ongoing sync process with your hardware device",
"return_address_desc": "In the rare case of a failed swap, our partners can automatically refund your payment to the provided refund address.",
"return_address_desc_external": "Since you chose Send From External, please set a refund address manually below.",
"robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.",
diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb
index 91c41c6..4f3590f 100644
--- a/res/values/strings_es.arb
+++ b/res/values/strings_es.arb
@@ -529,7 +529,6 @@
"i_understand": "Entiendo",
"icon_style": "Estilo de icono",
"id": "ID: ",
- "if_you_dont_see_your_device": "Si no ves tu dispositivo arriba, asegúrate de que tu Ledger esté encendido y desbloqueado.",
"ignor": "Ignorar",
"import": "Importar",
"importNFTs": "Importar NFTs",
@@ -1454,4 +1453,4 @@
"wallet_balance": "Saldo de la cartera",
"wc_desc": "Conéctate fácilmente a dApps",
"yes_exit": "Sí. Salir"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb
index 40b83e1..6a15e0c 100644
--- a/res/values/strings_fa.arb
+++ b/res/values/strings_fa.arb
@@ -527,7 +527,6 @@
"i_understand": "متوجه شدم",
"icon_style": "سبک آیکون",
"id": "شناسه: ",
- "if_you_dont_see_your_device": "اگر دستگاه خود را در بالا نمیبینید، لطفاً مطمئن شوید که Ledger شما روشن است و قفل آن باز شده است!",
"ignor": "نادیده گرفتن",
"import": "درونریزی",
"importNFTs": "وارد کردن NFTها",
@@ -1451,4 +1450,4 @@
"wallet_balance": "موجودی کیف پول",
"wc_desc": "بهراحتی به dAppها متصل شوید",
"yes_exit": "بله. خروج"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb
index 5327d20..397529f 100644
--- a/res/values/strings_fr.arb
+++ b/res/values/strings_fr.arb
@@ -529,7 +529,6 @@
"i_understand": "Je comprends",
"icon_style": "Style d'icône",
"id": "ID : ",
- "if_you_dont_see_your_device": "Si vous ne voyez pas votre appareil ci‑dessus, assurez‑vous que votre Ledger est allumé et déverrouillé !",
"ignor": "Ignorer",
"import": "Importer",
"importNFTs": "Importer des NFTs",
@@ -1451,4 +1450,4 @@
"wallet_balance": "Solde du portefeuille",
"wc_desc": "Connectez-vous facilement aux dApps",
"yes_exit": "Oui. Quitter"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb
index 693fe80..7cb149a 100644
--- a/res/values/strings_gn.arb
+++ b/res/values/strings_gn.arb
@@ -1285,7 +1285,6 @@
"hide": "Mokañy",
"how_to_connect": "Mba'éichapa Oñembojoaju",
"hww_exit_desc": "Kóva omboty pe joaju oikóva ne hardware mba'e'oka ndive",
- "if_you_dont_see_your_device": "Ndehecháiramo ne mba'e'oka yvate, ehecha porãke ne Ledger hesãi ha ipe'apyre!",
"incorrect_seed_option": "Hekope'ỹ. Eha'ã jey",
"incorrect_seed_option_back": "Hekope'ỹ. Ehecha porãke ne seed oñeñongatu hekopete ha eha'ã jey.",
"input": "Jeikeha (input)",
@@ -1459,4 +1458,4 @@
"you_will_receive_estimated_amount": "Rehupytýta (jehaipyre)",
"youCanGoBackToYourDapp": "Ikatúma rejevy ne dApp-pe",
"your": "Nde"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb
index b4c1423..db0b678 100644
--- a/res/values/strings_ha.arb
+++ b/res/values/strings_ha.arb
@@ -529,7 +529,6 @@
"i_understand": "Na fahimta",
"icon_style": "Salon icon",
"id": "ID: ",
- "if_you_dont_see_your_device": "Idan ba ka ga na'urarka a sama ba, da fatan ka tabbatar Ledger ɗinka yana kunne kuma a buɗe!",
"ignor": "Yi watsi",
"import": "Shigo da",
"importNFTs": "Shigo da NFT",
@@ -1454,4 +1453,4 @@
"wallet_balance": "Ma'aunin walat",
"wc_desc": "Haɗa da DApps cikin sauƙi",
"yes_exit": "Eh. Fita"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb
index 6f21d5d..413a191 100644
--- a/res/values/strings_hi.arb
+++ b/res/values/strings_hi.arb
@@ -529,7 +529,6 @@
"i_understand": "मैं समझता/समझती हूँ",
"icon_style": "चिह्न शैली",
"id": "ID: ",
- "if_you_dont_see_your_device": "यदि आपको ऊपर अपना डिवाइस नहीं दिख रहा है, तो कृपया सुनिश्चित करें कि आपका Ledger चालू है और अनलॉक है!",
"ignor": "नज़रअंदाज़ करें",
"import": "इम्पोर्ट",
"importNFTs": "NFTs आयात करें",
@@ -1453,4 +1452,4 @@
"wallet_balance": "वॉलेट बैलेंस",
"wc_desc": "DApps से आसानी से कनेक्ट करें",
"yes_exit": "हाँ। बाहर निकलें"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb
index eb66c8e..11a9ffe 100644
--- a/res/values/strings_hr.arb
+++ b/res/values/strings_hr.arb
@@ -529,7 +529,6 @@
"i_understand": "Razumijem",
"icon_style": "Stil ikone",
"id": "ID: ",
- "if_you_dont_see_your_device": "Ako gore ne vidite svoj uređaj, provjerite je li vaš Ledger uključen i otključan!",
"ignor": "Zanemari",
"import": "Uvoz",
"importNFTs": "Uvezi NFT-ove",
@@ -1451,4 +1450,4 @@
"wallet_balance": "Stanje novčanika",
"wc_desc": "Jednostavno se povežite s DApp-ovima",
"yes_exit": "Da. Izađi"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb
index c81c4e4..0d0c47e 100644
--- a/res/values/strings_hy.arb
+++ b/res/values/strings_hy.arb
@@ -529,7 +529,6 @@
"i_understand": "Հասկացա",
"icon_style": "Սրբապատկերների ոճը",
"id": "ID: ",
- "if_you_dont_see_your_device": "Եթե վերևում չեք տեսնում ձեր սարքը, համոզվեք, որ ձեր Ledger-ը միացված է և ապակողպված է։",
"ignor": "Անտեսել",
"import": "Ներմուծում",
"importNFTs": "Ներմուծել NFT-ներ",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "Հաստատել",
"wc_desc": "Հեշտությամբ միացեք DApps-երին",
"yes_exit": "Այո։ Դուրս գալ"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb
index 877fac3..ae2e5ca 100644
--- a/res/values/strings_id.arb
+++ b/res/values/strings_id.arb
@@ -529,7 +529,6 @@
"i_understand": "Saya mengerti",
"icon_style": "Gaya ikon",
"id": "ID: ",
- "if_you_dont_see_your_device": "Jika Anda tidak melihat perangkat Anda di atas, pastikan Ledger Anda menyala dan tidak terkunci!",
"ignor": "Abaikan",
"import": "Impor",
"importNFTs": "Impor NFT",
@@ -1454,4 +1453,4 @@
"wc_action_approve": "Setujui",
"wc_desc": "Hubungkan dengan mudah ke DApps",
"yes_exit": "Ya. Keluar"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb
index 2bd2285..7997f91 100644
--- a/res/values/strings_it.arb
+++ b/res/values/strings_it.arb
@@ -529,7 +529,6 @@
"i_understand": "Ho capito",
"icon_style": "Stile dell'icona",
"id": "ID: ",
- "if_you_dont_see_your_device": "Se non vedi il tuo dispositivo qui sopra, assicurati che il tuo Ledger sia acceso e sbloccato!",
"ignor": "Ignora",
"import": "Importa",
"importNFTs": "Importa NFT",
@@ -1454,4 +1453,4 @@
"wc_action_approve": "Approva",
"wc_desc": "Connettiti facilmente alle dApp",
"yes_exit": "Sì. Esci"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb
index 87f1193..f8ab953 100644
--- a/res/values/strings_ja.arb
+++ b/res/values/strings_ja.arb
@@ -530,7 +530,6 @@
"i_understand": "理解しました",
"icon_style": "アイコンのスタイル",
"id": "ID: ",
- "if_you_dont_see_your_device": "上記にデバイスが表示されない場合は、Ledgerが起動していてロック解除されていることを確認してください!",
"ignor": "無視",
"import": "インポート",
"importNFTs": "NFTをインポート",
@@ -1452,4 +1451,4 @@
"wc_action_approve": "承認",
"wc_desc": "dApp に簡単に接続",
"yes_exit": "はい。終了する"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb
index b756745..299814e 100644
--- a/res/values/strings_ko.arb
+++ b/res/values/strings_ko.arb
@@ -529,7 +529,6 @@
"i_understand": "이해했습니다",
"icon_style": "아이콘 스타일",
"id": "ID: ",
- "if_you_dont_see_your_device": "위에서 기기가 보이지 않으면 Ledger가 켜져 있고 잠금 해제되어 있는지 확인하세요!",
"ignor": "무시",
"import": "가져오기",
"importNFTs": "NFT 가져오기",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "승인",
"wc_desc": "DApp에 손쉽게 연결",
"yes_exit": "예. 종료"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb
index 51f7c23..d58b1ad 100644
--- a/res/values/strings_my.arb
+++ b/res/values/strings_my.arb
@@ -529,7 +529,6 @@
"i_understand": "နားလည်ပါတယ်",
"icon_style": "အိုင်ကွန်စတိုင်",
"id": "ID: ",
- "if_you_dont_see_your_device": "အထက်မှာ သင့်စက်ကို မတွေ့ပါက သင့် Ledger ကို ဖွင့်ထားပြီး လော့ခ်ဖွင့်ထားကြောင်း သေချာပါစေ။",
"ignor": "လျစ်လျူရှုပါ",
"import": "တင်သွင်း",
"importNFTs": "NFT များကို တင်သွင်းပါ",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "အတည်ပြု",
"wc_desc": "dApp များသို့ လွယ်ကူစွာ ချိတ်ဆက်ပါ",
"yes_exit": "ဟုတ်ကဲ့။ ထွက်ပါ"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb
index 9285015..d538ef2 100644
--- a/res/values/strings_nl.arb
+++ b/res/values/strings_nl.arb
@@ -527,7 +527,6 @@
"i_understand": "Ik begrijp het",
"icon_style": "Pictogramstijl",
"id": "ID: ",
- "if_you_dont_see_your_device": "Als je je apparaat hierboven niet ziet, zorg er dan voor dat je Ledger ontgrendeld is!",
"ignor": "Negeren",
"import": "Importeren",
"importNFTs": "NFT's importeren",
@@ -1452,4 +1451,4 @@
"wc_action_approve": "Goedkeuren",
"wc_desc": "Maak eenvoudig verbinding met dApps",
"yes_exit": "Ja. Afsluiten"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb
index 16045cd..5301630 100644
--- a/res/values/strings_pl.arb
+++ b/res/values/strings_pl.arb
@@ -528,7 +528,6 @@
"i_understand": "Rozumiem",
"icon_style": "Styl ikony",
"id": "ID: ",
- "if_you_dont_see_your_device": "Jeśli nie widzisz swojego urządzenia powyżej, upewnij się, że Twój Ledger jest aktywny i odblokowany!",
"ignor": "Ignoruj",
"import": "Importuj",
"importNFTs": "Importuj NFT",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "Zatwierdź",
"wc_desc": "Łatwo łącz się z aplikacjami dApp",
"yes_exit": "Tak. Wyjdź"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb
index 5ef972b..e357dc8 100644
--- a/res/values/strings_pt.arb
+++ b/res/values/strings_pt.arb
@@ -529,7 +529,6 @@
"i_understand": "Eu entendo",
"icon_style": "Estilo do ícone",
"id": "ID: ",
- "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, verifique se o seu Ledger está ligado e desbloqueado!",
"ignor": "Ignorar",
"import": "Importar",
"importNFTs": "Importar NFTs",
@@ -1454,4 +1453,4 @@
"wc_action_approve": "Aprovar",
"wc_desc": "Ligue-se facilmente a DApps",
"yes_exit": "Sim. Sair"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb
index ea1a3a4..2151363 100644
--- a/res/values/strings_pt_BR.arb
+++ b/res/values/strings_pt_BR.arb
@@ -524,7 +524,6 @@
"i_understand": "Eu entendo",
"icon_style": "Estilo do ícone",
"id": "ID: ",
- "if_you_dont_see_your_device": "Se você não vir seu dispositivo acima, certifique-se de que seu Ledger esteja ligado e desbloqueado!",
"ignor": "Ignorar",
"import": "Importar",
"importNFTs": "Importar NFTs",
diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb
index aaac422..9ce39e0 100644
--- a/res/values/strings_ru.arb
+++ b/res/values/strings_ru.arb
@@ -529,7 +529,6 @@
"i_understand": "Я понимаю",
"icon_style": "Стиль иконок",
"id": "ID: ",
- "if_you_dont_see_your_device": "Если вы не видите свое устройство выше, пожалуйста, убедитесь, что ваш Ledger включен и разблокирован!",
"ignor": "Игнорировать",
"import": "Импорт",
"importNFTs": "Импортировать NFT",
@@ -1452,4 +1451,4 @@
"wc_action_approve": "Одобрить",
"wc_desc": "Легко подключайтесь к dApps",
"yes_exit": "Да. Выйти"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb
index 6691539..9c57593 100644
--- a/res/values/strings_th.arb
+++ b/res/values/strings_th.arb
@@ -529,7 +529,6 @@
"i_understand": "ฉันเข้าใจ",
"icon_style": "สไตล์ไอคอน",
"id": "ID: ",
- "if_you_dont_see_your_device": "หากคุณไม่เห็นอุปกรณ์ของคุณด้านบน โปรดตรวจสอบให้แน่ใจว่า Ledger ของคุณเปิดอยู่และปลดล็อกแล้ว!",
"ignor": "เพิกเฉย",
"import": "นำเข้า",
"importNFTs": "นำเข้า NFT",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "อนุมัติ",
"wc_desc": "เชื่อมต่อกับ DApps ได้อย่างง่ายดาย",
"yes_exit": "ใช่ ออก"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb
index 8e23566..91b49c2 100644
--- a/res/values/strings_tl.arb
+++ b/res/values/strings_tl.arb
@@ -529,7 +529,6 @@
"i_understand": "Naiintindihan ko",
"icon_style": "Estilo ng icon",
"id": "ID: ",
- "if_you_dont_see_your_device": "Kung hindi mo nakikita ang iyong device sa itaas, pakitiyak na gising at naka-unlock ang iyong Ledger!",
"ignor": "Huwag pansinin",
"import": "I-import",
"importNFTs": "Mag-import ng mga NFT",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "Aprubahan",
"wc_desc": "Madaling kumonekta sa mga DApp",
"yes_exit": "Oo. Lumabas"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb
index beb1e6f..765fa2f 100644
--- a/res/values/strings_tr.arb
+++ b/res/values/strings_tr.arb
@@ -529,7 +529,6 @@
"i_understand": "Anladım",
"icon_style": "Simge stili",
"id": "ID: ",
- "if_you_dont_see_your_device": "Cihazınızı yukarıda görmüyorsanız, lütfen Ledger'ınızın açık, uyanık ve kilidinin açılmış olduğundan emin olun!",
"ignor": "Yoksay",
"import": "İçe aktar",
"importNFTs": "NFT'leri İçe Aktar",
@@ -1451,4 +1450,4 @@
"wc_action_approve": "Onayla",
"wc_desc": "DApp'lere kolayca bağlanın",
"yes_exit": "Evet. Çık"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb
index 53fee6e..f9421be 100644
--- a/res/values/strings_uk.arb
+++ b/res/values/strings_uk.arb
@@ -529,7 +529,6 @@
"i_understand": "Я розумію",
"icon_style": "Стиль значка",
"id": "ID: ",
- "if_you_dont_see_your_device": "Якщо ви не бачите свій пристрій вище, будь ласка, переконайтеся, що ваш Ledger увімкнений і розблокований!",
"ignor": "Ігнорувати",
"import": "Імпорт",
"importNFTs": "Імпорт NFT",
@@ -1452,4 +1451,4 @@
"wc_action_approve": "Схвалити",
"wc_desc": "Легко підключайтеся до dApp",
"yes_exit": "Так. Вийти"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb
index b283752..89c767e 100644
--- a/res/values/strings_ur.arb
+++ b/res/values/strings_ur.arb
@@ -529,7 +529,6 @@
"i_understand": "میں سمجھ گیا/گئی ہوں",
"icon_style": "آئیکن اسٹائل",
"id": "ID: ",
- "if_you_dont_see_your_device": "اگر آپ کو اوپر اپنا ڈیوائس نظر نہیں آ رہا، تو براہِ کرم یقینی بنائیں کہ آپ کا Ledger آن ہے اور اَن لاک ہے!",
"ignor": "نظر انداز کریں",
"import": "درآمد کریں",
"importNFTs": "NFTs درآمد کریں",
@@ -1453,4 +1452,4 @@
"wc_action_approve": "منظور کریں",
"wc_desc": "DApps سے آسانی سے کنیکٹ کریں",
"yes_exit": "ہاں۔ باہر جائیں"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb
index c5eb964..6963c9a 100644
--- a/res/values/strings_vi.arb
+++ b/res/values/strings_vi.arb
@@ -528,7 +528,6 @@
"i_understand": "Tôi hiểu",
"icon_style": "Kiểu biểu tượng",
"id": "ID: ",
- "if_you_dont_see_your_device": "Nếu bạn không thấy thiết bị của mình ở trên, hãy đảm bảo rằng Ledger của bạn đang bật và đã được mở khóa!",
"ignor": "Bỏ qua",
"import": "Nhập",
"importNFTs": "Nhập NFT",
@@ -1452,4 +1451,4 @@
"wc_action_approve": "Phê duyệt",
"wc_desc": "Dễ dàng kết nối với các dApp",
"yes_exit": "Có. Thoát"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb
index 089e192..8a073f3 100644
--- a/res/values/strings_yo.arb
+++ b/res/values/strings_yo.arb
@@ -530,7 +530,6 @@
"i_understand": "Mo ye mi",
"icon_style": "Aami ara",
"id": "ID: ",
- "if_you_dont_see_your_device": "Ti o ko ba ri ẹrọ rẹ loke, jọwọ rii daju pe Ledger rẹ wa ni titan ati pe o ti ṣí i silẹ!",
"ignor": "Foju kọ",
"import": "Gbe wọle",
"importNFTs": "Gbe NFTs wọle",
@@ -1452,4 +1451,4 @@
"wc_action_approve": "Fọwọ́sí",
"wc_desc": "So pọ̀ mọ́ àwọn DApp ní rọrùn",
"yes_exit": "Bẹ́ẹ̀ni. Jáde"
-}
\ No newline at end of file
+}
diff --git a/test/new-ui/widgets/new_future_primary_button_test.dart b/test/new-ui/widgets/new_future_primary_button_test.dart
new file mode 100644
index 0000000..858dc5f
--- /dev/null
+++ b/test/new-ui/widgets/new_future_primary_button_test.dart
@@ -0,0 +1,265 @@
+import "dart:async";
+
+import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart";
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
+import "package:flutter/material.dart";
+import "package:flutter_svg/flutter_svg.dart";
+import "package:flutter_test/flutter_test.dart";
+
+const _svg = '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"/>';
+
+void main() {
+ Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child));
+
+ NewPrimaryButton inner(WidgetTester tester) =>
+ tester.widget<NewPrimaryButton>(find.byType(NewPrimaryButton));
+
+ Future<void>? fire(WidgetTester tester) {
+ final result = (inner(tester).onPressed as dynamic)();
+ return result is Future<void> ? result : null;
+ }
+
+ Widget build({
+ required Future<void> Function() onPressed,
+ bool disabled = false,
+ SvgPicture? image,
+ Color color = Colors.blue,
+ Color textColor = Colors.white,
+ Color borderColor = Colors.transparent,
+ String text = "Send",
+ }) =>
+ wrap(
+ NewFuturePrimaryButton(
+ onPressed: onPressed,
+ text: text,
+ color: color,
+ textColor: textColor,
+ borderColor: borderColor,
+ disabled: disabled,
+ image: image,
+ ),
+ );
+
+ group("NewFuturePrimaryButton", () {
+ group("prop forwarding", () {
+ testWidgets("passes every prop through to NewPrimaryButton", (tester) async {
+ final image = SvgPicture.string(_svg);
+
+ await tester.pumpWidget(
+ build(
+ onPressed: () async {},
+ text: "Confirm",
+ color: Colors.green,
+ textColor: Colors.black,
+ borderColor: Colors.red,
+ disabled: true,
+ image: image,
+ ),
+ );
+
+ final button = inner(tester);
+ expect(button.text, "Confirm");
+ expect(button.color, Colors.green);
+ expect(button.textColor, Colors.black);
+ expect(button.borderColor, Colors.red);
+ expect(button.disabled, isTrue);
+ expect(button.image, same(image));
+ });
+
+ testWidgets("defaults: transparent border, not disabled, no image", (tester) async {
+ await tester.pumpWidget(
+ wrap(
+ NewFuturePrimaryButton(
+ onPressed: () async {},
+ text: "Send",
+ color: Colors.blue,
+ textColor: Colors.white,
+ ),
+ ),
+ );
+
+ final button = inner(tester);
+ expect(button.borderColor, Colors.transparent);
+ expect(button.disabled, isFalse);
+ expect(button.image, isNull);
+ });
+
+ testWidgets("starts out not loading", (tester) async {
+ await tester.pumpWidget(build(onPressed: () async {}));
+
+ expect(inner(tester).isLoading, isFalse);
+ });
+ });
+
+ group("loading lifecycle", () {
+ testWidgets("flips isLoading true while pending, false once resolved", (tester) async {
+ final completer = Completer<void>();
+ await tester.pumpWidget(build(onPressed: () => completer.future));
+
+ final pressed = fire(tester);
+ await tester.pump();
+ expect(
+ inner(tester).isLoading,
+ isTrue,
+ reason: "should be loading while the future is in flight",
+ );
+
+ completer.complete();
+ await pressed;
+ await tester.pump();
+ expect(inner(tester).isLoading, isFalse);
+ });
+
+ testWidgets("synchronous-ish callback still settles back to not loading", (tester) async {
+ var calls = 0;
+ await tester.pumpWidget(build(onPressed: () async => calls++));
+
+ await fire(tester);
+ await tester.pump();
+
+ expect(calls, 1);
+ expect(inner(tester).isLoading, isFalse);
+ });
+
+ testWidgets("invokes the callback exactly once per press", (tester) async {
+ var calls = 0;
+ await tester.pumpWidget(build(onPressed: () async => calls++));
+
+ await fire(tester);
+ await tester.pump();
+ await fire(tester);
+ await tester.pump();
+
+ expect(calls, 2);
+ });
+ });
+
+ testWidgets("ignores presses while a previous future is pending", (tester) async {
+ final completer = Completer<void>();
+ var calls = 0;
+ await tester.pumpWidget(
+ build(
+ onPressed: () {
+ calls++;
+ return completer.future;
+ },
+ ),
+ );
+
+ final pressed = fire(tester);
+ await tester.pump();
+
+ unawaited(fire(tester));
+ unawaited(fire(tester));
+ unawaited(fire(tester));
+ await tester.pump();
+
+ expect(calls, 1, reason: "extra presses must be swallowed");
+
+ completer.complete();
+ await pressed;
+ await tester.pump();
+
+ await fire(tester);
+ await tester.pump();
+ expect(calls, 2);
+ });
+
+ group("handle errors", () {
+ testWidgets("clears the loading flag when the callback throws", (tester) async {
+ await tester.pumpWidget(
+ build(onPressed: () async => throw StateError("boom")),
+ );
+
+ await expectLater(fire(tester), throwsA(isA<StateError>()));
+ await tester.pump();
+
+ expect(inner(tester).isLoading, isFalse, reason: "the finally block must reset the flag");
+ });
+
+ testWidgets("rethrows instead of swallowing the failure", (tester) async {
+ await tester.pumpWidget(
+ build(onPressed: () async => throw StateError("boom")),
+ );
+
+ await expectLater(fire(tester), throwsA(isA<StateError>()));
+ });
+
+ testWidgets("stays pressable after the callback throws", (tester) async {
+ var calls = 0;
+ await tester.pumpWidget(
+ build(
+ onPressed: () {
+ calls++;
+ throw StateError("boom");
+ },
+ ),
+ );
+
+ await expectLater(fire(tester), throwsA(isA<StateError>()));
+ await tester.pump();
+ await expectLater(fire(tester), throwsA(isA<StateError>()));
+ await tester.pump();
+
+ expect(calls, 2, reason: "the re-entrancy guard must not latch on failure");
+ expect(inner(tester).isLoading, isFalse);
+ });
+ });
+
+ group("disposal mid-flight", () {
+ testWidgets("does not setState after being disposed", (tester) async {
+ final completer = Completer<void>();
+ await tester.pumpWidget(build(onPressed: () => completer.future));
+
+ final pressed = fire(tester);
+ await tester.pump();
+
+ await tester.pumpWidget(wrap(const SizedBox.shrink()));
+ expect(find.byType(NewPrimaryButton), findsNothing);
+
+ completer.complete();
+ await pressed;
+ await tester.pump();
+
+ expect(
+ tester.takeException(),
+ isNull,
+ reason: "the mounted check must guard the second setState",
+ );
+ });
+
+ testWidgets("still propagates a throw when disposed", (tester) async {
+ final completer = Completer<void>();
+ await tester.pumpWidget(build(onPressed: () => completer.future));
+
+ final pressed = fire(tester);
+ await tester.pump();
+ await tester.pumpWidget(wrap(const SizedBox.shrink()));
+
+ completer.completeError(StateError("boom"));
+
+ await expectLater(pressed, throwsA(isA<StateError>()));
+ expect(
+ tester.takeException(),
+ isNull,
+ reason: "no setState-after-dispose FlutterError on the error path",
+ );
+ });
+ });
+
+ testWidgets("disabled is forwarded but not enforced by this wrapper", (tester) async {
+ // Press-blocking lives in NewPrimaryButton; this widget only relays the
+ // flag. If that ever moves up here, this expectation should change.
+ var calls = 0;
+ await tester.pumpWidget(
+ build(onPressed: () async => calls++, disabled: true),
+ );
+
+ expect(inner(tester).disabled, isTrue);
+
+ await fire(tester);
+ await tester.pump();
+ expect(calls, 1);
+ });
+ });
+}
diff --git a/tool/configure.dart b/tool/configure.dart
index 66c3027..022eaa2 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -442,6 +442,8 @@ abstract class Monero {
Map<String, String> exportOutputsUR(Object wallet);
bool needExportOutputs(Object wallet, Money amount);
+
+ bool hasUnknownKeyImages(Object wallet);
bool importKeyImagesUR(Object wallet, String ur);
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.