dev
dart 426 lines 16 KB
Raw
1 import "dart:async";
2 import "dart:io";
3
4 import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
5 import "package:cake_wallet/generated/i18n.dart";
6 import "package:cake_wallet/main.dart";
7 import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
8 import "package:cake_wallet/routes.dart";
9 import "package:cake_wallet/src/screens/base_page.dart";
10 import "package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart";
11 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
12 import "package:cake_wallet/src/widgets/primary_button.dart";
13 import "package:cake_wallet/themes/core/material_base_theme.dart";
14 import "package:cake_wallet/utils/responsive_layout_util.dart";
15 import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
16 import "package:cw_core/utils/print_verbose.dart";
17 import "package:cw_core/wallet_info.dart";
18 import "package:cw_core/wallet_type.dart";
19 import "package:flutter/cupertino.dart";
20 import "package:flutter/material.dart";
21 import "package:flutter_mobx/flutter_mobx.dart";
22
23 typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel);
24
25 class ConnectDevicePageParams {
26 const ConnectDevicePageParams({
27 required this.walletType,
28 required this.hardwareWalletType,
29 required this.onConnectDevice,
30 this.allowChangeWallet = false,
31 this.isReconnect = true,
32 });
33
34 final WalletType walletType;
35 final OnConnectDevice onConnectDevice;
36 final bool allowChangeWallet;
37 final bool isReconnect;
38 final HardwareWalletType hardwareWalletType;
39 }
40
41 class ConnectDevicePage extends BasePage {
42 ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
43 : walletType = params.walletType,
44 onConnectDevice = params.onConnectDevice,
45 allowChangeWallet = params.allowChangeWallet || params.isReconnect,
46 isReconnect = params.isReconnect;
47 final WalletType walletType;
48 final OnConnectDevice onConnectDevice;
49 final bool allowChangeWallet;
50 final bool isReconnect;
51 final HardwareWalletViewModel hardwareWalletVM;
52
53 @override
54 String get title => isReconnect
55 ? S.current.reconnect_your_hardware_wallet
56 : S.current.restore_title_from_hardware_wallet;
57
58 @override
59 Widget? leading(BuildContext context) => !isReconnect ? super.leading(context) : null;
60
61 @override
62 Widget body(BuildContext context) => PopScope(
63 canPop: !isReconnect,
64 child: ConnectDevicePageBody(
65 walletType,
66 onConnectDevice,
67 hardwareWalletVM,
68 currentTheme,
69 allowChangeWallet: allowChangeWallet,
70 ),
71 );
72 }
73
74 class ConnectDevicePageBody extends StatefulWidget {
75 const ConnectDevicePageBody(
76 this.walletType,
77 this.onConnectDevice,
78 this.hardwareWalletVM,
79 this.currentTheme, {
80 this.allowChangeWallet = false,
81 });
82
83 final WalletType walletType;
84 final OnConnectDevice onConnectDevice;
85 final bool allowChangeWallet;
86 final HardwareWalletViewModel hardwareWalletVM;
87 final MaterialThemeBase currentTheme;
88
89 @override
90 ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState();
91 }
92
93 class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
94 var bleDevices = <HardwareWalletDevice>[];
95 var usbDevices = <HardwareWalletDevice>[];
96
97 List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
98
99 Timer? _usbRefreshTimer;
100 Timer? _bleRefreshTimer;
101 Timer? _bleStateTimer;
102 StreamSubscription<HardwareWalletDevice>? _bleRefresh;
103
104 bool longWait = false;
105 Timer? _longWaitTimer;
106
107 @override
108 void initState() {
109 super.initState();
110 WidgetsBinding.instance.addPostFrameCallback((_) {
111 _bleStateTimer = Timer.periodic(
112 const Duration(seconds: 1),
113 (_) => widget.hardwareWalletVM.updateBleState(),
114 );
115
116 _bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices());
117
118 if (Platform.isAndroid) {
119 _usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices());
120 }
121
122 if (widget.hardwareWalletVM.hasBluetooth) {
123 widget.hardwareWalletVM.updateBleState();
124
125 _longWaitTimer = Timer(const Duration(seconds: 10), () {
126 if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) {
127 setState(() => longWait = true);
128 }
129 });
130 }
131 });
132 }
133
134 @override
135 void dispose() {
136 _bleRefreshTimer?.cancel();
137 _bleStateTimer?.cancel();
138 _usbRefreshTimer?.cancel();
139 _bleRefresh?.cancel();
140 _longWaitTimer?.cancel();
141
142 widget.hardwareWalletVM.stopScanning();
143 _isConnectPressed = false;
144 super.dispose();
145 }
146
147 var _isRefreshingUsb = false;
148 Future<void> _refreshUsbDevices() async {
149 if (_isRefreshingUsb) {
150 return;
151 }
152 _isRefreshingUsb = true;
153 try {
154 final dev = await widget.hardwareWalletVM.getAllUsbDevices();
155
156 if (usbDevices.length != dev.length) {
157 setState(() => usbDevices = dev);
158 }
159 } catch(e) {
160 printV(e);
161 }
162 _isRefreshingUsb = false;
163 }
164
165 Future<void> _refreshBleDevices() async {
166 try {
167 if (widget.hardwareWalletVM.isBleEnabled) {
168 _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen(
169 (device) => setState(() {
170 bleDevices.add(device);
171 if (longWait) {
172 longWait = false;
173 }
174 }),
175 )..onError((e) {
176 printV(e);
177 });
178 _bleRefreshTimer?.cancel();
179 _bleRefreshTimer = null;
180 }
181 } catch (e) {
182 printV(e);
183 }
184 }
185
186 var _isConnectPressed = false;
187
188 Future<void> _connectToDevice(HardwareWalletDevice device) async {
189 if (_isConnectPressed) {
190 return;
191 }
192
193 _isConnectPressed = true;
194 try {
195 final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
196 _isConnectPressed = false;
197
198 WidgetsBinding.instance.addPostFrameCallback((_) {
199 if (isConnected) {
200 widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
201 }
202 });
203 } catch (_) {
204 _isConnectPressed = false;
205 }
206 }
207
208 String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
209 printV(deviceType);
210 switch (deviceType) {
211 case HardwareWalletDeviceType.ledgerNanoX:
212 return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
213 case HardwareWalletDeviceType.ledgerNanoGen5:
214 return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg";
215 case HardwareWalletDeviceType.ledgerStax:
216 return "assets/new-ui/hardware_wallets/device_ledger_stax.svg";
217 case HardwareWalletDeviceType.ledgerFlex:
218 return "assets/new-ui/hardware_wallets/device_ledger_flex.svg";
219 case HardwareWalletDeviceType.BitBox02:
220 case HardwareWalletDeviceType.BitBox02Nova:
221 return "assets/new-ui/hardware_wallets/device_bitbox.svg";
222 case HardwareWalletDeviceType.trezorModelOne:
223 return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg";
224 case HardwareWalletDeviceType.trezorModelT:
225 return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg";
226 case HardwareWalletDeviceType.trezorSafe3:
227 return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg";
228 case HardwareWalletDeviceType.trezorSafe5:
229 return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
230 case HardwareWalletDeviceType.trezorSafe7:
231 return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
232
233 default:
234 return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
235 }
236 }
237
238 String get description {
239 if (!Platform.isAndroid) {
240 return S.of(context).connect_your_hardware_wallet_ble;
241 }
242
243 if (widget.hardwareWalletVM.hasBluetooth) {
244 return S.of(context).connect_your_hardware_wallet;
245 }
246
247 return S.of(context).connect_your_hardware_wallet_usb;
248 }
249
250 @override
251 Widget build(BuildContext context) => Center(
252 child: Container(
253 width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
254 padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24),
255 child: Column(
256 children: [
257 Expanded(
258 child: SingleChildScrollView(
259 child: Column(
260 spacing: 24,
261 children: [
262 const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"),
263 Text(
264 description,
265 style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
266 textAlign: TextAlign.center,
267 ),
268 Observer(
269 builder: (_) => Offstage(
270 offstage: widget.hardwareWalletVM.isBleEnabled ||
271 !widget.hardwareWalletVM.hasBluetooth,
272 child: Padding(
273 padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
274 child: Text(
275 S.of(context).ledger_please_enable_bluetooth,
276 style: Theme.of(context).textTheme.titleMedium,
277 textAlign: TextAlign.center,
278 ),
279 ),
280 ),
281 ),
282 Container(
283 decoration: BoxDecoration(
284 color: Theme.of(context).colorScheme.surfaceContainer,
285 borderRadius: BorderRadius.circular(18),
286 ),
287 child: ListView.separated(
288 shrinkWrap: true,
289 padding: EdgeInsets.zero,
290 itemBuilder: (context, index) {
291 final item = allDevices[index];
292 return GestureDetector(
293 onTap: () => _connectToDevice(item),
294 behavior: HitTestBehavior.opaque,
295 child: SizedBox(
296 height: 48,
297 child: Padding(
298 padding: const EdgeInsets.symmetric(horizontal: 12),
299 child: Row(
300 mainAxisAlignment: MainAxisAlignment.spaceBetween,
301 children: [
302 Row(
303 spacing: 12,
304 children: [
305 Observer(
306 builder: (_) {
307 if (widget.hardwareWalletVM.isConnecting) {
308 return CupertinoActivityIndicator(
309 animating: true,
310 color: Theme.of(context)
311 .colorScheme
312 .onSurfaceVariant,
313 radius: 12,
314 );
315 }
316
317 return CakeImageWidget(
318 imageUrl: _getDeviceTileLeading(item.type),
319 width: 24,
320 height: 24,
321 colorFilter: ColorFilter.mode(
322 Theme.of(context).colorScheme.onSurfaceVariant,
323 BlendMode.srcIn,
324 ),
325 );
326 },
327 ),
328 Text(item.name),
329 ],
330 ),
331 Row(
332 spacing: 12,
333 children: [
334 if (item.connectionType ==
335 HardwareWalletConnectionType.ble)
336 CakeImageWidget(
337 imageUrl: "assets/new-ui/bluetooth.svg",
338 width: 24,
339 height: 24,
340 colorFilter: ColorFilter.mode(
341 Theme.of(context).colorScheme.primary,
342 BlendMode.srcIn,
343 ),
344 ),
345 CakeImageWidget(
346 imageUrl: "assets/new-ui/arrow_forward.svg",
347 height: 16,
348 colorFilter: ColorFilter.mode(
349 Theme.of(context).colorScheme.onSurfaceVariant,
350 BlendMode.srcIn,
351 ),
352 ),
353 ],
354 ),
355 ],
356 ),
357 ),
358 ),
359 );
360 },
361 separatorBuilder: (context, index) => Container(
362 height: 1,
363 color: Theme.of(context).colorScheme.surfaceContainerHigh,
364 ),
365 itemCount: allDevices.length,
366 ),
367 ),
368 if (widget.allowChangeWallet) ...[
369 PrimaryButton(
370 text: S.of(context).wallets,
371 color: Theme.of(context).colorScheme.primary,
372 textColor: Theme.of(context).colorScheme.onPrimary,
373 onPressed: _onChangeWallet,
374 ),
375 ],
376 ],
377 ),
378 ),
379 ),
380 NewPrimaryButton(
381 text: S.of(context).how_to_connect,
382 color: Theme.of(context).colorScheme.surfaceContainer,
383 textColor: Theme.of(context).colorScheme.primary,
384 onPressed: () => _onHowToConnect(context),
385 ),
386 ],
387 ),
388 ),
389 );
390
391 void _onChangeWallet() {
392 Navigator.of(context).pushNamed(
393 Routes.walletList,
394 arguments: (BuildContext context) =>
395 Navigator.of(context).pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
396 );
397 }
398
399 void _onHowToConnect(BuildContext context) {
400 showModalBottomSheet(
401 context: context,
402 isScrollControlled: true,
403 builder: (_) => InfoStepsBottomSheet(
404 titleText: S.of(context).how_to_connect,
405 steps: [
406 InfoStep(
407 "assets/images/wallet_connect_step_icons/step1_power.svg",
408 S.of(context).connect_hw_info_step_1,
409 ),
410 InfoStep(
411 "assets/images/wallet_connect_step_icons/step2_connect.svg",
412 S.of(context).connect_hw_info_step_2,
413 ),
414 InfoStep(
415 "assets/images/wallet_connect_step_icons/step3_unlock.svg",
416 S.of(context).connect_hw_info_step_3,
417 ),
418 InfoStep(
419 "assets/images/wallet_connect_step_icons/step4_select.svg",
420 S.of(context).connect_hw_info_step_4,
421 ),
422 ],
423 ),
424 );
425 }
426 }