1
+import 'package:cake_wallet/generated/i18n.dart';
2
+import 'package:cake_wallet/new-ui/widgets/digit_input.dart';
3
+import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
4
+import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
5
+import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
6
+import 'package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart';
7
+import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
8
+import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
9
+import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
10
+import 'package:cake_wallet/utils/show_pop_up.dart';
11
+import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
12
+import 'package:cw_core/wallet_info.dart';
13
+import 'package:flutter/cupertino.dart';
14
+import 'package:flutter/material.dart';
15
+import 'package:mobx/mobx.dart';
16
+
17
+class HardwareWalletProceedOnDeviceSheet extends StatefulWidget {
18
+ const HardwareWalletProceedOnDeviceSheet({
19
+ super.key,
20
+ required this.hardwareWalletType,
21
+ required this.trezorConnectVM,
22
+ required this.onRetry,
23
+ });
24
+
25
+ final HardwareWalletType hardwareWalletType;
26
+ final TrezorConnectViewModelBase trezorConnectVM;
27
+ final void Function() onRetry;
28
+
29
+ @override
30
+ State<HardwareWalletProceedOnDeviceSheet> createState() =>
31
+ _HardwareWalletProceedOnDeviceSheetState();
32
+}
33
+
34
+class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProceedOnDeviceSheet> {
35
+ TrezorParingState _paringState = TrezorParingState.initial;
36
+
37
+ final DigitInputController _controller = DigitInputController();
38
+
39
+ static const pinOpenDuration = Duration(milliseconds: 300);
40
+
41
+ // you should probably check if there can be other pin lengths
42
+ static const pinLength = 6;
43
+
44
+ ReactionDisposer? paringStateReaction;
45
+
46
+ @override
47
+ void initState() {
48
+ super.initState();
49
+ _controller.addListener(() => setState(() {}));
50
+
51
+ _paringState = widget.trezorConnectVM.paringState;
52
+ paringStateReaction = reaction((_) => widget.trezorConnectVM.paringState, (paringState) {
53
+ if (paringState is SuccessTrezorParingState) {
54
+ if (mounted) Navigator.of(context).pop();
55
+ return;
56
+ }
57
+ setState(() => _paringState = paringState);
58
+ });
59
+ }
60
+
61
+ @override
62
+ void dispose() {
63
+ super.dispose();
64
+ paringStateReaction?.reaction.dispose();
65
+ }
66
+
67
+ bool get _isAwaitingPin => _paringState == TrezorParingState.enterPin;
68
+
69
+ @override
70
+ Widget build(BuildContext context) {
71
+ return PopScope(
72
+ canPop: false,
73
+ child: SafeArea(
74
+ bottom: false,
75
+ child: Container(
76
+ width: MediaQuery.of(context).size.width,
77
+ decoration: BoxDecoration(
78
+ color: Theme.of(context).colorScheme.surface,
79
+ borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
80
+ ),
81
+ child: SafeArea(
82
+ child: Padding(
83
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
84
+ child: Column(
85
+ children: [
86
+ ModalTopBar(
87
+ title: "",
88
+ leadingWidget: AnimatedSwitcher(
89
+ layoutBuilder: (currentChild, previousChildren) {
90
+ return Stack(
91
+ alignment: Alignment.centerLeft,
92
+ children: <Widget>[
93
+ ...previousChildren,
94
+ if (currentChild != null) currentChild,
95
+ ],
96
+ );
97
+ },
98
+ duration: pinOpenDuration,
99
+ child: Text(
100
+ key: ValueKey(pageTitle),
101
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
102
+ pageTitle,
103
+ ),
104
+ ),
105
+ trailingIcon: Icon(
106
+ Icons.close,
107
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
108
+ ),
109
+ onTrailingPressed: () => showPopUp(
110
+ context: context,
111
+ builder: (context) => AlertWithTwoActions(
112
+ alertTitle: S.of(context).are_you_sure_exit,
113
+ alertContent: S.of(context).hww_exit_desc,
114
+ leftButtonText: S.of(context).cancel,
115
+ rightButtonText: S.of(context).yes_exit,
116
+ rightAlertButtonStyle: AlertButtonStyle.error(context),
117
+ actionLeftButton: Navigator.of(context).pop,
118
+ actionRightButton: () {
119
+ Navigator.of(context).pop();
120
+ Navigator.of(context).pop();
121
+ },
122
+ ),
123
+ ),
124
+ ),
125
+ Expanded(
126
+ child: DirectionalAnimatedSwitcher(
127
+ duration: Duration(milliseconds: 400),
128
+ child: content,
129
+ ),
130
+ ),
131
+ Padding(
132
+ padding: EdgeInsets.only(bottom: 24, right: 24),
133
+ child: Row(
134
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
135
+ children: [
136
+ SizedBox(),
137
+ AnimatedOpacity(
138
+ curve: Curves.easeOutQuad,
139
+ opacity: hasFullPin ? 1 : 0,
140
+ duration: pinOpenDuration,
141
+ child: ModernButton(
142
+ backgroundColor: Theme.of(context).colorScheme.primary,
143
+ iconColor: Theme.of(context).colorScheme.onPrimary,
144
+ size: 36,
145
+ icon: Icon(Icons.arrow_forward),
146
+ onPressed: () =>
147
+ widget.trezorConnectVM.setParingPin(_controller.text.trim()),
148
+ ),
149
+ ),
150
+ ],
151
+ ),
152
+ )
153
+ ],
154
+ ),
155
+ )),
156
+ ),
157
+ ),
158
+ );
159
+ }
160
+
161
+ Widget get content {
162
+ if (_paringState is InitialTrezorParingState || _paringState is EnterPinTrezorParingState) {
163
+ return _enterPinCode();
164
+ }
165
+
166
+ if (_paringState is VerifyingPinTrezorParingState) return _verifyingCode();
167
+
168
+ if (_paringState is FailTrezorParingState)
169
+ return _errorBox((_paringState as FailTrezorParingState).message);
170
+
171
+ return SizedBox.shrink(key: ValueKey(3));
172
+ }
173
+
174
+ void retry() {
175
+ _controller.text = "";
176
+ widget.onRetry();
177
+ }
178
+
179
+ String get pageTitle {
180
+ if (_paringState is InitialTrezorParingState) return S.of(context).device_confirmation;
181
+ if (_isAwaitingPin) return S.of(context).pairing_code;
182
+ return "";
183
+ }
184
+
185
+ bool get hasFullPin => _controller.text.length == pinLength && _isAwaitingPin;
186
+
187
+ String? get hardwareWalletIcon {
188
+ switch (widget.hardwareWalletType) {
189
+ case HardwareWalletType.bitbox:
190
+ return "assets/new-ui/hardware_wallets/device_bitbox.svg";
191
+ case HardwareWalletType.ledger:
192
+ return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
193
+ case HardwareWalletType.trezor:
194
+ return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
195
+ case HardwareWalletType.cupcake:
196
+ return "assets/images/cupcake.svg";
197
+ case HardwareWalletType.coldcard:
198
+ case HardwareWalletType.seedsigner:
199
+ case HardwareWalletType.keystone:
200
+ return "assets/images/hardware_wallet/device_qr.svg";
201
+ }
202
+ }
203
+
204
+ Widget _verifyingCode() => Container(
205
+ key: ValueKey(1),
206
+ width: MediaQuery.of(context).size.width,
207
+ child: Column(
208
+ spacing: 12,
209
+ mainAxisAlignment: MainAxisAlignment.center,
210
+ children: [
211
+ const CupertinoActivityIndicator(radius: 36),
212
+ const SizedBox(),
213
+ Text(
214
+ "${S.of(context).verifying_code}...",
215
+ style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
216
+ ),
217
+ Text(
218
+ S.of(context).this_can_take_few_seconds,
219
+ style: TextStyle(
220
+ fontWeight: FontWeight.w400,
221
+ fontSize: 16,
222
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
223
+ ),
224
+ )
225
+ ],
226
+ ),
227
+ );
228
+
229
+ Widget _errorBox(String errorText) => Container(
230
+ key: ValueKey(2),
231
+ width: MediaQuery.of(context).size.width,
232
+ child: Padding(
233
+ padding: const EdgeInsets.symmetric(horizontal: 18),
234
+ child: Column(
235
+ mainAxisAlignment: MainAxisAlignment.center,
236
+ spacing: 12,
237
+ children: [
238
+ const Spacer(),
239
+ Icon(
240
+ Icons.error_outline,
241
+ size: 80,
242
+ color: Theme.of(context).colorScheme.error,
243
+ ),
244
+ Text(
245
+ S.of(context).pairing_error,
246
+ style: TextStyle(
247
+ fontSize: 20,
248
+ fontWeight: FontWeight.w500,
249
+ color: Theme.of(context).colorScheme.onSurface,
250
+ ),
251
+ textAlign: TextAlign.center,
252
+ ),
253
+ Text(
254
+ errorText,
255
+ style: TextStyle(
256
+ fontSize: 16,
257
+ fontWeight: FontWeight.w400,
258
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
259
+ ),
260
+ textAlign: TextAlign.center,
261
+ ),
262
+ const Spacer(),
263
+ NewPrimaryButton(
264
+ onPressed: retry,
265
+ text: S.of(context).try_again,
266
+ color: Theme.of(context).colorScheme.primary,
267
+ textColor: Theme.of(context).colorScheme.onPrimary,
268
+ )
269
+ ],
270
+ ),
271
+ ),
272
+ );
273
+
274
+ Widget _enterPinCode() => Column(
275
+ key: ValueKey(0),
276
+ spacing: 12,
277
+ mainAxisAlignment: MainAxisAlignment.center,
278
+ children: [
279
+ AnimatedContainer(
280
+ duration: pinOpenDuration,
281
+ curve: Curves.easeOutCubic,
282
+ width: _isAwaitingPin ? 40 : 100,
283
+ height: _isAwaitingPin ? 40 : 100,
284
+ child: CakeImageWidget(
285
+ imageUrl: hardwareWalletIcon,
286
+ colorFilter: ColorFilter.mode(
287
+ Theme.of(context).colorScheme.onSurfaceVariant,
288
+ BlendMode.srcIn,
289
+ ),
290
+ ),
291
+ ),
292
+ AnimatedSwitcher(
293
+ duration: pinOpenDuration,
294
+ switchInCurve: Curves.easeInCubic,
295
+ switchOutCurve: Curves.easeOutCubic,
296
+ child: _isAwaitingPin
297
+ ? Column(
298
+ spacing: 24,
299
+ key: ValueKey(1),
300
+ children: [
301
+ Text(
302
+ textAlign: TextAlign.center,
303
+ "${S.of(context).pairing_code_desc_1}\n${S.of(context).pairing_code_desc_2}",
304
+ ),
305
+ DigitInput(
306
+ controller: _controller,
307
+ desiredLength: pinLength,
308
+ )
309
+ ],
310
+ )
311
+ : Column(
312
+ key: ValueKey(0),
313
+ spacing: 12,
314
+ children: [
315
+ Text(
316
+ S.of(context).proceed_on_device,
317
+ style: TextStyle(
318
+ fontWeight: FontWeight.w500,
319
+ fontSize: 20,
320
+ ),
321
+ ),
322
+ Padding(
323
+ padding: EdgeInsets.symmetric(horizontal: 20),
324
+ child: Text(
325
+ S.of(context).proceed_on_device_description,
326
+ textAlign: TextAlign.center,
327
+ style: TextStyle(
328
+ fontSize: 16,
329
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
330
+ ),
331
+ ),
332
+ )
333
+ ],
334
+ ),
335
+ )
336
+ ],
337
+ );
338
+}