dev
dart 693 lines 31.7 KB
Raw
1 import 'dart:math';
2
3 import 'package:auto_size_text/auto_size_text.dart';
4 import 'package:cake_wallet/bitcoin/bitcoin.dart';
5 import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/routes.dart';
7 import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
8 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
9 import 'package:cake_wallet/themes/core/theme_extension.dart';
10 import 'package:cake_wallet/utils/payment_request.dart';
11 import 'package:cake_wallet/utils/show_pop_up.dart';
12 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
13 import 'package:cw_core/crypto_currency.dart';
14 import 'package:cw_core/unspent_coin_type.dart';
15 import 'package:cw_core/wallet_type.dart';
16 import 'package:flutter/material.dart';
17 import 'package:flutter_svg/flutter_svg.dart';
18 import 'package:fluttertoast/fluttertoast.dart';
19 import 'package:url_launcher/url_launcher.dart';
20
21 class BalanceRowWidget extends StatelessWidget {
22 BalanceRowWidget({
23 required this.availableBalanceLabel,
24 required this.availableBalance,
25 required this.availableFiatBalance,
26 required this.additionalBalanceLabel,
27 required this.additionalBalance,
28 required this.additionalFiatBalance,
29 required this.secondAvailableBalanceLabel,
30 required this.secondAvailableBalance,
31 required this.secondAvailableFiatBalance,
32 required this.secondAdditionalBalanceLabel,
33 required this.secondAdditionalBalance,
34 required this.secondAdditionalFiatBalance,
35 required this.frozenBalance,
36 required this.frozenFiatBalance,
37 required this.currency,
38 required this.hasAdditionalBalance,
39 required this.hasSecondAvailableBalance,
40 required this.hasSecondAdditionalBalance,
41 required this.isTestnet,
42 required this.dashboardViewModel,
43 super.key,
44 });
45
46 final String availableBalanceLabel;
47 final String availableBalance;
48 final String availableFiatBalance;
49 final String additionalBalanceLabel;
50 final String additionalBalance;
51 final String additionalFiatBalance;
52 final String secondAvailableBalanceLabel;
53 final String secondAvailableBalance;
54 final String secondAvailableFiatBalance;
55 final String secondAdditionalBalanceLabel;
56 final String secondAdditionalBalance;
57 final String secondAdditionalFiatBalance;
58 final String frozenBalance;
59 final String frozenFiatBalance;
60 final CryptoCurrency currency;
61 final bool hasAdditionalBalance;
62 final bool hasSecondAvailableBalance;
63 final bool hasSecondAdditionalBalance;
64 final bool isTestnet;
65 final DashboardViewModel dashboardViewModel;
66
67 @override
68 Widget build(BuildContext context) {
69 return Column(
70 children: [
71 Container(
72 margin: const EdgeInsets.only(left: 16, right: 16),
73 decoration: BoxDecoration(
74 borderRadius: BorderRadius.circular(15.0),
75 gradient: LinearGradient(
76 colors: [
77 context.customColors.cardGradientColorPrimary,
78 context.customColors.cardGradientColorSecondary,
79 ],
80 begin: Alignment.topCenter,
81 end: Alignment.bottomCenter,
82 ),
83 ),
84 child: TextButton(
85 onPressed: _showToast,
86 onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
87 style: TextButton.styleFrom(
88 side: BorderSide(
89 width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
90 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
91 ),
92 child: Container(
93 margin: const EdgeInsets.only(
94 top: 10,
95 left: 12,
96 right: 12,
97 bottom: 10,
98 ),
99 child: Column(
100 crossAxisAlignment: CrossAxisAlignment.start,
101 children: [
102 Row(
103 mainAxisAlignment: MainAxisAlignment.spaceBetween,
104 crossAxisAlignment: CrossAxisAlignment.center,
105 children: [
106 Expanded(
107 child: Column(
108 crossAxisAlignment: CrossAxisAlignment.start,
109 children: [
110 GestureDetector(
111 behavior: HitTestBehavior.opaque,
112 onTap: hasAdditionalBalance
113 ? () => _showBalanceDescription(
114 context, S.of(context).available_balance_description)
115 : null,
116 child: Row(
117 children: [
118 Semantics(
119 hint: 'Double tap to see more information',
120 container: true,
121 child: Text(
122 '${availableBalanceLabel}',
123 style: Theme.of(context).textTheme.bodySmall?.copyWith(
124 color: Theme.of(context).colorScheme.onSurfaceVariant,
125 height: 1,
126 ),
127 ),
128 ),
129 if (hasAdditionalBalance)
130 Padding(
131 padding: const EdgeInsets.symmetric(horizontal: 4),
132 child: Icon(
133 Icons.help_outline,
134 size: 16,
135 color: Theme.of(context).colorScheme.onSurfaceVariant,
136 ),
137 ),
138 ],
139 ),
140 ),
141 SizedBox(height: 6),
142 AutoSizeText(
143 availableBalance,
144 minFontSize: 16,
145 overflow: TextOverflow.ellipsis,
146 style: Theme.of(context).textTheme.titleLarge?.copyWith(
147 color: Theme.of(context).colorScheme.onSurface,
148 fontWeight: FontWeight.w900,
149 fontSize: 24,
150 height: 1,
151 ),
152 maxLines: 1,
153 textAlign: TextAlign.start,
154 ),
155 SizedBox(height: 6),
156 if (isTestnet)
157 Text(
158 S.of(context).testnet_coins_no_value,
159 textAlign: TextAlign.center,
160 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
161 height: 1,
162 ),
163 ),
164 if (!isTestnet)
165 Text(
166 '${availableFiatBalance}',
167 textAlign: TextAlign.center,
168 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
169 fontSize: 16,
170 fontWeight: FontWeight.w500,
171 color: Theme.of(context).colorScheme.onSurfaceVariant,
172 height: 1,
173 ),
174 ),
175 ],
176 ),
177 ),
178 Expanded(
179 child: Column(
180 crossAxisAlignment: CrossAxisAlignment.end,
181 children: [
182 CakeImageWidget(
183 imageUrl: currency.iconPath,
184 height: 40,
185 width: 40,
186 errorWidget: Container(
187 height: 30.0,
188 width: 30.0,
189 child: Center(
190 child: Text(
191 currency.title.substring(0, min(currency.title.length, 2)),
192 style: Theme.of(context).textTheme.bodySmall?.copyWith(
193 fontSize: 11,
194 color: Theme.of(context).colorScheme.onSurfaceVariant,
195 ),
196 ),
197 ),
198 decoration: BoxDecoration(
199 shape: BoxShape.circle,
200 color: Theme.of(context).colorScheme.surfaceContainer,
201 ),
202 ),
203 ),
204 const SizedBox(height: 10),
205 Text(
206 currency.title,
207 overflow: TextOverflow.ellipsis,
208 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
209 fontSize: 16,
210 fontWeight: FontWeight.w700,
211 color: Theme.of(context).colorScheme.onSurface,
212 height: 1,
213 ),
214 ),
215 ],
216 ),
217 ),
218 ],
219 ),
220 if (currency.isPotentialScam)
221 Container(
222 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
223 margin: const EdgeInsets.only(top: 4),
224 decoration: BoxDecoration(
225 color: Theme.of(context).colorScheme.errorContainer,
226 borderRadius: BorderRadius.circular(8),
227 ),
228 child: Row(
229 mainAxisSize: MainAxisSize.min,
230 children: [
231 Icon(
232 Icons.warning_amber_outlined,
233 size: 16,
234 color: Theme.of(context).colorScheme.onErrorContainer,
235 ),
236 const SizedBox(width: 4),
237 Text(
238 S.of(context).potential_scam,
239 style: Theme.of(context).textTheme.bodySmall?.copyWith(
240 color: Theme.of(context).colorScheme.onErrorContainer,
241 fontWeight: FontWeight.w700,
242 ),
243 ),
244 ],
245 ),
246 ),
247 if (frozenBalance.isNotEmpty)
248 Column(
249 crossAxisAlignment: CrossAxisAlignment.start,
250 children: [
251 const SizedBox(height: 26),
252 Row(
253 children: [
254 Text(
255 S.of(context).frozen_balance,
256 textAlign: TextAlign.center,
257 style: Theme.of(context).textTheme.bodySmall!.copyWith(
258 color: Theme.of(context).colorScheme.onSurfaceVariant,
259 height: 1,
260 ),
261 ),
262 ],
263 ),
264 const SizedBox(height: 8),
265 AutoSizeText(
266 frozenBalance,
267 style: Theme.of(context).textTheme.bodyLarge!.copyWith(
268 fontSize: 20,
269 color: Theme.of(context).colorScheme.primary,
270 height: 1,
271 ),
272 maxLines: 1,
273 textAlign: TextAlign.center,
274 ),
275 const SizedBox(height: 4),
276 if (!isTestnet)
277 Text(
278 frozenFiatBalance,
279 textAlign: TextAlign.center,
280 style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1),
281 ),
282 ],
283 ),
284 if (hasAdditionalBalance)
285 Column(
286 crossAxisAlignment: CrossAxisAlignment.start,
287 children: [
288 const SizedBox(height: 24),
289 Text(
290 '${additionalBalanceLabel}',
291 textAlign: TextAlign.center,
292 style: Theme.of(context).textTheme.bodySmall!.copyWith(
293 color: Theme.of(context).colorScheme.onSurfaceVariant,
294 height: 1,
295 ),
296 ),
297 const SizedBox(height: 8),
298 AutoSizeText(
299 additionalBalance,
300 style: Theme.of(context).textTheme.bodyLarge!.copyWith(
301 fontSize: 20,
302 color: Theme.of(context).colorScheme.secondary,
303 height: 1,
304 ),
305 maxLines: 1,
306 textAlign: TextAlign.center,
307 ),
308 const SizedBox(height: 4),
309 if (!isTestnet)
310 Text(
311 '${additionalFiatBalance}',
312 textAlign: TextAlign.center,
313 style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1),
314 ),
315 ],
316 ),
317 ],
318 ),
319 ),
320 ),
321 ),
322 if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[
323 SizedBox(height: 10),
324 Container(
325 margin: const EdgeInsets.only(left: 16, right: 16),
326 decoration: BoxDecoration(
327 borderRadius: BorderRadius.circular(15),
328 gradient: LinearGradient(
329 colors: [
330 context.customColors.cardGradientColorPrimary,
331 context.customColors.cardGradientColorSecondary,
332 ],
333 begin: Alignment.topCenter,
334 end: Alignment.bottomCenter,
335 ),
336 ),
337 child: TextButton(
338 onPressed: _showToast,
339 onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
340 style: TextButton.styleFrom(
341 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
342 ),
343 child: Column(
344 crossAxisAlignment: CrossAxisAlignment.start,
345 children: [
346 Container(
347 margin: const EdgeInsets.only(top: 10, left: 12, right: 8, bottom: 10),
348 child: Stack(
349 children: [
350 if (currency == CryptoCurrency.ltc)
351 Row(
352 mainAxisAlignment: MainAxisAlignment.end,
353 children: [
354 Column(
355 children: [
356 ImageIcon(
357 AssetImage('assets/images/mweb_logo.png'),
358 size: 40,
359 ),
360 const SizedBox(height: 10),
361 Text(
362 'MWEB',
363 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
364 fontSize: 16,
365 fontWeight: FontWeight.w700,
366 color: Theme.of(context).colorScheme.onSurface,
367 height: 1,
368 ),
369 ),
370 ],
371 ),
372 ],
373 ),
374 if (currency == CryptoCurrency.btc)
375 Row(
376 mainAxisAlignment: MainAxisAlignment.end,
377 children: [
378 Column(
379 children: [
380 SvgPicture.asset(
381 'assets/images/lightning-icon.svg',
382 width: 40,
383 height: 40,
384 ),
385 const SizedBox(height: 10),
386 Text(
387 'Lightning',
388 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
389 fontSize: 16,
390 fontWeight: FontWeight.w700,
391 color: Theme.of(context).colorScheme.onSurface,
392 height: 1,
393 ),
394 ),
395 ],
396 ),
397 ],
398 ),
399 if (hasSecondAvailableBalance)
400 Row(
401 children: [
402 Column(
403 crossAxisAlignment: CrossAxisAlignment.start,
404 children: [
405 GestureDetector(
406 behavior: HitTestBehavior.opaque,
407 onTap: onPressedHelp,
408 child: Row(
409 children: [
410 Text(
411 secondAvailableBalanceLabel,
412 textAlign: TextAlign.center,
413 style: Theme.of(context).textTheme.bodySmall?.copyWith(
414 color:
415 Theme.of(context).colorScheme.onSurfaceVariant,
416 height: 1,
417 ),
418 ),
419 Padding(
420 padding: const EdgeInsets.symmetric(horizontal: 4),
421 child: Icon(
422 Icons.help_outline,
423 size: 16,
424 color: Theme.of(context).colorScheme.onSurfaceVariant,
425 ),
426 )
427 ],
428 ),
429 ),
430 SizedBox(height: 8),
431 AutoSizeText(
432 secondAvailableBalance,
433 style: Theme.of(context).textTheme.bodyLarge?.copyWith(
434 fontSize: 24,
435 fontWeight: FontWeight.w800,
436 color: Theme.of(context).colorScheme.onSurface,
437 height: 1,
438 ),
439 maxLines: 1,
440 textAlign: TextAlign.center,
441 ),
442 SizedBox(height: 6),
443 if (!isTestnet)
444 Text(
445 secondAvailableFiatBalance,
446 textAlign: TextAlign.center,
447 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
448 fontSize: 16,
449 fontWeight: FontWeight.w500,
450 color: Theme.of(context).colorScheme.onSurfaceVariant,
451 height: 1,
452 ),
453 ),
454 ],
455 ),
456 ],
457 ),
458 ],
459 ),
460 ),
461 Container(
462 margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16),
463 child: Stack(
464 children: [
465 if (hasSecondAdditionalBalance)
466 Row(
467 children: [
468 Column(
469 crossAxisAlignment: CrossAxisAlignment.start,
470 children: [
471 SizedBox(height: 24),
472 Text(
473 secondAdditionalBalanceLabel,
474 textAlign: TextAlign.center,
475 style: Theme.of(context).textTheme.bodySmall?.copyWith(
476 color: Theme.of(context).colorScheme.onSurfaceVariant,
477 height: 1,
478 ),
479 ),
480 SizedBox(height: 8),
481 AutoSizeText(
482 secondAdditionalBalance,
483 style: Theme.of(context).textTheme.bodyLarge?.copyWith(
484 fontSize: 20,
485 color: Theme.of(context).colorScheme.secondary,
486 height: 1,
487 ),
488 maxLines: 1,
489 textAlign: TextAlign.center,
490 ),
491 SizedBox(height: 4),
492 if (!isTestnet)
493 Text(
494 '${secondAdditionalFiatBalance}',
495 textAlign: TextAlign.center,
496 style: Theme.of(context).textTheme.bodySmall?.copyWith(
497 height: 1,
498 ),
499 ),
500 ],
501 ),
502 ],
503 ),
504 ],
505 ),
506 ),
507 IntrinsicHeight(
508 child: Container(
509 padding: const EdgeInsets.symmetric(horizontal: 12),
510 child: Row(
511 mainAxisAlignment: MainAxisAlignment.spaceBetween,
512 children: [
513 Expanded(
514 child: Semantics(
515 label: depositToL2Label,
516 child: OutlinedButton(
517 onPressed: () => depositToL2(context),
518 style: OutlinedButton.styleFrom(
519 backgroundColor: Theme.of(context).colorScheme.primary,
520 side: BorderSide(
521 color:
522 Theme.of(context).colorScheme.outlineVariant.withAlpha(0),
523 width: 0,
524 ),
525 shape: RoundedRectangleBorder(
526 borderRadius: BorderRadius.circular(10),
527 ),
528 ),
529 child: Container(
530 padding: const EdgeInsets.symmetric(vertical: 12),
531 child: Row(
532 mainAxisAlignment: MainAxisAlignment.center,
533 children: [
534 Image.asset(
535 height: 30,
536 width: 30,
537 'assets/images/received.png',
538 color: Theme.of(context).colorScheme.onPrimary,
539 ),
540 const SizedBox(width: 8),
541 Text(
542 depositToL2Label,
543 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
544 color: Theme.of(context).colorScheme.onPrimary,
545 fontWeight: FontWeight.w700,
546 ),
547 ),
548 ],
549 ),
550 ),
551 ),
552 ),
553 ),
554 SizedBox(width: 16),
555 Expanded(
556 child: Semantics(
557 label: withdrawFromL2Label,
558 child: OutlinedButton(
559 onPressed: () => withdrawFromL2(context),
560 style: OutlinedButton.styleFrom(
561 backgroundColor: Theme.of(context).colorScheme.surface,
562 side: BorderSide(
563 color:
564 Theme.of(context).colorScheme.outlineVariant.withAlpha(0),
565 width: 0,
566 ),
567 shape: RoundedRectangleBorder(
568 borderRadius: BorderRadius.circular(10),
569 ),
570 ),
571 child: Container(
572 padding: EdgeInsets.symmetric(vertical: 12),
573 child: Row(
574 mainAxisAlignment: MainAxisAlignment.center,
575 children: [
576 Image.asset(
577 height: 30,
578 width: 30,
579 'assets/images/upload.png',
580 color: Theme.of(context).colorScheme.onSecondaryContainer,
581 ),
582 const SizedBox(width: 8),
583 Text(
584 withdrawFromL2Label,
585 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
586 color: Theme.of(context)
587 .colorScheme
588 .onSecondaryContainer,
589 fontWeight: FontWeight.w700,
590 ),
591 ),
592 ],
593 ),
594 ),
595 ),
596 ),
597 ),
598 ],
599 ),
600 ),
601 ),
602 const SizedBox(height: 16),
603 ],
604 ),
605 ),
606 ),
607 ],
608 ],
609 );
610 }
611
612 String get depositToL2Label => dashboardViewModel.type == WalletType.litecoin
613 ? S.current.litecoin_mweb_pegin
614 : S.current.bitcoin_lightning_deposit;
615
616 String get withdrawFromL2Label => dashboardViewModel.type == WalletType.litecoin
617 ? S.current.litecoin_mweb_pegout
618 : S.current.bitcoin_lightning_withdraw;
619
620 Future<void> depositToL2(BuildContext context) async {
621 PaymentRequest? paymentRequest = null;
622
623 if (dashboardViewModel.type == WalletType.litecoin) {
624 final depositAddress = bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet);
625 if ((depositAddress?.isNotEmpty ?? false)) {
626 paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress"));
627 }
628 } else if (dashboardViewModel.type == WalletType.bitcoin) {
629 final depositAddress = await bitcoin!.getUnusedSpakDepositAddress(dashboardViewModel.wallet);
630 if ((depositAddress?.isNotEmpty ?? false)) {
631 paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
632 }
633 }
634
635 Navigator.pushNamed(
636 context,
637 Routes.send,
638 arguments: {
639 'paymentRequest': paymentRequest,
640 'coinTypeToSpendFrom': UnspentCoinType.nonMweb,
641 },
642 );
643 }
644
645 Future<void> withdrawFromL2(BuildContext context) async {
646 PaymentRequest? paymentRequest = null;
647 UnspentCoinType unspentCoinType = UnspentCoinType.any;
648 final withdrawAddress = bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet);
649
650 if (dashboardViewModel.type == WalletType.litecoin) {
651 if ((withdrawAddress?.isNotEmpty ?? false)) {
652 paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress"));
653 }
654 unspentCoinType = UnspentCoinType.mweb;
655 } else if (dashboardViewModel.type == WalletType.bitcoin) {
656 if ((withdrawAddress?.isNotEmpty ?? false)) {
657 paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress"));
658 }
659 unspentCoinType = UnspentCoinType.lightning;
660 }
661
662 Navigator.pushNamed(
663 context,
664 Routes.send,
665 arguments: {
666 'paymentRequest': paymentRequest,
667 'coinTypeToSpendFrom': unspentCoinType,
668 },
669 );
670 }
671
672 void onPressedHelp() {
673 var helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin#lightning");
674 if (dashboardViewModel.type == WalletType.litecoin) {
675 helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/litecoin#mweb");
676 }
677
678 launchUrl(helpUri, mode: LaunchMode.externalApplication);
679 }
680
681 void _showBalanceDescription(BuildContext context, String content) {
682 showPopUp<void>(context: context, builder: (_) => InformationPage(information: content));
683 }
684
685 void _showToast() async {
686 try {
687 await Fluttertoast.showToast(
688 msg: S.current.show_balance_toast,
689 backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
690 );
691 } catch (_) {}
692 }
693 }