trezor-monero-passphrase-modal (#3337)

* trezor monero passphrase modal * feat: add Trezor passphrase support for wallet restoration process * revert: send page changes * chore: format code * chore: format code * chore: reorganize imports and improve code formatting in send_page.dart [skip ci] * auto-reformat * fix: trezor usb device refresh race condition * fix: ui bugs in relation to trezor * fix: only show passphrase option for trezor devices --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Konstantin Ullrich committed Jul 17, 2026 at 16:52 UTC 9d685d4645ffb349694cc0d4625183c581b0b23c
17 files changed +318 -171
cw_monero/lib/api/wallet.dart
+4
@@ -257,6 +257,10 @@ void setRecoveringFromSeed({required bool isRecovery}) {
257
258 final storeMutex = Mutex();
259
260 +void refresh() {
261 + currentWallet!.refresh();
262 +}
263 +
264 int lastStorePointer = 0;
265 int lastStoreHeight = 0;
266 void storeSync({bool force = false}) async {
cw_monero/lib/api/wallet_manager.dart
+5
@@ -150,6 +150,11 @@ void restoreWalletFromSeedSync(
150 _lastOpenedWallet = path;
151 }
152
153 +void storePassphrase({required String path, required String passphrase}) {
154 + currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
155 + currentWallet!.store(path: path);
156 +}
157 +
158 void restoreWalletFromKeys(
159 {required String path,
160 required String password,
cw_monero/lib/monero_wallet.dart
+13 -5
@@ -382,22 +382,30 @@ abstract class MoneroWalletBase
382 }
383
384 MoneroTrezorService? trezorService;
385 +
386 + Future<Trezor> _getTrezor() async {
387 + if (trezorService == null) throw Exception("Trezor not connected");
388 +
389 + final trezor = Trezor(trezorService!);
390 + await trezor.newPassphraseSession(passphrase);
391 + return trezor;
392 + }
393 +
394 Future<void> syncTrezor() async {
395 if (trezorService == null) throw Exception("Trezor not connected");
396
397 final ptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
398 final tdis = monero.Wallet_exportTrezorTdis(ptr);
390 -
391 - final response = await Trezor(trezorService!).keyImageSync(tdis);
392 -
399 + final trezor = await _getTrezor();
400 + final response = await trezor.keyImageSync(tdis);
401 final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response);
402
403 if (!success) throw Exception(monero.Wallet_errorString(ptr));
404 }
405
406 Future<String> signTrezorTransaction(String json) async {
399 - if (trezorService == null) throw Exception("Trezor not connected");
400 - return Trezor(trezorService!).signTransaction(json);
407 + final trezor = await _getTrezor();
408 + return trezor.signTransaction(json);
409 }
410
411 @override
cw_monero/lib/monero_wallet_service.dart
+20 -5
@@ -44,10 +44,15 @@ class MoneroNewWalletCredentials extends WalletCredentials {
44 }
45
46 class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
47 - MoneroRestoreWalletFromHardwareCredentials(
48 - {required String name, required this.hardwareWalletService, int height = 0, String? password})
49 - : super(name: name, password: password, height: height);
50 - HardwareWalletService hardwareWalletService;
47 + MoneroRestoreWalletFromHardwareCredentials({
48 + required String name,
49 + required this.hardwareWalletService,
50 + int height = 0,
51 + String? password,
52 + String? passphrase,
53 + }) : super(name: name, password: password, height: height, passphrase: passphrase);
54 +
55 + final HardwareWalletService hardwareWalletService;
56 }
57
58 class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
@@ -306,7 +311,13 @@ class MoneroWalletService extends WalletService<
311 deviceName: 'Ledger',
312 );
313 } else if (credentials.hardwareWalletService case MoneroTrezorService service) {
309 - final watchCredentials = await Trezor(service).getWatchCredentials();
314 + final trezor = Trezor(service);
315 +
316 + if (credentials.passphrase != null) {
317 + await trezor.newPassphraseSession(credentials.passphrase);
318 + }
319 +
320 + final watchCredentials = await trezor.getWatchCredentials();
321
322 monero_wallet_manager.restoreWalletFromKeys(
323 path: path,
@@ -317,6 +328,10 @@ class MoneroWalletService extends WalletService<
328 viewKey: watchCredentials.watchKey,
329 spendKey: "",
330 );
331 +
332 + if (credentials.passphrase != null) {
333 + monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!);
334 + }
335 }
336
337 final wallet = MoneroWallet(
cw_monero/lib/trezor.dart
+9
@@ -21,6 +21,15 @@ class Trezor {
21
22 Trezor(this.service);
23
24 + String? _sessionPassphrase;
25 + Future<void> newPassphraseSession(String? passphrase) async {
26 + if (passphrase == null) return;
27 +
28 + if (_sessionPassphrase == passphrase) return;
29 + _sessionPassphrase = passphrase;
30 + return service.client.createChannel(passphrase: _sessionPassphrase);
31 + }
32 +
33 Future<MoneroTrezorWatchCredentials> getWatchCredentials() async {
34 final credentials = await TrezorMonero(service.client).getWatchCredentials();
35
cw_monero/pubspec.lock
+4 -4
@@ -921,8 +921,8 @@ packages:
921 dependency: "direct main"
922 description:
923 path: trezor-flutter
924 - ref: d1289dfa4247a93ee57342dfd249957b339915ef
925 - resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
924 + ref: "944080d950a62dd48a199b9736237cbe580351de"
925 + resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
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: d1289dfa4247a93ee57342dfd249957b339915ef
934 - resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
933 + ref: "944080d950a62dd48a199b9736237cbe580351de"
934 + resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
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: d1289dfa4247a93ee57342dfd249957b339915ef
36 + ref: 944080d950a62dd48a199b9736237cbe580351de
37 path: trezor-flutter
38
39 dev_dependencies:
lib/monero/cw_monero.dart
+2
@@ -236,12 +236,14 @@ class CWMonero extends Monero {
236 required String password,
237 required int height,
238 required HardwareWalletService hardwareWalletService,
239 + required String? passphrase,
240 }) =>
241 MoneroRestoreWalletFromHardwareCredentials(
242 name: name,
243 password: password,
244 height: height,
245 hardwareWalletService: hardwareWalletService,
246 + passphrase: passphrase,
247 );
248
249 @override
lib/new-ui/widgets/digit_input.dart
+42 -46
@@ -1,5 +1,7 @@
1 -import 'package:flutter/material.dart';
2 -import 'package:flutter/services.dart';
1 +import "dart:io";
2 +
3 +import "package:flutter/material.dart";
4 +import "package:flutter/services.dart";
5
6 class DigitInputController {
7 String _text = "";
@@ -19,38 +21,37 @@ class DigitInputController {
21 }
22
23 class DigitInputPill extends StatelessWidget {
22 - const DigitInputPill({super.key, this.digit, required this.highlighted});
24 + const DigitInputPill({required this.highlighted, super.key, this.digit});
25
26 final String? digit;
27 final bool highlighted;
28
29 @override
28 - Widget build(BuildContext context) {
29 - return Container(
30 - width: 51,
31 - height: 84,
32 - decoration: BoxDecoration(
33 - color: Theme.of(context).colorScheme.surfaceContainerHigh,
34 - borderRadius: BorderRadius.circular(16),
35 - border:
36 - highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
37 - ),
38 - child: Center(
39 - child: Padding(
40 - padding: EdgeInsets.symmetric(vertical: 16),
41 - child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
30 + Widget build(BuildContext context) => Container(
31 + width: 51,
32 + height: 84,
33 + decoration: BoxDecoration(
34 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
35 + borderRadius: BorderRadius.circular(16),
36 + border: highlighted
37 + ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
38 + : null,
39 ),
43 - ),
44 - );
45 - }
40 + child: Center(
41 + child: Padding(
42 + padding: const EdgeInsets.symmetric(vertical: 16),
43 + child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)),
44 + ),
45 + ),
46 + );
47 }
48
49 class DigitInput extends StatefulWidget {
50 const DigitInput({
50 - super.key,
51 required this.controller,
52 required this.desiredLength,
53 this.breakAt = 3,
54 + super.key,
55 });
56
57 final DigitInputController controller;
@@ -74,12 +75,9 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
75 if (_textInputConnection == null || !_textInputConnection!.attached) {
76 _textInputConnection = TextInput.attach(
77 this,
77 - const TextInputConfiguration(
78 - inputType: TextInputType.numberWithOptions(
79 - signed: false,
80 - decimal: false,
81 - ),
82 - inputAction: TextInputAction.none,
78 + TextInputConfiguration(
79 + inputType: TextInputType.number,
80 + inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
81 ),
82 );
83 }
@@ -102,30 +100,28 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
100 }
101
102 @override
105 - Widget build(BuildContext context) {
106 - return GestureDetector(
107 - onTap: _openVirtualKeyboard,
108 - child: SizedBox(
109 - height: 84,
110 - child: ListView.separated(
111 - scrollDirection: Axis.horizontal,
112 - shrinkWrap: true,
113 - physics: NeverScrollableScrollPhysics(),
114 - itemBuilder: (context, index) => DigitInputPill(
115 - digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
116 - highlighted: widget.controller.text.length == index,
103 + Widget build(BuildContext context) => GestureDetector(
104 + onTap: _openVirtualKeyboard,
105 + child: SizedBox(
106 + height: 84,
107 + child: ListView.separated(
108 + scrollDirection: Axis.horizontal,
109 + shrinkWrap: true,
110 + physics: const NeverScrollableScrollPhysics(),
111 + itemBuilder: (context, index) => DigitInputPill(
112 + digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
113 + highlighted: widget.controller.text.length == index,
114 + ),
115 + separatorBuilder: (context, index) =>
116 + SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
117 + itemCount: widget.desiredLength,
118 ),
118 - separatorBuilder: (context, index) =>
119 - SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
120 - itemCount: widget.desiredLength,
119 ),
122 - ),
123 - );
124 - }
120 + );
121
122 @override
123 void updateEditingValue(TextEditingValue value) {
128 - final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
124 + final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
125
126 if (numberRegExp.hasMatch(value.text)) {
127 setState(() => widget.controller.text = value.text);
lib/src/screens/connect_device/connect_device_page.dart
+13 -3
@@ -142,12 +142,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
142 super.dispose();
143 }
144
145 + var _isRefreshingUsb = false;
146 Future<void> _refreshUsbDevices() async {
146 - final dev = await widget.hardwareWalletVM.getAllUsbDevices();
147 + if (_isRefreshingUsb) {
148 + return;
149 + }
150 + _isRefreshingUsb = true;
151 + try {
152 + final dev = await widget.hardwareWalletVM.getAllUsbDevices();
153
148 - if (usbDevices.length != dev.length) {
149 - setState(() => usbDevices = dev);
154 + if (usbDevices.length != dev.length) {
155 + setState(() => usbDevices = dev);
156 + }
157 + } catch(e) {
158 + printV(e);
159 }
160 + _isRefreshingUsb = false;
161 }
162
163 Future<void> _refreshBleDevices() async {
lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart
+137 -104
@@ -1,19 +1,22 @@
1 -import 'package:cake_wallet/core/wallet_name_validator.dart';
2 -import 'package:cw_core/generate_name.dart';
3 -import 'package:cake_wallet/generated/i18n.dart';
4 -import 'package:cake_wallet/src/screens/base_page.dart';
5 -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6 -import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
7 -import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart';
8 -import 'package:cake_wallet/src/widgets/primary_button.dart';
9 -import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
10 -import 'package:cake_wallet/utils/responsive_layout_util.dart';
11 -import 'package:cake_wallet/utils/show_pop_up.dart';
12 -import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
13 -import 'package:cw_core/wallet_type.dart';
14 -import 'package:flutter/material.dart';
15 -import 'package:flutter_mobx/flutter_mobx.dart';
16 -import 'package:mobx/mobx.dart';
1 +import "dart:async";
2 +
3 +import "package:cake_wallet/core/wallet_name_validator.dart";
4 +import "package:cake_wallet/generated/i18n.dart";
5 +import "package:cake_wallet/src/screens/base_page.dart";
6 +import "package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart";
7 +import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
8 +import "package:cake_wallet/src/widgets/base_text_form_field.dart";
9 +import "package:cake_wallet/src/widgets/blockchain_height_widget.dart";
10 +import "package:cake_wallet/src/widgets/primary_button.dart";
11 +import "package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart";
12 +import "package:cake_wallet/utils/responsive_layout_util.dart";
13 +import "package:cake_wallet/utils/show_pop_up.dart";
14 +import "package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart";
15 +import "package:cw_core/generate_name.dart";
16 +import "package:cw_core/wallet_type.dart";
17 +import "package:flutter/material.dart";
18 +import "package:flutter_mobx/flutter_mobx.dart";
19 +import "package:mobx/mobx.dart";
20
21 class MoneroHardwareWalletOptionsPage extends BasePage {
22 MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM);
@@ -42,13 +45,15 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
45 : _formKey = GlobalKey<FormState>(),
46 _blockchainHeightKey = GlobalKey<BlockchainHeightState>(),
47 _blockHeightFocusNode = FocusNode(),
45 - _controller = TextEditingController();
48 + _walletNameController = TextEditingController(),
49 + _passphraseController = TextEditingController();
50
51 final GlobalKey<FormState> _formKey;
52 final GlobalKey<BlockchainHeightState> _blockchainHeightKey;
53 final FocusNode _blockHeightFocusNode;
54 final WalletHardwareRestoreViewModel _walletHardwareRestoreVM;
51 - final TextEditingController _controller;
55 + final TextEditingController _walletNameController;
56 + final TextEditingController _passphraseController;
57
58 @override
59 void initState() {
@@ -57,131 +62,159 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
62 }
63
64 @override
60 - Widget build(BuildContext context) {
61 - return Padding(
62 - padding: EdgeInsets.only(top: 24),
63 - child: ScrollableWithBottomSection(
64 - contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
65 - content: Center(
66 - child: ConstrainedBox(
67 - constraints:
68 - BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
69 - child: Column(
70 - crossAxisAlignment: CrossAxisAlignment.center,
71 - children: [
72 - Padding(
73 - padding: EdgeInsets.only(top: 0),
74 - child: Form(
75 - key: _formKey,
76 - child: Stack(
77 - alignment: Alignment.centerRight,
78 - children: [
79 - BaseTextFormField(
80 - onChanged: (value) => _walletHardwareRestoreVM.name = value,
81 - controller: _controller,
82 - textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
83 - fontSize: 20.0,
84 - fontWeight: FontWeight.w600,
85 - ),
86 - placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
87 - fontSize: 18.0,
88 - fontWeight: FontWeight.w500,
89 - color: Theme.of(context).colorScheme.onSurfaceVariant,
90 - ),
91 - hintText: S.of(context).wallet_name,
92 - suffixIcon: Semantics(
93 - label: S.of(context).generate_name,
94 - child: IconButton(
95 - onPressed: _onGenerateName,
96 - icon: Container(
97 - padding: const EdgeInsets.all(8),
98 - decoration: BoxDecoration(
99 - borderRadius: BorderRadius.circular(6.0),
100 - color: Theme.of(context).colorScheme.surfaceContainerHighest,
65 + Widget build(BuildContext context) => Padding(
66 + padding: const EdgeInsets.only(top: 24),
67 + child: ScrollableWithBottomSection(
68 + contentPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
69 + content: Center(
70 + child: ConstrainedBox(
71 + constraints: const BoxConstraints(
72 + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
73 + ),
74 + child: Column(
75 + crossAxisAlignment: CrossAxisAlignment.center,
76 + children: [
77 + Padding(
78 + padding: EdgeInsets.zero,
79 + child: Form(
80 + key: _formKey,
81 + child: Stack(
82 + alignment: Alignment.centerRight,
83 + children: [
84 + BaseTextFormField(
85 + onChanged: (value) => _walletHardwareRestoreVM.name = value,
86 + controller: _walletNameController,
87 + textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
88 + fontSize: 20,
89 + fontWeight: FontWeight.w600,
90 ),
102 - width: 34,
103 - height: 34,
104 - child: Image.asset(
105 - 'assets/images/refresh_icon.png',
106 - color: Theme.of(context).colorScheme.primary,
91 + placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
92 + fontSize: 18,
93 + fontWeight: FontWeight.w500,
94 + color: Theme.of(context).colorScheme.onSurfaceVariant,
95 + ),
96 + hintText: S.of(context).wallet_name,
97 + suffixIcon: Semantics(
98 + label: S.of(context).generate_name,
99 + child: IconButton(
100 + onPressed: _onGenerateName,
101 + icon: Container(
102 + padding: const EdgeInsets.all(8),
103 + decoration: BoxDecoration(
104 + borderRadius: BorderRadius.circular(6),
105 + color: Theme.of(context).colorScheme.surfaceContainerHighest,
106 + ),
107 + width: 34,
108 + height: 34,
109 + child: Image.asset(
110 + "assets/images/refresh_icon.png",
111 + color: Theme.of(context).colorScheme.primary,
112 + ),
113 ),
114 ),
115 ),
116 + validator: WalletNameValidator(),
117 ),
111 - validator: WalletNameValidator(),
112 - ),
113 - ],
118 + ],
119 + ),
120 ),
121 ),
116 - ),
117 - Padding(
118 - padding: EdgeInsets.only(top: 20),
119 - child: BlockchainHeightWidget(
120 - focusNode: _blockHeightFocusNode,
121 - key: _blockchainHeightKey,
122 - hasDatePicker: true,
123 - walletType: WalletType.monero,
122 + Padding(
123 + padding: const EdgeInsets.only(top: 20),
124 + child: BlockchainHeightWidget(
125 + focusNode: _blockHeightFocusNode,
126 + key: _blockchainHeightKey,
127 + hasDatePicker: true,
128 + walletType: WalletType.monero,
129 + ),
130 ),
131 + ],
132 + ),
133 + ),
134 + ),
135 + bottomSectionPadding: const EdgeInsets.all(24),
136 + bottomSection: Observer(
137 + builder: (_) => Column(
138 + children: [
139 + if (_walletHardwareRestoreVM.passphraseAvailable) ...[
140 + TextButton(
141 + onPressed: () {
142 + showModalBottomSheet(
143 + context: context,
144 + isScrollControlled: true,
145 + backgroundColor: Colors.transparent,
146 + builder: (context) => MoneroHardwareWalletPassphraseInputModal(
147 + controller: _passphraseController,
148 + ),
149 + );
150 + },
151 + child: Text(S.of(context).add_a_passphrase),
152 + ),
153 + ],
154 + LoadingPrimaryButton(
155 + onPressed: _confirmForm,
156 + text: S.of(context).seed_language_next,
157 + color: Theme.of(context).colorScheme.primary,
158 + textColor: Theme.of(context).colorScheme.onPrimary,
159 + isDisabled: _walletHardwareRestoreVM.name.isEmpty,
160 ),
161 ],
162 ),
163 ),
164 ),
130 - bottomSectionPadding: EdgeInsets.all(24),
131 - bottomSection: Observer(
132 - builder: (context) => LoadingPrimaryButton(
133 - onPressed: _confirmForm,
134 - text: S.of(context).seed_language_next,
135 - color: Theme.of(context).colorScheme.primary,
136 - textColor: Theme.of(context).colorScheme.onPrimary,
137 - isDisabled: _walletHardwareRestoreVM.name.isEmpty,
138 - ),
139 - ),
140 - ),
141 - );
142 - }
165 + );
166
167 Future<void> _onGenerateName() async {
168 final rName = await generateName();
169 FocusManager.instance.primaryFocus?.unfocus();
170
171 setState(() {
149 - _controller.text = rName;
172 + _walletNameController.text = rName;
173 _walletHardwareRestoreVM.name = rName;
151 - _controller.selection =
152 - TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
174 + _walletNameController.selection =
175 + TextSelection.fromPosition(TextPosition(offset: _walletNameController.text.length));
176 });
177 }
178
179 Future<void> _confirmForm() async {
157 - showPopUp<void>(
158 - context: context,
159 - builder: (BuildContext context) => AlertWithOneAction(
160 - alertTitle: S.of(context).proceed_on_device,
161 - alertContent: S.of(context).proceed_on_device_description,
162 - buttonText: S.of(context).cancel,
163 - alertBarrierDismissible: false,
164 - buttonAction: () => Navigator.of(context).pop(),
180 + unawaited(
181 + showPopUp<void>(
182 + context: context,
183 + builder: (context) => AlertWithOneAction(
184 + alertTitle: S.of(context).proceed_on_device,
185 + alertContent: S.of(context).proceed_on_device_description,
186 + buttonText: S.of(context).cancel,
187 + alertBarrierDismissible: false,
188 + buttonAction: () => Navigator.of(context).pop(),
189 + ),
190 ),
191 );
192
168 - final options = {'height': _blockchainHeightKey.currentState?.height ?? -1};
193 + final options = <String, dynamic>{"height": _blockchainHeightKey.currentState?.height ?? -1};
194 +
195 + if (_walletHardwareRestoreVM.passphraseAvailable && _passphraseController.text.isNotEmpty) {
196 + options["passphrase"] = _passphraseController.text;
197 + }
198 await _walletHardwareRestoreVM.create(options: options);
199 }
200
201 bool _effectsInstalled = false;
202
203 void _setEffects(BuildContext context) {
175 - if (_effectsInstalled) return;
204 + if (_effectsInstalled) {
205 + return;
206 + }
207
177 - reaction((_) => _walletHardwareRestoreVM.error, (String? error) {
208 + reaction((_) => _walletHardwareRestoreVM.error, (error) {
209 if (error != null) {
179 - if (error == S.current.ledger_connection_error) Navigator.of(context).pop();
210 + if (error == S.current.ledger_connection_error) {
211 + Navigator.of(context).pop();
212 + }
213
214 WidgetsBinding.instance.addPostFrameCallback((_) {
215 showPopUp<void>(
216 context: context,
184 - builder: (BuildContext context) => AlertWithOneAction(
217 + builder: (context) => AlertWithOneAction(
218 alertTitle: S.of(context).error,
219 alertContent: error,
220 buttonText: S.of(context).ok,
lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart new
+54
@@ -0,0 +1,54 @@
1 +import "package:cake_wallet/generated/i18n.dart";
2 +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
3 +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
4 +import "package:cake_wallet/src/widgets/base_text_form_field.dart";
5 +import "package:flutter/material.dart";
6 +
7 +class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
8 + const MoneroHardwareWalletPassphraseInputModal({required this.controller, super.key});
9 +
10 + final TextEditingController controller;
11 +
12 + @override
13 + Widget build(BuildContext context) => Padding(
14 + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
15 + child: Container(
16 + decoration: BoxDecoration(
17 + borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
18 + color: Theme.of(context).colorScheme.surface,
19 + ),
20 + child: SafeArea(
21 + child: Column(
22 + mainAxisSize: MainAxisSize.min,
23 + children: [
24 + ModalTopBar(
25 + title: S.of(context).passphrase_entry,
26 + leadingIcon: const Icon(Icons.close),
27 + onLeadingPressed: Navigator.of(context).pop,
28 + ),
29 + Padding(
30 + padding: const EdgeInsets.symmetric(horizontal: 18),
31 + child: Column(
32 + spacing: 12,
33 + children: [
34 + BaseTextFormField(
35 + controller: controller,
36 + hintText: S.of(context).passphrase_raw,
37 + ),
38 + const SizedBox(),
39 + NewPrimaryButton(
40 + onPressed: Navigator.of(context).pop,
41 + text: S.of(context).restore_next,
42 + color: Theme.of(context).colorScheme.primary,
43 + textColor: Theme.of(context).colorScheme.onPrimary,
44 + ),
45 + const SizedBox(),
46 + ],
47 + ),
48 + ),
49 + ],
50 + ),
51 + ),
52 + ),
53 + );
54 +}
lib/src/screens/restore/wallet_restore_page.dart
+1 -1
@@ -99,7 +99,7 @@ class WalletRestorePage extends BasePage {
99 children: [
100 Observer(
101 builder: (context) {
102 - return walletRestoreViewModel.mode == WalletRestoreMode.seed
102 + return walletRestoreViewModel.passphraseAvailable
103 ? StandardCheckbox(
104 captionColor: Theme.of(context).colorScheme.onSecondaryContainer,
105 value: walletRestoreViewModel.hasPassphrase,
lib/view_model/wallet_hardware_restore_view_model.dart
+4 -1
@@ -6,13 +6,13 @@ import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/monero/monero.dart';
7 import 'package:cake_wallet/store/app_store.dart';
8 import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
9 -import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
9 import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
10 import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
11 import 'package:cw_core/hardware/hardware_account_data.dart';
12 import 'package:cw_core/utils/print_verbose.dart';
13 import 'package:cw_core/wallet_base.dart';
14 import 'package:cw_core/wallet_credentials.dart';
15 +import 'package:cw_core/wallet_info.dart';
16 import 'package:cw_core/wallet_type.dart';
17 import 'package:mobx/mobx.dart';
18
@@ -43,6 +43,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
43 @observable
44 String? error = null;
45
46 + bool get passphraseAvailable => hardwareWalletVM.hardwareWalletType == HardwareWalletType.trezor;
47 +
48 // @observable
49 ObservableList<HardwareAccountData> availableAccounts = ObservableList();
50
@@ -87,6 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
89 hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type),
90 password: password,
91 height: _options['height'] as int? ?? 0,
92 + passphrase: _options['passphrase'] as String?,
93 );
94 default:
95 throw Exception('Unexpected type: ${type.toString()}');
lib/view_model/wallet_restore_view_model.dart
+4
@@ -108,6 +108,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
108 @observable
109 WalletRestoreMode mode;
110
111 + @computed
112 + bool get passphraseAvailable =>
113 + mode == WalletRestoreMode.seed || hardwareWalletType == HardwareWalletType.trezor;
114 +
115 @observable
116 bool hasPassphrase;
117
res/values/strings_en.arb
+4
@@ -14,6 +14,7 @@
14 "activeConnectionsPrompt": "Active connections will appear here",
15 "add": "Add",
16 "add_a_note": "Add a note",
17 + "add_a_passphrase": "Add a passphrase",
18 "add_account": "Add Account",
19 "add_contact": "Add contact",
20 "add_contact_to_address_book": "Would you like to add this contact to your address book?",
@@ -402,6 +403,7 @@
403 "enter_code": "Enter code",
404 "enter_greater_than": "Please enter an amount greater than",
405 "enter_less_than": "Please enter an amount less than",
406 + "enter_on_device_instead": "Enter on device instead",
407 "enter_recipient_address": "Please enter recipient address",
408 "enter_seed_phrase": "Enter your seed phrase",
409 "enter_totp_code": "Please enter the TOTP Code.",
@@ -745,6 +747,8 @@
747 "pairing_error": "Pairing Error",
748 "pairingInvalidEvent": "Pairing Invalid Event",
749 "passphrase": "Passphrase (Optional)",
750 + "passphrase_raw": "Passphrase",
751 + "passphrase_entry": "Passphrase entry",
752 "passphrase_view_keys": "Passphrase",
753 "passphrases_doesnt_match": "Passphrases do not match, please try again",
754 "password": "Password",
tool/configure.dart
+1 -1
@@ -455,7 +455,7 @@ abstract class Monero {
455 HardwareWalletType? hardwareWalletType,
456 required int height});
457 WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic});
458 - WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService});
458 + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService, required String? passphrase});
459 WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required int seedType, required String? passphrase, String? password, String? mnemonic});
460 Map<String, String> getKeys(Object wallet);
461 int? getRestoreHeight(Object wallet);