dev
dart 592 lines 26.6 KB
Raw
1 import 'dart:async';
2
3 import 'package:another_flushbar/flushbar.dart';
4 import 'package:cake_wallet/core/auth_service.dart';
5 import 'package:cake_wallet/core/new_wallet_arguments.dart';
6 import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
7 import 'package:cake_wallet/entities/wallet_list_order_types.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/monero/monero.dart';
10 import 'package:cake_wallet/routes.dart';
11 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
12 import 'package:cake_wallet/src/screens/base_page.dart';
13 import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
14 import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
15 import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart';
16 import 'package:cake_wallet/src/screens/wallet_list/edit_wallet_button_widget.dart';
17 import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
18 import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
19 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
20 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
21 import 'package:cake_wallet/src/widgets/gradient_background.dart';
22 import 'package:cake_wallet/src/widgets/primary_button.dart';
23 import 'package:cake_wallet/store/settings_store.dart';
24 import 'package:cake_wallet/utils/exception_handler.dart';
25 import 'package:cake_wallet/utils/feature_flag.dart';
26 import 'package:cake_wallet/utils/responsive_layout_util.dart';
27 import 'package:cake_wallet/utils/show_bar.dart';
28 import 'package:cake_wallet/utils/show_pop_up.dart';
29 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
30 import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
31 import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
32 import 'package:cake_wallet/wallet_type_utils.dart';
33 import 'package:cw_core/currency_for_wallet_type.dart';
34 import 'package:cw_core/utils/print_verbose.dart';
35 import 'package:cw_core/wallet_info.dart';
36 import 'package:cw_core/wallet_type.dart';
37 import 'package:flutter/material.dart';
38 import 'package:flutter_mobx/flutter_mobx.dart';
39
40 class WalletListPage extends BasePage {
41 WalletListPage({
42 required this.walletListViewModel,
43 required this.authService,
44 this.onWalletLoaded,
45 });
46
47 @override
48 bool get gradientBackground => true;
49
50 final WalletListViewModel walletListViewModel;
51 final AuthService authService;
52 final Future<void> Function(BuildContext)? onWalletLoaded;
53
54 @override
55 String get title => S.current.wallets;
56
57 @override
58 Widget body(BuildContext context) => Observer(builder: (_) {
59 if (walletListViewModel.singleWalletsList.isEmpty &&
60 walletListViewModel.multiWalletGroups.isEmpty) {
61 return Center(
62 child: CircularProgressIndicator(),
63 );
64 }
65 return WalletListBody(
66 walletListViewModel: walletListViewModel,
67 authService: authService,
68 onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(),
69 );
70 });
71
72 @override
73 Widget trailing(BuildContext context) {
74 return MergeSemantics(
75 child: SizedBox(
76 height: 37,
77 width: 37,
78 child: ButtonTheme(
79 minWidth: double.minPositive,
80 child: Semantics(
81 container: true,
82 child: GestureDetector(
83 onTap: () async {
84 await showPopUp<void>(
85 context: context,
86 builder: (context) => FilterListWidget(
87 initalType: walletListViewModel.orderType,
88 initalAscending: walletListViewModel.ascending,
89 onClose: (bool ascending, FilterListOrderType type) async {
90 walletListViewModel.setAscending(ascending);
91 await walletListViewModel.setOrderType(type);
92 },
93 ),
94 );
95 },
96 child: Semantics(
97 label: 'Transaction Filter',
98 button: true,
99 enabled: true,
100 child: Container(
101 height: 36,
102 width: 36,
103 decoration: BoxDecoration(
104 shape: BoxShape.circle,
105 color: Theme.of(context).colorScheme.surfaceContainer,
106 ),
107 child: Image.asset(
108 'assets/images/filter_icon.png',
109 color: Theme.of(context).colorScheme.primary,
110 ),
111 ),
112 ),
113 ),
114 ),
115 ),
116 ),
117 );
118 }
119 }
120
121 class WalletListBody extends StatefulWidget {
122 WalletListBody({
123 required this.walletListViewModel,
124 required this.authService,
125 required this.onWalletLoaded,
126 });
127
128 final WalletListViewModel walletListViewModel;
129 final AuthService authService;
130 final Function(BuildContext) onWalletLoaded;
131
132 @override
133 WalletListBodyState createState() => WalletListBodyState();
134 }
135
136 class WalletListBodyState extends State<WalletListBody> {
137 final scrollController = ScrollController();
138 final double tileHeight = 60;
139 Flushbar<void>? _progressBar;
140
141 bool _loadingWallet = false;
142
143 @override
144 Widget build(BuildContext context) {
145 return GradientBackground(
146 scaffold: Container(
147 height: double.infinity,
148 padding: EdgeInsets.only(top: 16),
149 child: Stack(
150 alignment: Alignment.bottomCenter,
151 fit: StackFit.expand,
152 children: <Widget>[
153 SingleChildScrollView(
154 physics: AlwaysScrollableScrollPhysics(),
155 child: Column(
156 crossAxisAlignment: CrossAxisAlignment.start,
157 children: [
158 if (widget.walletListViewModel.multiWalletGroups.isNotEmpty) ...{
159 Padding(
160 padding: const EdgeInsets.only(left: 24),
161 child: Text(
162 S.current.shared_seed_wallets,
163 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
164 fontSize: 18,
165 fontWeight: FontWeight.w700,
166 color: Theme.of(context).colorScheme.onSurface,
167 ),
168 ),
169 ),
170 SizedBox(height: 16),
171 Container(
172 child: Padding(
173 padding: EdgeInsets.only(left: 20, right: 20),
174 child: Observer(
175 builder: (_) => FilteredList(
176 shrinkWrap: true,
177 list: widget.walletListViewModel.multiWalletGroups,
178 updateFunction: widget.walletListViewModel.reorderAccordingToWalletList,
179 itemBuilder: (context, index) {
180 final group = widget.walletListViewModel.multiWalletGroups[index];
181 final groupName =
182 group.groupName ?? '${S.current.wallet_group} ${index + 1}';
183
184 widget.walletListViewModel.updateTileState(
185 index,
186 widget.walletListViewModel.expansionTileStateTrack[index] ?? false,
187 );
188
189 return GroupedWalletExpansionTile(
190 onExpansionChanged: (value) {
191 widget.walletListViewModel.updateTileState(index, value);
192 setState(() {});
193 },
194 shouldShowCurrentWalletPointer: true,
195 borderRadius: BorderRadius.all(Radius.circular(18)),
196 title: groupName,
197 tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'),
198 leadingWidget: CakeImageWidget(
199 imageUrl: "assets/new-ui/navbar/wallets.svg",
200 width: 28,
201 color: Theme.of(context).colorScheme.onSurfaceVariant,
202 ),
203 trailingWidget: EditWalletButtonWidget(
204 width: 88,
205 isGroup: true,
206 isExpanded:
207 widget.walletListViewModel.expansionTileStateTrack[index]!,
208 onTap: () {
209 final wallet = widget.walletListViewModel
210 .convertWalletInfoToWalletListItem(group.wallets.first);
211 Navigator.of(context).pushNamed(
212 Routes.walletEdit,
213 arguments: WalletEditPageArguments(
214 walletListViewModel: widget.walletListViewModel,
215 editingWallet: wallet,
216 isWalletGroup: true,
217 groupName: groupName,
218 walletGroupKey: group.groupKey,
219 ),
220 );
221 },
222 ),
223 childWallets: group.wallets.map((walletInfo) {
224 return widget.walletListViewModel
225 .convertWalletInfoToWalletListItem(walletInfo);
226 }).toList(),
227 isSelected: false,
228 onChildItemTapped: (wallet) =>
229 wallet.isCurrent ? null : _loadWallet(wallet),
230 childTrailingWidget: (item) {
231 return item.isCurrent
232 ? SizedBox.shrink()
233 : EditWalletButtonWidget(
234 width: 64,
235 onTap: () => Navigator.of(context).pushNamed(
236 Routes.walletEdit,
237 arguments: WalletEditPageArguments(
238 walletListViewModel: widget.walletListViewModel,
239 editingWallet: item,
240 ),
241 ),
242 );
243 },
244 );
245 },
246 ),
247 ),
248 ),
249 ),
250 SizedBox(height: 24),
251 },
252 if (widget.walletListViewModel.singleWalletsList.isNotEmpty) ...{
253 Padding(
254 padding: const EdgeInsets.only(left: 24),
255 child: Text(
256 S.current.single_seed_wallets_group,
257 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
258 fontSize: 18,
259 fontWeight: FontWeight.w700,
260 color: Theme.of(context).colorScheme.onSurface,
261 ),
262 ),
263 ),
264 SizedBox(height: 16),
265 Padding(
266 padding: EdgeInsets.only(left: 20, right: 20),
267 child: Container(
268 child: Observer(
269 builder: (_) => FilteredList(
270 shrinkWrap: true,
271 list: widget.walletListViewModel.singleWalletsList,
272 updateFunction: widget.walletListViewModel.reorderAccordingToWalletList,
273 itemBuilder: (context, index) {
274 final wallet = widget.walletListViewModel.singleWalletsList[index];
275 final currentColor = wallet.isCurrent
276 ? Theme.of(context).colorScheme.primary
277 : Theme.of(context).colorScheme.surface;
278 return GroupedWalletExpansionTile(
279 tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'),
280 isCurrentlySelectedWallet: wallet.isCurrent,
281 leadingWidget: SizedBox(
282 width: wallet.isCurrent ? 56 : 40,
283 child: Row(
284 children: [
285 wallet.isCurrent
286 ? Container(
287 height: 35,
288 width: 6,
289 margin: EdgeInsets.only(right: 16),
290 decoration: BoxDecoration(
291 borderRadius: BorderRadius.only(
292 topRight: Radius.circular(18),
293 bottomRight: Radius.circular(18),
294 ),
295 color: currentColor,
296 ),
297 )
298 : SizedBox(width: 6),
299 CakeImageWidget(
300 imageUrl: getCryptoCurrencyIconForWalletListItem(
301 wallet.type,
302 ),
303 width: 32,
304 height: 32,
305 ),
306 ],
307 ),
308 ),
309 title: wallet.name,
310 isSelected: false,
311 borderRadius: BorderRadius.all(Radius.circular(18)),
312 margin: EdgeInsets.only(left: 20, right: 20, bottom: 12),
313 onTitleTapped: () => wallet.isCurrent ? null : _loadWallet(wallet),
314 trailingWidget: wallet.isCurrent
315 ? null
316 : EditWalletButtonWidget(
317 width: 64,
318 onTap: () {
319 Navigator.of(context).pushNamed(
320 Routes.walletEdit,
321 arguments: WalletEditPageArguments(
322 walletListViewModel: widget.walletListViewModel,
323 editingWallet: wallet,
324 ),
325 );
326 },
327 ),
328 );
329 },
330 ),
331 ),
332 ),
333 ),
334 },
335 SizedBox(height: 250),
336 ],
337 ),
338 ),
339 Stack(
340 alignment: Alignment.bottomCenter,
341 children: [
342 !FeatureFlag.hasNewUi
343 ? IgnorePointer(
344 child: Container(
345 alignment: Alignment.bottomCenter,
346 height: 185,
347 decoration: BoxDecoration(
348 gradient: LinearGradient(
349 begin: Alignment.topCenter,
350 end: Alignment.bottomCenter,
351 colors: <Color>[
352 Theme.of(context).colorScheme.surface.withAlpha(10),
353 Theme.of(context).colorScheme.surface,
354 Theme.of(context).colorScheme.surface,
355 Theme.of(context).colorScheme.surface
356 ],
357 ),
358 ),
359 ),
360 )
361 : IgnorePointer(
362 child: Container(
363 height: 275,
364 decoration: BoxDecoration(
365 gradient: LinearGradient(
366 begin: Alignment.topCenter,
367 end: Alignment.bottomCenter,
368 colors: <Color>[
369 Theme.of(context).colorScheme.surfaceDim.withAlpha(10),
370 Theme.of(context).colorScheme.surfaceDim.withAlpha(150),
371 Theme.of(context).colorScheme.surfaceDim.withAlpha(255),
372 Theme.of(context).colorScheme.surfaceDim.withAlpha(255),
373 Theme.of(context).colorScheme.surfaceDim.withAlpha(255),
374 Theme.of(context).colorScheme.surfaceDim.withAlpha(255),
375 Theme.of(context).colorScheme.surfaceDim.withAlpha(255)
376 ],
377 ),
378 ),
379 ),
380 ),
381 Container(
382 height: 240,
383 width: MediaQuery.of(context).size.width,
384 margin: EdgeInsets.only(bottom: 24),
385 padding: EdgeInsets.only(left: 16, right: 16),
386 child: Column(
387 mainAxisSize: MainAxisSize.min,
388 mainAxisAlignment: MainAxisAlignment.center,
389 crossAxisAlignment: CrossAxisAlignment.center,
390 children: <Widget>[
391 PrimaryImageButton(
392 image: Image.asset(
393 'assets/images/restore_wallet.png',
394 height: 12,
395 width: 12,
396 color: Theme.of(context).colorScheme.onSecondaryContainer,
397 ),
398 key: ValueKey('wallet_list_page_restore_wallet_button_key'),
399 onPressed: () {
400 if (widget
401 .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
402 widget.authService.authenticateAction(
403 context,
404 route: Routes.restoreOptions,
405 arguments: false,
406 conditionToDetermineIfToUse2FA: widget
407 .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
408 );
409 } else {
410 Navigator.of(context)
411 .pushNamed(Routes.restoreOptions, arguments: false);
412 }
413 },
414 text: S.of(context).wallet_list_restore_wallet,
415 color: Theme.of(context).colorScheme.surfaceContainer,
416 textColor: Theme.of(context).colorScheme.onSecondaryContainer,
417 ),
418 SizedBox(height: 10.0),
419 PrimaryImageButton(
420 image: Image.asset(
421 'assets/images/new_wallet.png',
422 height: 12,
423 width: 12,
424 color: Theme.of(context).colorScheme.onPrimary,
425 ),
426 key: ValueKey('wallet_list_page_create_new_wallet_button_key'),
427 onPressed: () {
428 //TODO(David): Find a way to optimize this
429 if (isSingleCoin) {
430 if (widget
431 .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
432 widget.authService.authenticateAction(
433 context,
434 route: Routes.newWallet,
435 arguments: NewWalletArguments(
436 type: widget.walletListViewModel.currentWalletType,
437 ),
438 conditionToDetermineIfToUse2FA: widget
439 .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
440 );
441 } else {
442 Navigator.of(context).pushNamed(
443 Routes.newWallet,
444 arguments: NewWalletArguments(
445 type: widget.walletListViewModel.currentWalletType,
446 ),
447 );
448 }
449 } else {
450 if (widget
451 .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
452 widget.authService.authenticateAction(
453 context,
454 route: Routes.newWalletType,
455 conditionToDetermineIfToUse2FA: widget
456 .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
457 );
458 } else {
459 Navigator.of(context).pushNamed(Routes.newWalletType);
460 }
461 }
462 },
463 text: S.of(context).wallet_list_create_new_wallet,
464 color: Theme.of(context).colorScheme.primary,
465 textColor: Theme.of(context).colorScheme.onPrimary,
466 ),
467 if (FeatureFlag.hasNewUi) SizedBox(height: 52.0)
468 ],
469 ),
470 ),
471 ],
472 ),
473 ],
474 ),
475 ),
476 );
477 }
478
479 Future<void> _loadWallet(WalletListItem wallet) async {
480 if (_loadingWallet) {
481 printV("_loadWallet abandoned because _loadingWallet");
482 return;
483 }
484
485 _loadingWallet = true;
486
487 if (SettingsStoreBase.walletPasswordDirectInput) {
488 Navigator.of(context).pushNamed(Routes.walletUnlockLoadable,
489 arguments: WalletUnlockArguments(
490 callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
491 if (isAuthenticatedSuccessfully) {
492 auth.close();
493 setState(() {});
494 }
495 },
496 walletName: wallet.name,
497 walletType: wallet.type));
498 _loadingWallet = false;
499 return;
500 }
501
502 await widget.authService.authenticateAction(
503 context,
504 onAuthSuccess: (isAuthenticatedSuccessfully) async {
505 if (!isAuthenticatedSuccessfully) {
506 printV("!isAuthenticatedSuccessfully");
507 _loadingWallet = false;
508 return;
509 }
510
511 try {
512 final requireHardwareWalletConnection =
513 await widget.walletListViewModel.requireHardwareWalletConnection(wallet);
514 if (requireHardwareWalletConnection) {
515 bool didConnect = false;
516 await Navigator.of(context).pushNamed(
517 Routes.connectDevices,
518 arguments: ConnectDevicePageParams(
519 walletType: WalletType.monero,
520 hardwareWalletType: HardwareWalletType.ledger,
521 onConnectDevice: (context, ledgerVM) async {
522 if (ledgerVM is LedgerViewModel) {
523 monero!.setGlobalLedgerConnection(ledgerVM.connection);
524 didConnect = true;
525 Navigator.of(context).pop();
526 }
527 },
528 isReconnect: true,
529 ),
530 );
531
532 if (!didConnect) return;
533
534 showPopUp<void>(
535 context: context,
536 builder: (BuildContext context) => AlertWithOneAction(
537 alertTitle: S.of(context).proceed_on_device,
538 alertContent: S.of(context).proceed_on_device_description,
539 buttonText: S.of(context).cancel,
540 alertBarrierDismissible: false,
541 buttonAction: () => Navigator.of(context).pop()),
542 );
543 }
544 changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
545 await widget.walletListViewModel.loadWallet(wallet);
546 // only pop the wallets route in mobile as it will go back to dashboard page
547 // in desktop platforms the navigation tree is different
548 unawaited(hideProgressText());
549 if (responsiveLayoutUtil.shouldRenderMobileUI) {
550 // await Future.delayed(Duration(seconds: 1));
551 if (!this.mounted) return;
552 if (!context.mounted) return;
553 if (requireHardwareWalletConnection) {
554 Navigator.of(context).pop();
555 }
556 await widget.onWalletLoaded.call(context);
557 }
558 } catch (e) {
559 await ExceptionHandler.resetLastPopupDate();
560 final err = e.toString();
561 await ExceptionHandler.onError(FlutterErrorDetails(exception: err));
562 if (this.mounted) {
563 changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
564 }
565 } finally {
566 _loadingWallet = false;
567 }
568 },
569 conditionToDetermineIfToUse2FA:
570 widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
571 );
572 _loadingWallet = false;
573 }
574
575 void changeProcessText(String text) {
576 try {
577 if (_progressBar != null) {
578 _progressBar!.dismiss();
579 }
580 _progressBar = createBar<void>(text, context, duration: null)..show(context);
581 } catch (e) {}
582 }
583
584 Future<void> hideProgressText() async {
585 await Future.delayed(Duration(milliseconds: 50), () {
586 try {
587 _progressBar?.dismiss();
588 _progressBar = null;
589 } catch (e) {}
590 });
591 }
592 }