dev
dart 815 lines 34.7 KB
Raw
1 import 'dart:io';
2
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
5 import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
6 import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
7 import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dart';
8 import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
9 import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
10 import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
11 import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart';
12 import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
13 import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
14 import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
15 import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
16 import 'package:cake_wallet/core/execution_state.dart';
17 import 'package:cake_wallet/generated/i18n.dart';
18 import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
19 import 'package:cake_wallet/routes.dart';
20 import 'package:cake_wallet/src/screens/base_page.dart';
21 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
22 import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
23 import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart';
24 import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
25 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
26 import 'package:cake_wallet/src/widgets/primary_button.dart';
27 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
28 import 'package:cake_wallet/typography.dart';
29 import 'package:cake_wallet/utils/feature_flag.dart';
30 import 'package:cake_wallet/utils/show_pop_up.dart';
31 import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
32 import 'package:cake_wallet/view_model/send/output.dart';
33 import 'package:cake_wallet/view_model/send/send_view_model.dart';
34 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
35 import 'package:cw_core/unspent_coin_type.dart';
36 import 'package:cw_core/wallet_type.dart';
37 import 'package:flutter/material.dart';
38 import 'package:flutter_mobx/flutter_mobx.dart';
39 import 'package:mobx/mobx.dart';
40
41 class CakePayBuyCardPage extends BasePage {
42 CakePayBuyCardPage(this.cakePayBuyCardViewModel)
43 : _sendViewModel = cakePayBuyCardViewModel.sendViewModel,
44 _amountFieldFocus = FocusNode(),
45 _amountController = TextEditingController(),
46 _quantityFieldFocus = FocusNode(),
47 _quantityController =
48 TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
49 _amountController.addListener(() {
50 cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
51 });
52 }
53
54 final CakePayBuyCardViewModel cakePayBuyCardViewModel;
55 final SendViewModel _sendViewModel;
56
57 bool _effectsInstalled = false;
58 late final BuildContext _overlayCtx;
59
60 @override
61 String get title => cakePayBuyCardViewModel.card.name;
62
63 @override
64 bool get extendBodyBehindAppBar => true;
65
66 @override
67 bool get gradientAll => true;
68
69 @override
70 bool get resizeToAvoidBottomInset => false;
71
72 @override
73 AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
74
75 @override
76 Widget? trailing(BuildContext context) {
77 return const SizedBox(
78 width: 54,
79 height: 0,
80 );
81 }
82
83 @override
84 Widget? middle(BuildContext context) {
85 return Text(
86 title,
87 textAlign: TextAlign.center,
88 maxLines: 2,
89 style: TextStyle(
90 fontSize: 18.0,
91 fontWeight: FontWeight.bold,
92 fontFamily: 'Lato',
93 color: titleColor(context)),
94 );
95 }
96
97 final TextEditingController _amountController;
98 final FocusNode _amountFieldFocus;
99 final TextEditingController _quantityController;
100 final FocusNode _quantityFieldFocus;
101
102 @override
103 Widget body(BuildContext context) {
104 _setEffects(context);
105
106 final card = cakePayBuyCardViewModel.card;
107 final vendor = cakePayBuyCardViewModel.vendor;
108
109 return Container(
110 color: Colors.transparent,
111 child: KeyboardHideOverlay(
112 child: Column(
113 children: [
114 RoundedOverlayCards(
115 topCardChild: Column(
116 children: [
117 Expanded(flex: 4, child: const SizedBox()),
118 Expanded(
119 flex: 7,
120 child: Container(
121 decoration: BoxDecoration(
122 borderRadius: BorderRadius.circular(10),
123 boxShadow: [
124 BoxShadow(
125 color: Colors.black.withAlpha(150),
126 blurRadius: 8,
127 offset: const Offset(0, 2))
128 ],
129 ),
130 child: ClipRRect(
131 borderRadius: BorderRadius.circular(10),
132 child: Image.network(
133 card.cardImageUrl ?? '',
134 fit: BoxFit.cover,
135 loadingBuilder: (BuildContext context, Widget child,
136 ImageChunkEvent? loadingProgress) {
137 if (loadingProgress == null) return child;
138 return Center(child: CircularProgressIndicator());
139 },
140 errorBuilder: (context, error, stackTrace) =>
141 CakePayCardImagePlaceholder(),
142 ),
143 ),
144 ),
145 ),
146 Expanded(child: const SizedBox()),
147 ],
148 ),
149 bottomCardChild: Padding(
150 padding: const EdgeInsets.symmetric(horizontal: 24),
151 child: Column(children: [
152 if (card.prepaidRange.isNotEmpty)
153 PrepaidRangeAmountWidget(
154 fiatCurrency: card.fiatCurrency.title,
155 prepaidRanges: card.prepaidRange,
156 amountFieldFocus: _amountFieldFocus,
157 amountController: _amountController,
158 cakePayBuyCardViewModel: cakePayBuyCardViewModel,
159 onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
160 )
161 else if (card.denominationItems.isNotEmpty)
162 DenominationsAmountWidget(
163 fiatCurrency: card.fiatCurrency.title,
164 denominations: card.denominationItems,
165 amountFieldFocus: _amountFieldFocus,
166 amountController: _amountController,
167 quantityFieldFocus: _quantityFieldFocus,
168 quantityController: _quantityController,
169 onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
170 onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
171 cakePayBuyCardViewModel: cakePayBuyCardViewModel,
172 )
173 else
174 EnterAmountWidget(
175 minValue: card.minValue ?? '-',
176 maxValue: card.maxValue ?? '-',
177 fiatCurrency: card.fiatCurrency.title,
178 amountFieldFocus: _amountFieldFocus,
179 amountController: _amountController,
180 onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
181 ),
182 ]))),
183 Expanded(
184 flex: 2,
185 child: Padding(
186 padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
187 child: Column(
188 crossAxisAlignment: CrossAxisAlignment.start,
189 children: [
190 if (vendor.cakeWarnings != null)
191 Padding(
192 padding: const EdgeInsets.symmetric(vertical: 4),
193 child: Center(
194 child: Container(
195 decoration: BoxDecoration(
196 color: Theme.of(context).primaryColor,
197 borderRadius: BorderRadius.circular(10),
198 border: Border.all(color: Colors.white.withAlpha(50)),
199 ),
200 child: Padding(
201 padding: const EdgeInsets.all(8.0),
202 child: Text(
203 vendor.cakeWarnings!,
204 textAlign: TextAlign.center,
205 style: textSmallSemiBold(color: Colors.white),
206 ),
207 ),
208 ),
209 ),
210 ),
211 Expanded(
212 child: SingleChildScrollView(
213 primary: false,
214 padding: EdgeInsets.zero,
215 child: ClickableLinksText(
216 text: card.description ?? '',
217 textStyle: TextStyle(
218 color: Theme.of(context).textTheme.titleLarge!.color!,
219 fontSize: 14,
220 fontWeight: FontWeight.w400,
221 ),
222 ),
223 ),
224 ),
225 ],
226 ),
227 ),
228 ),
229 Expanded(
230 child: SingleChildScrollView(
231 primary: false,
232 child: Padding(
233 padding: const EdgeInsets.only(left: 24, right: 24),
234 child: Column(
235 children: [
236 if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
237 Row(
238 children: [
239 Text(S.of(context).expiry_and_validity + ':',
240 style: TextStyle(
241 color: Theme.of(context).textTheme.titleLarge!.color!,
242 fontSize: 16,
243 fontWeight: FontWeight.w900)),
244 Expanded(
245 child: Text(card.expiryAndValidity!,
246 textAlign: TextAlign.center,
247 style: Theme.of(context).textTheme.labelMedium)),
248 ],
249 ),
250 SizedBox(height: 8),
251 TextIconButton(
252 label: S.of(context).how_to_use_card,
253 onTap: () => _showHowToUseCard(context, card)),
254 SizedBox(height: 8),
255 TextIconButton(
256 label: S.of(context).settings_terms_and_conditions,
257 onTap: () => _showTermsAndCondition(context, card.termsAndConditions)),
258 ],
259 ),
260 ),
261 ),
262 ),
263 SizedBox(height: 8),
264 Observer(builder: (_) {
265 Widget _buildPaymentMethodWidget(
266 List<CakePayPaymentMethod> methods, CakePayPaymentMethod selected) {
267 return Row(
268 children: [
269 Padding(
270 padding: const EdgeInsets.only(left: 24, right: 8),
271 child: Text(
272 'Payment Method',
273 style: TextStyle(
274 color: Theme.of(context).textTheme.titleLarge!.color!,
275 fontSize: 16,
276 fontWeight: FontWeight.w900,
277 ),
278 ),
279 ),
280 Expanded(child: const SizedBox()),
281 if (methods.length > 1)
282 Padding(
283 padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
284 child: ToggleButtons(
285 isSelected: methods.map((m) => m == selected).toList(),
286 borderRadius: BorderRadius.circular(8),
287 onPressed: (index) =>
288 cakePayBuyCardViewModel.chooseMethod(methods[index]),
289 children: methods
290 .map((m) => Padding(
291 padding:
292 const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
293 child: Text(m.label),
294 ))
295 .toList(),
296 ),
297 ),
298 ],
299 );
300 }
301
302 final methods = cakePayBuyCardViewModel.availableMethods;
303 final selected = cakePayBuyCardViewModel.selectedPaymentMethod ??
304 (methods.isNotEmpty ? methods.first : null);
305
306 return Column(
307 children: [
308 methods.length <= 1 || selected == null
309 ? const SizedBox.shrink()
310 : _buildPaymentMethodWidget(methods, selected),
311 if (_sendViewModel.walletType == WalletType.litecoin &&
312 _sendViewModel.isMwebEnabled)
313 Observer(
314 builder: (_) => Padding(
315 padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
316 child: GestureDetector(
317 key: ValueKey('cake_pay_buy_page_unspent_coin_button_key'),
318 onTap: () {
319 bool value = _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any;
320 _sendViewModel.setAllowMwebCoins(!value);
321 },
322 child: Container(
323 color: Colors.transparent,
324 child: Row(
325 mainAxisAlignment: MainAxisAlignment.spaceBetween,
326 children: [
327 StandardCheckbox(
328 caption: S.of(context).litecoin_mweb_allow_coins,
329 captionColor: Theme.of(context).colorScheme.onSurfaceVariant,
330 borderColor: Theme.of(context).colorScheme.primary,
331 iconColor: Theme.of(context).colorScheme.primary,
332 value: _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any,
333 onChanged: (bool? value) {
334 _sendViewModel.setAllowMwebCoins(value ?? false);
335 },
336 ),
337 ],
338 ),
339 ),
340 ),
341 ),
342 ),
343 if (FeatureFlag.hasDevOptions && FeatureFlag.isCakePayPurchaseSimulationEnabled)
344 Padding(
345 padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
346 child: LoadingPrimaryButton(
347 onPressed: () {
348 //Request dummy node to get the focus out of the text fields
349 FocusScope.of(context).requestFocus(FocusNode());
350
351 cakePayBuyCardViewModel.isSimulatingFlow = true;
352 isIOSUnavailable(card)
353 ? alertIOSAvailability(context, card)
354 : confirmPurchaseFirst(context);
355 },
356 text: '(Dev) Simulate Purchasing Gift Card',
357 isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
358 cakePayBuyCardViewModel.isPurchasing,
359 isLoading: _sendViewModel.state is IsExecutingState ||
360 cakePayBuyCardViewModel.isPurchasing,
361 color: Theme.of(context).colorScheme.primary,
362 textColor: Theme.of(context).colorScheme.onPrimary,
363 ),
364 ),
365 Padding(
366 padding: EdgeInsets.only(top: 10, bottom: 34, right: 20, left: 20),
367 child: LoadingPrimaryButton(
368 onPressed: () {
369 //Request dummy node to get the focus out of the text fields
370 FocusScope.of(context).requestFocus(FocusNode());
371
372 isIOSUnavailable(card)
373 ? alertIOSAvailability(context, card)
374 : confirmPurchaseFirst(context);
375 },
376 text: S.of(context).purchase_gift_card,
377 isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
378 cakePayBuyCardViewModel.isPurchasing ||
379 _sendViewModel.state is ExecutedSuccessfullyState,
380 isLoading: _sendViewModel.state is IsExecutingState ||
381 cakePayBuyCardViewModel.isPurchasing,
382 color: Theme.of(context).colorScheme.primary,
383 textColor: Theme.of(context).colorScheme.onPrimary,
384 ),
385 ),
386 ],
387 );
388 }),
389 ],
390 ),
391 ),
392 );
393 }
394
395 bool isWordInCardsName(CakePayCard card, String word) {
396 return card.name.toLowerCase().contains(word.toLowerCase());
397 }
398
399 bool isIOSUnavailable(CakePayCard card) {
400 if (!Platform.isIOS && !Platform.isMacOS) {
401 return false;
402 }
403
404 final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
405 isWordInCardsName(card, 'xbox') ||
406 isWordInCardsName(card, 'steam') ||
407 isWordInCardsName(card, 'meta quest') ||
408 isWordInCardsName(card, 'kigso') ||
409 isWordInCardsName(card, 'game world') ||
410 isWordInCardsName(card, 'google') ||
411 isWordInCardsName(card, 'nintendo');
412 final isGCodes = isWordInCardsName(card, 'gcodes');
413 final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
414 final isTidal = isWordInCardsName(card, 'tidal');
415 final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
416 isWordInCardsName(card, 'expressvpn') ||
417 isWordInCardsName(card, 'surfshark') ||
418 isWordInCardsName(card, 'proton');
419 final isStreamingServices = isWordInCardsName(card, 'netflix') ||
420 isWordInCardsName(card, 'spotify') ||
421 isWordInCardsName(card, 'hulu') ||
422 isWordInCardsName(card, 'hbo') ||
423 isWordInCardsName(card, 'soundcloud') ||
424 isWordInCardsName(card, 'twitch');
425 final isDatingServices = isWordInCardsName(card, 'tinder');
426
427 return isDigitalGameStores ||
428 isGCodes ||
429 isApple ||
430 isTidal ||
431 isVPNServices ||
432 isStreamingServices ||
433 isDatingServices;
434 }
435
436 Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
437 cakePayBuyCardViewModel.isSimulatingFlow = false;
438 return await showPopUp<void>(
439 context: context,
440 builder: (BuildContext context) {
441 return AlertWithOneAction(
442 alertTitle: S.of(context).error,
443 alertContent: S.of(context).cakepay_ios_not_available,
444 buttonText: S.of(context).ok,
445 buttonAction: () {
446 // _walletHardwareRestoreVM.error = null;
447 Navigator.of(context).pop();
448 });
449 });
450 }
451
452 Future<void> confirmPurchaseFirst(BuildContext context) async {
453 bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
454 if (!isLogged) {
455 cakePayBuyCardViewModel.isSimulatingFlow = false;
456 Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
457 } else {
458 cakePayBuyCardViewModel.isPurchasing = true;
459 await _showconfirmPurchaseFirstAlert(context);
460 }
461 }
462
463 Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
464 if (!cakePayBuyCardViewModel.confirmsNoVpn ||
465 !cakePayBuyCardViewModel.confirmsVoidedRefund ||
466 !cakePayBuyCardViewModel.confirmsTermsAgreed) {
467 await showPopUp<void>(
468 context: context,
469 builder: (BuildContext context) => ThreeCheckboxAlert(
470 alertTitle: S.of(context).cakepay_confirm_purchase,
471 leftButtonText: S.of(context).cancel,
472 rightButtonText: S.of(context).confirm,
473 actionLeftButton: () {
474 cakePayBuyCardViewModel.isPurchasing = false;
475 cakePayBuyCardViewModel.isSimulatingFlow = false;
476 Navigator.of(context).pop();
477 },
478 actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
479 cakePayBuyCardViewModel.confirmsNoVpn = confirmsNoVpn;
480 cakePayBuyCardViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
481 cakePayBuyCardViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
482
483 Navigator.of(context).pop();
484 },
485 ),
486 );
487 }
488
489 if (cakePayBuyCardViewModel.confirmsNoVpn &&
490 cakePayBuyCardViewModel.confirmsVoidedRefund &&
491 cakePayBuyCardViewModel.confirmsTermsAgreed) {
492 await purchaseCard(context);
493 } else {
494 cakePayBuyCardViewModel.isPurchasing = false;
495 cakePayBuyCardViewModel.isSimulatingFlow = false;
496 }
497 }
498
499 Future<void> purchaseCard(BuildContext context) async {
500 bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
501 if (!isLogged) {
502 cakePayBuyCardViewModel.isSimulatingFlow = false;
503 Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
504 } else {
505 try {
506 await cakePayBuyCardViewModel.createOrder();
507 } catch (_) {
508 cakePayBuyCardViewModel.isSimulatingFlow = false;
509 await cakePayBuyCardViewModel.logout();
510 }
511 }
512 cakePayBuyCardViewModel.isPurchasing = false;
513 }
514
515 BuildContext? dialogContext;
516 BuildContext? loadingBottomSheetContext;
517 BuildContext? confirmBottomSheetContext;
518
519 void _setEffects(BuildContext context) {
520 if (_effectsInstalled) {
521 return;
522 }
523
524 _overlayCtx = Navigator.of(context).context;
525
526 if (_sendViewModel.isElectrumWallet) {
527 bitcoin!.updateFeeRates(_sendViewModel.wallet);
528 }
529
530 reaction((_) => _sendViewModel.state, (ExecutionState state) async {
531 if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop();
532
533 if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) {
534 Navigator.of(confirmBottomSheetContext!).pop();
535 }
536
537 if (state is! IsExecutingState &&
538 loadingBottomSheetContext != null &&
539 loadingBottomSheetContext!.mounted) {
540 Navigator.of(loadingBottomSheetContext!).pop();
541 }
542
543 if (state is FailureState) {
544 WidgetsBinding.instance.addPostFrameCallback((_) {
545 if (context.mounted)
546 showPopUp<void>(
547 context: context,
548 builder: (BuildContext context) {
549 return AlertWithOneAction(
550 key: ValueKey('cake_pay_buy_page_send_failure_dialog_key'),
551 buttonKey: ValueKey('cake_pay_buy_page_send_failure_dialog_button_key'),
552 alertTitle: S.of(context).error,
553 alertContent: state.error,
554 buttonText: S.of(context).ok,
555 buttonAction: () => Navigator.of(context).pop());
556 });
557 });
558 }
559
560 if (state is IsExecutingState) {
561 // wait a bit to avoid showing the loading dialog if transaction is failed
562 await Future.delayed(const Duration(milliseconds: 300));
563 final currentState = _sendViewModel.state;
564 if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
565 return;
566 }
567
568 WidgetsBinding.instance.addPostFrameCallback((_) {
569 if (context.mounted) {
570 showModalBottomSheet<void>(
571 context: context,
572 isDismissible: false,
573 builder: (BuildContext context) {
574 loadingBottomSheetContext = context;
575 return LoadingBottomSheet(
576 titleText: S.of(context).generating_transaction,
577 );
578 },
579 );
580 }
581 });
582 }
583
584 if (state is ExecutedSuccessfullyState) {
585 if (cakePayBuyCardViewModel.order == null) return;
586
587 ReactionDisposer? disposer;
588
589 disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) {
590 if (isExpired) {
591 _sendViewModel.state = FailureState('Order expired');
592 disposer?.call();
593 }
594 });
595
596 WidgetsBinding.instance.addPostFrameCallback((_) async {
597 if (context.mounted) {
598 final order = cakePayBuyCardViewModel.order;
599
600 final displayingOutputs = _sendViewModel.outputs
601 .map((o) => o.OutputCopyWithParsedAddress(
602 parsedAddress: ParsedAddress(
603 parsedAddressByCurrencyMap: {
604 cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency: o.address,
605 },
606 handle: 'Cake Pay',
607 profileName: order?.cards.first.cardName ?? 'Cake Pay',
608 profileImageUrl: order?.cards.first.cardImagePath ?? '',
609 ),
610 fiatAmount: '${order?.totalReceiveAmount}',
611 ))
612 .toList();
613
614 final result = await showModalBottomSheet<bool>(
615 context: context,
616 isDismissible: false,
617 isScrollControlled: true,
618 builder: (BuildContext bottomSheetContext) {
619 confirmBottomSheetContext = bottomSheetContext;
620 return ConfirmSendingBottomSheet(
621 key: ValueKey('cake_pay_buy_page_confirm_sending_dialog_key'),
622 titleText: S.of(bottomSheetContext).confirm_transaction,
623 cakePayBuyCardViewModel: cakePayBuyCardViewModel,
624 paymentId: S.of(bottomSheetContext).payment_id,
625 paymentIdValue: cakePayBuyCardViewModel.order?.orderId,
626 expirationTime: cakePayBuyCardViewModel.formattedRemainingTime,
627 walletType: _sendViewModel.walletType,
628 titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath,
629 currency: _sendViewModel.selectedCryptoCurrency,
630 amount: S.of(bottomSheetContext).send_amount,
631 amountValue: _sendViewModel.amountParsingProxy
632 .asDisplayStringWithSymbol(_sendViewModel.pendingTransaction!.amount),
633 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
634 explanation: _sendViewModel.pendingTransactionAdditionalCostNotice,
635 fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
636 fee: S.of(bottomSheetContext).send_fee,
637 feeValue: _sendViewModel.amountParsingProxy
638 .asDisplayStringWithSymbol(_sendViewModel.pendingTransaction!.fee),
639 feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
640 outputs: displayingOutputs,
641 footerType: FooterType.slideActionButton,
642 isSlideActionEnabled: _sendViewModel.isReadyForSend,
643 slideActionButtonText:
644 cakePayBuyCardViewModel.isSimulating ? 'Swipe to simulate' : 'Swipe to send',
645 accessibleNavigationModeSlideActionButtonText:
646 cakePayBuyCardViewModel.isSimulating ? 'Simulate' : S.of(context).send,
647 onSlideActionComplete: () async {
648 Navigator.of(bottomSheetContext).pop(true);
649 cakePayBuyCardViewModel.isSimulating
650 ? cakePayBuyCardViewModel.simulatePayment()
651 : _sendViewModel.commitTransaction(context);
652 },
653 change: _sendViewModel.pendingTransaction!.change,
654 isOpenCryptoPay: _sendViewModel.ocpRequest != null,
655 amountParsingProxy: _sendViewModel.amountParsingProxy,
656 );
657 },
658 );
659
660 confirmBottomSheetContext = null;
661 cakePayBuyCardViewModel.isSimulatingFlow = false;
662 _handleDispose(disposer);
663 if (result == null) _sendViewModel.dismissTransaction();
664 }
665 });
666 }
667
668 if (state is TransactionCommitted) {
669 final order = cakePayBuyCardViewModel.order;
670 final outputsCopy = List<Output>.from(_sendViewModel.outputs);
671
672 final displayingOutputs = outputsCopy
673 .map((o) => o.OutputCopyWithParsedAddress(
674 parsedAddress: ParsedAddress(
675 parsedAddressByCurrencyMap: {
676 cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency: o.address,
677 },
678 handle: 'Cake Pay',
679 profileName: order?.cards.first.cardName ?? 'Cake Pay',
680 profileImageUrl: order?.cards.first.cardImagePath ?? '',
681 ),
682 fiatAmount: '${order?.totalReceiveAmount}',
683 ))
684 .toList();
685
686 _sendViewModel.clearOutputs();
687
688 final bool usePageContextLater = context.mounted;
689
690 WidgetsBinding.instance.addPostFrameCallback((_) async {
691 if (displayingOutputs.isEmpty) {
692 if (context.mounted) Navigator.of(context).pop();
693 return;
694 }
695
696 final BuildContext sheetParentCtx = usePageContextLater ? context : _overlayCtx;
697
698 if (!sheetParentCtx.mounted) {
699 return;
700 }
701
702 await showModalBottomSheet<void>(
703 context: sheetParentCtx,
704 useRootNavigator: !usePageContextLater,
705 isScrollControlled: true,
706 isDismissible: true,
707 backgroundColor: Colors.transparent,
708 builder: (sheetCtx) {
709 return CakePayTransactionSentBottomSheet(
710 key: const ValueKey('cake_pay_buy_page_transaction_sent_bottom_sheet_key'),
711 titleText: S.of(sheetCtx).transaction_sent,
712 titleIconWidget: const CircleAvatar(
713 radius: 10,
714 backgroundColor: Colors.green,
715 child: Icon(Icons.check, size: 16, color: Colors.white),
716 ),
717 output: displayingOutputs.first,
718 amount: S.of(sheetCtx).send_amount,
719 amountValue: _sendViewModel.pendingTransaction!.amount.toStringWithSymbol(),
720 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
721 fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
722 fee: S.of(sheetCtx).send_fee,
723 feeValue: _sendViewModel.pendingTransaction!.fee.toStringWithSymbol(),
724 feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
725 paymentId: 'Order ID',
726 paymentIdValue: order?.orderId ?? '',
727 onClose: () {
728 Navigator.of(sheetCtx).pop();
729 },
730 );
731 },
732 );
733 if (context.mounted) {
734 Navigator.of(context).pop();
735 }
736 });
737 }
738
739 if (state is IsAwaitingDeviceResponseState) {
740 WidgetsBinding.instance.addPostFrameCallback((_) {
741 if (!context.mounted) return;
742
743 showModalBottomSheet<void>(
744 context: context,
745 isDismissible: false,
746 builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
747 footerType: FooterType.singleActionButton,
748 titleText: S.of(bottomSheetContext).proceed_on_device,
749 contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
750 contentImageColor: Theme.of(context).textTheme.titleLarge!.color!,
751 content: S.of(bottomSheetContext).proceed_on_device_description,
752 singleActionButtonText: S.of(context).cancel,
753 onSingleActionButtonPressed: () {
754 _sendViewModel.state = InitialExecutionState();
755 Navigator.of(bottomSheetContext).pop();
756 },
757 ),
758 );
759 });
760 }
761 });
762
763 _effectsInstalled = true;
764 }
765
766 void _handleDispose(ReactionDisposer? disposer) {
767 cakePayBuyCardViewModel.dispose();
768 if (disposer != null) {
769 disposer();
770 }
771 }
772 }
773
774 void _showHowToUseCard(BuildContext context, CakePayCard card) {
775 showPopUp<void>(
776 context: context,
777 builder: (BuildContext context) {
778 return CakePayAlertModal(
779 title: S.of(context).how_to_use_card,
780 dismissible: true,
781 content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
782 Padding(
783 padding: EdgeInsets.all(10),
784 child: Text(card.name, style: Theme.of(context).textTheme.headlineSmall)),
785 ClickableLinksText(
786 text: card.howToUse ?? '',
787 textStyle: Theme.of(context).textTheme.bodyMedium!,
788 linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
789 color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))
790 ]),
791 actionTitle: S.current.got_it,
792 showCloseButton: false,
793 );
794 });
795 }
796
797 void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
798 showPopUp<void>(
799 context: context,
800 builder: (BuildContext context) {
801 return CakePayAlertModal(
802 title: S.of(context).settings_terms_and_conditions,
803 dismissible: true,
804 content: Align(
805 alignment: Alignment.bottomLeft,
806 child: ClickableLinksText(
807 text: termsAndConditions ?? '',
808 textStyle: Theme.of(context).textTheme.bodyMedium!,
809 linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
810 color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))),
811 actionTitle: S.of(context).agree,
812 showCloseButton: false,
813 );
814 });
815 }