dev
dart 340 lines 12 KB
Raw
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 trailingSemanticLabel: S.of(context).close,
110 onTrailingPressed: () => showPopUp(
111 context: context,
112 builder: (context) => AlertWithTwoActions(
113 alertTitle: S.of(context).are_you_sure_exit,
114 alertContent: S.of(context).hww_exit_desc,
115 leftButtonText: S.of(context).cancel,
116 rightButtonText: S.of(context).yes_exit,
117 rightAlertButtonStyle: AlertButtonStyle.error(context),
118 actionLeftButton: Navigator.of(context).pop,
119 actionRightButton: () {
120 Navigator.of(context).pop();
121 Navigator.of(context).pop();
122 },
123 ),
124 ),
125 ),
126 Expanded(
127 child: DirectionalAnimatedSwitcher(
128 duration: Duration(milliseconds: 400),
129 child: content,
130 ),
131 ),
132 Padding(
133 padding: EdgeInsets.only(bottom: 24, right: 24),
134 child: Row(
135 mainAxisAlignment: MainAxisAlignment.spaceBetween,
136 children: [
137 SizedBox(),
138 AnimatedOpacity(
139 curve: Curves.easeOutQuad,
140 opacity: hasFullPin ? 1 : 0,
141 duration: pinOpenDuration,
142 child: ModernButton(
143 backgroundColor: Theme.of(context).colorScheme.primary,
144 iconColor: Theme.of(context).colorScheme.onPrimary,
145 size: 36,
146 icon: Icon(Icons.arrow_forward),
147 semanticLabel: S.of(context).confirm,
148 onPressed: () =>
149 widget.trezorConnectVM.setParingPin(_controller.text.trim()),
150 ),
151 ),
152 ],
153 ),
154 )
155 ],
156 ),
157 )),
158 ),
159 ),
160 );
161 }
162
163 Widget get content {
164 if (_paringState is InitialTrezorParingState || _paringState is EnterPinTrezorParingState) {
165 return _enterPinCode();
166 }
167
168 if (_paringState is VerifyingPinTrezorParingState) return _verifyingCode();
169
170 if (_paringState is FailTrezorParingState)
171 return _errorBox((_paringState as FailTrezorParingState).message);
172
173 return SizedBox.shrink(key: ValueKey(3));
174 }
175
176 void retry() {
177 _controller.text = "";
178 widget.onRetry();
179 }
180
181 String get pageTitle {
182 if (_paringState is InitialTrezorParingState) return S.of(context).device_confirmation;
183 if (_isAwaitingPin) return S.of(context).pairing_code;
184 return "";
185 }
186
187 bool get hasFullPin => _controller.text.length == pinLength && _isAwaitingPin;
188
189 String? get hardwareWalletIcon {
190 switch (widget.hardwareWalletType) {
191 case HardwareWalletType.bitbox:
192 return "assets/new-ui/hardware_wallets/device_bitbox.svg";
193 case HardwareWalletType.ledger:
194 return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
195 case HardwareWalletType.trezor:
196 return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
197 case HardwareWalletType.cupcake:
198 return "assets/images/cupcake.svg";
199 case HardwareWalletType.coldcard:
200 case HardwareWalletType.seedsigner:
201 case HardwareWalletType.keystone:
202 return "assets/images/hardware_wallet/device_qr.svg";
203 }
204 }
205
206 Widget _verifyingCode() => Container(
207 key: ValueKey(1),
208 width: MediaQuery.of(context).size.width,
209 child: Column(
210 spacing: 12,
211 mainAxisAlignment: MainAxisAlignment.center,
212 children: [
213 const CupertinoActivityIndicator(radius: 36),
214 const SizedBox(),
215 Text(
216 "${S.of(context).verifying_code}...",
217 style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
218 ),
219 Text(
220 S.of(context).this_can_take_few_seconds,
221 style: TextStyle(
222 fontWeight: FontWeight.w400,
223 fontSize: 16,
224 color: Theme.of(context).colorScheme.onSurfaceVariant,
225 ),
226 )
227 ],
228 ),
229 );
230
231 Widget _errorBox(String errorText) => Container(
232 key: ValueKey(2),
233 width: MediaQuery.of(context).size.width,
234 child: Padding(
235 padding: const EdgeInsets.symmetric(horizontal: 18),
236 child: Column(
237 mainAxisAlignment: MainAxisAlignment.center,
238 spacing: 12,
239 children: [
240 const Spacer(),
241 Icon(
242 Icons.error_outline,
243 size: 80,
244 color: Theme.of(context).colorScheme.error,
245 ),
246 Text(
247 S.of(context).pairing_error,
248 style: TextStyle(
249 fontSize: 20,
250 fontWeight: FontWeight.w500,
251 color: Theme.of(context).colorScheme.onSurface,
252 ),
253 textAlign: TextAlign.center,
254 ),
255 Text(
256 errorText,
257 style: TextStyle(
258 fontSize: 16,
259 fontWeight: FontWeight.w400,
260 color: Theme.of(context).colorScheme.onSurfaceVariant,
261 ),
262 textAlign: TextAlign.center,
263 ),
264 const Spacer(),
265 NewPrimaryButton(
266 onPressed: retry,
267 text: S.of(context).try_again,
268 color: Theme.of(context).colorScheme.primary,
269 textColor: Theme.of(context).colorScheme.onPrimary,
270 )
271 ],
272 ),
273 ),
274 );
275
276 Widget _enterPinCode() => Column(
277 key: ValueKey(0),
278 spacing: 12,
279 mainAxisAlignment: MainAxisAlignment.center,
280 children: [
281 AnimatedContainer(
282 duration: pinOpenDuration,
283 curve: Curves.easeOutCubic,
284 width: _isAwaitingPin ? 40 : 100,
285 height: _isAwaitingPin ? 40 : 100,
286 child: CakeImageWidget(
287 imageUrl: hardwareWalletIcon,
288 colorFilter: ColorFilter.mode(
289 Theme.of(context).colorScheme.onSurfaceVariant,
290 BlendMode.srcIn,
291 ),
292 ),
293 ),
294 AnimatedSwitcher(
295 duration: pinOpenDuration,
296 switchInCurve: Curves.easeInCubic,
297 switchOutCurve: Curves.easeOutCubic,
298 child: _isAwaitingPin
299 ? Column(
300 spacing: 24,
301 key: ValueKey(1),
302 children: [
303 Text(
304 textAlign: TextAlign.center,
305 "${S.of(context).pairing_code_desc_1}\n${S.of(context).pairing_code_desc_2}",
306 ),
307 DigitInput(
308 controller: _controller,
309 desiredLength: pinLength,
310 )
311 ],
312 )
313 : Column(
314 key: ValueKey(0),
315 spacing: 12,
316 children: [
317 Text(
318 S.of(context).proceed_on_device,
319 style: TextStyle(
320 fontWeight: FontWeight.w500,
321 fontSize: 20,
322 ),
323 ),
324 Padding(
325 padding: EdgeInsets.symmetric(horizontal: 20),
326 child: Text(
327 S.of(context).proceed_on_device_description,
328 textAlign: TextAlign.center,
329 style: TextStyle(
330 fontSize: 16,
331 color: Theme.of(context).colorScheme.onSurfaceVariant,
332 ),
333 ),
334 )
335 ],
336 ),
337 )
338 ],
339 );
340 }