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);