CW-1132-Cake-Pay-flow-does-not-use-MWEB-funds (#2434)

* add MWEB coin selection for Cake Pay flow * add MWEB checks --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Serhii committed Aug 12, 2025 at 16:06 UTC d24cb84e16bbec3d329524e6b4ff0cbd9086c7d3
6 files changed +92 -55
lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+70 -43
@@ -3,7 +3,6 @@ import 'dart:io';
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/services/cake_pay_service.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';
@@ -24,23 +23,26 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_shee
23 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
24 import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
25 import 'package:cake_wallet/src/widgets/primary_button.dart';
26 +import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
27 import 'package:cake_wallet/typography.dart';
28 import 'package:cake_wallet/utils/feature_flag.dart';
29 import 'package:cake_wallet/utils/show_pop_up.dart';
30 import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
31 import 'package:cake_wallet/view_model/send/output.dart';
32 +import 'package:cake_wallet/view_model/send/send_view_model.dart';
33 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
34 +import 'package:cw_core/unspent_coin_type.dart';
35 +import 'package:cw_core/wallet_type.dart';
36 import 'package:flutter/material.dart';
34 -import 'package:flutter/services.dart';
37 import 'package:flutter_mobx/flutter_mobx.dart';
38 import 'package:keyboard_actions/keyboard_actions.dart';
39 import 'package:mobx/mobx.dart';
40
41 class CakePayBuyCardPage extends BasePage {
42 CakePayBuyCardPage(
41 - this.cakePayBuyCardViewModel,
42 - this.cakePayService,
43 - ) : _amountFieldFocus = FocusNode(),
43 + this.cakePayBuyCardViewModel
44 + ) : _sendViewModel = cakePayBuyCardViewModel.sendViewModel,
45 + _amountFieldFocus = FocusNode(),
46 _amountController = TextEditingController(),
47 _quantityFieldFocus = FocusNode(),
48 _quantityController =
@@ -51,7 +53,7 @@ class CakePayBuyCardPage extends BasePage {
53 }
54
55 final CakePayBuyCardViewModel cakePayBuyCardViewModel;
54 - final CakePayService cakePayService;
56 + final SendViewModel _sendViewModel;
57
58 bool _effectsInstalled = false;
59 late final BuildContext _overlayCtx;
@@ -301,6 +303,38 @@ class CakePayBuyCardPage extends BasePage {
303 methods.length <= 1 || selected == null
304 ? const SizedBox.shrink()
305 : _buildPaymentMethodWidget(methods, selected),
306 + if (_sendViewModel.walletType == WalletType.litecoin && _sendViewModel.isMwebEnabled)
307 + Observer(
308 + builder: (_) => Padding(
309 + padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
310 + child: GestureDetector(
311 + key: ValueKey('cake_pay_buy_page_unspent_coin_button_key'),
312 + onTap: () {
313 + bool value = _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any;
314 + _sendViewModel.setAllowMwebCoins(!value);
315 + },
316 + child: Container(
317 + color: Colors.transparent,
318 + child: Row(
319 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
320 + children: [
321 + StandardCheckbox(
322 + caption: S.of(context).litecoin_mweb_allow_coins,
323 + captionColor: Theme.of(context).colorScheme.onSurfaceVariant,
324 + borderColor: Theme.of(context).colorScheme.primary,
325 + iconColor: Theme.of(context).colorScheme.primary,
326 + value:
327 + _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any,
328 + onChanged: (bool? value) {
329 + _sendViewModel.setAllowMwebCoins(value ?? false);
330 + },
331 + ),
332 + ],
333 + ),
334 + ),
335 + ),
336 + ),
337 + ),
338 if (FeatureFlag.hasDevOptions)
339 Padding(
340 padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
@@ -318,7 +352,7 @@ class CakePayBuyCardPage extends BasePage {
352 isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
353 cakePayBuyCardViewModel.isPurchasing,
354 isLoading:
321 - cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
355 + _sendViewModel.state is IsExecutingState ||
356 cakePayBuyCardViewModel.isPurchasing,
357 color: Theme.of(context).colorScheme.primary,
358 textColor: Theme.of(context).colorScheme.onPrimary,
@@ -338,7 +372,7 @@ class CakePayBuyCardPage extends BasePage {
372 text: S.of(context).purchase_gift_card,
373 isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
374 cakePayBuyCardViewModel.isPurchasing,
341 - isLoading: cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
375 + isLoading: _sendViewModel.state is IsExecutingState ||
376 cakePayBuyCardViewModel.isPurchasing,
377 color: Theme.of(context).colorScheme.primary,
378 textColor: Theme.of(context).colorScheme.onPrimary,
@@ -411,7 +445,7 @@ class CakePayBuyCardPage extends BasePage {
445 }
446
447 Future<void> confirmPurchaseFirst(BuildContext context) async {
414 - bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
448 + bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
449 if (!isLogged) {
450 cakePayBuyCardViewModel.isSimulatingFlow = false;
451 Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
@@ -458,7 +492,7 @@ class CakePayBuyCardPage extends BasePage {
492 }
493
494 Future<void> purchaseCard(BuildContext context) async {
461 - bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
495 + bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
496 if (!isLogged) {
497 cakePayBuyCardViewModel.isSimulatingFlow = false;
498 Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
@@ -467,7 +501,7 @@ class CakePayBuyCardPage extends BasePage {
501 await cakePayBuyCardViewModel.createOrder();
502 } catch (_) {
503 cakePayBuyCardViewModel.isSimulatingFlow = false;
470 - await cakePayBuyCardViewModel.cakePayService.logout();
504 + await cakePayBuyCardViewModel.logout();
505 }
506 }
507 cakePayBuyCardViewModel.isPurchasing = false;
@@ -484,11 +518,11 @@ class CakePayBuyCardPage extends BasePage {
518
519 _overlayCtx = Navigator.of(context).context;
520
487 - if (cakePayBuyCardViewModel.sendViewModel.isElectrumWallet) {
488 - bitcoin!.updateFeeRates(cakePayBuyCardViewModel.sendViewModel.wallet);
521 + if (_sendViewModel.isElectrumWallet) {
522 + bitcoin!.updateFeeRates(_sendViewModel.wallet);
523 }
524
491 - reaction((_) => cakePayBuyCardViewModel.sendViewModel.state, (ExecutionState state) async {
525 + reaction((_) => _sendViewModel.state, (ExecutionState state) async {
526 if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop();
527
528 if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) {
@@ -521,7 +555,7 @@ class CakePayBuyCardPage extends BasePage {
555 if (state is IsExecutingState) {
556 // wait a bit to avoid showing the loading dialog if transaction is failed
557 await Future.delayed(const Duration(milliseconds: 300));
524 - final currentState = cakePayBuyCardViewModel.sendViewModel.state;
558 + final currentState = _sendViewModel.state;
559 if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
560 return;
561 }
@@ -549,7 +583,7 @@ class CakePayBuyCardPage extends BasePage {
583
584 disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) {
585 if (isExpired) {
552 - cakePayBuyCardViewModel.sendViewModel.state = FailureState('Order expired');
586 + _sendViewModel.state = FailureState('Order expired');
587 disposer?.call();
588 }
589 });
@@ -558,7 +592,7 @@ class CakePayBuyCardPage extends BasePage {
592 if (context.mounted) {
593 final order = cakePayBuyCardViewModel.order;
594
561 - final displayingOutputs = cakePayBuyCardViewModel.sendViewModel.outputs
595 + final displayingOutputs = _sendViewModel.outputs
596 .map((o) => o.OutputCopyWithParsedAddress(
597 parsedAddress: ParsedAddress(
598 addresses: [o.address],
@@ -584,20 +618,16 @@ class CakePayBuyCardPage extends BasePage {
618 paymentId: S.of(bottomSheetContext).payment_id,
619 paymentIdValue: cakePayBuyCardViewModel.order?.orderId,
620 expirationTime: cakePayBuyCardViewModel.formattedRemainingTime,
587 - walletType: cakePayBuyCardViewModel.sendViewModel.walletType,
588 - titleIconPath:
589 - cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
590 - currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
621 + walletType: _sendViewModel.walletType,
622 + titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath,
623 + currency: _sendViewModel.selectedCryptoCurrency,
624 amount: S.of(bottomSheetContext).send_amount,
592 - amountValue:
593 - cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
625 + amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
626 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
595 - fiatAmountValue:
596 - cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
627 + fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
628 fee: S.of(bottomSheetContext).send_fee,
598 - feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
599 - feeFiatAmount: cakePayBuyCardViewModel
600 - .sendViewModel.pendingTransactionFeeFiatAmountFormatted,
629 + feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
630 + feeFiatAmount:_sendViewModel.pendingTransactionFeeFiatAmountFormatted,
631 outputs: displayingOutputs,
632 footerType: FooterType.slideActionButton,
633 slideActionButtonText:
@@ -608,10 +638,10 @@ class CakePayBuyCardPage extends BasePage {
638 Navigator.of(bottomSheetContext).pop(true);
639 cakePayBuyCardViewModel.isSimulating
640 ? cakePayBuyCardViewModel.simulatePayment()
611 - : cakePayBuyCardViewModel.sendViewModel.commitTransaction(context);
641 + : _sendViewModel.commitTransaction(context);
642 },
613 - change: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.change,
614 - isOpenCryptoPay: cakePayBuyCardViewModel.sendViewModel.ocpRequest != null,
643 + change: _sendViewModel.pendingTransaction!.change,
644 + isOpenCryptoPay: _sendViewModel.ocpRequest != null,
645 );
646 },
647 );
@@ -619,14 +649,14 @@ class CakePayBuyCardPage extends BasePage {
649 confirmBottomSheetContext = null;
650 cakePayBuyCardViewModel.isSimulatingFlow = false;
651 _handleDispose(disposer);
622 - if (result == null) cakePayBuyCardViewModel.sendViewModel.dismissTransaction();
652 + if (result == null) _sendViewModel.dismissTransaction();
653 }
654 });
655 }
656
657 if (state is TransactionCommitted) {
658 final order = cakePayBuyCardViewModel.order;
629 - final outputsCopy = List<Output>.from(cakePayBuyCardViewModel.sendViewModel.outputs);
659 + final outputsCopy = List<Output>.from(_sendViewModel.outputs);
660
661 final displayingOutputs = outputsCopy
662 .map((o) => o.OutputCopyWithParsedAddress(
@@ -640,7 +670,7 @@ class CakePayBuyCardPage extends BasePage {
670 ))
671 .toList();
672
643 - cakePayBuyCardViewModel.sendViewModel.clearOutputs();
673 + _sendViewModel.clearOutputs();
674
675 final bool usePageContextLater = context.mounted;
676
@@ -668,17 +698,14 @@ class CakePayBuyCardPage extends BasePage {
698 child: Icon(Icons.check, size: 16, color: Colors.white),
699 ),
700 output: displayingOutputs.first,
671 - currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
701 + currency: _sendViewModel.selectedCryptoCurrency,
702 amount: S.of(sheetCtx).send_amount,
673 - amountValue:
674 - cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
703 + amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
704 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
676 - fiatAmountValue:
677 - cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
705 + fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
706 fee: S.of(sheetCtx).send_fee,
679 - feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
680 - feeFiatAmount:
681 - cakePayBuyCardViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
707 + feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
708 + feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
709 paymentId: 'Order ID',
710 paymentIdValue: order?.orderId ?? '',
711 onClose: () {
@@ -709,7 +736,7 @@ class CakePayBuyCardPage extends BasePage {
736 content: S.of(bottomSheetContext).proceed_on_device_description,
737 singleActionButtonText: S.of(context).cancel,
738 onSingleActionButtonPressed: () {
712 - cakePayBuyCardViewModel.sendViewModel.state = InitialExecutionState();
739 + _sendViewModel.state = InitialExecutionState();
740 Navigator.of(bottomSheetContext).pop();
741 },
742 ),
lib/cake_pay/src/models/cake_pay_order.dart
+2 -2
@@ -3,10 +3,10 @@ enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB }
3 extension CakePayPaymentMethodLabel on CakePayPaymentMethod {
4 String get label => switch (this) {
5 CakePayPaymentMethod.BTC => 'Bitcoin',
6 - CakePayPaymentMethod.BTC_LN => 'Bitcoin Lightning',
6 + CakePayPaymentMethod.BTC_LN => 'Lightning',
7 CakePayPaymentMethod.XMR => 'Monero',
8 CakePayPaymentMethod.LTC => 'Litecoin',
9 - CakePayPaymentMethod.LTC_MWEB => 'Litecoin MWEB',
9 + CakePayPaymentMethod.LTC_MWEB => 'MWEB',
10 };
11 }
12
lib/di.dart
+1 -2
@@ -1424,8 +1424,7 @@ Future<void> setup({
1424 getIt.registerFactoryParam<CakePayBuyCardPage, List<dynamic>, void>((List<dynamic> args, _) {
1425 final vendor = args.first as CakePayVendor;
1426
1427 - return CakePayBuyCardPage(
1428 - getIt.get<CakePayBuyCardViewModel>(param1: vendor), getIt.get<CakePayService>());
1427 + return CakePayBuyCardPage(getIt.get<CakePayBuyCardViewModel>(param1: vendor));
1428 });
1429
1430 getIt.registerFactory(() => CakePayCardsPage(getIt.get<CakePayCardsListViewModel>()));
lib/src/screens/send/widgets/send_card.dart
+1 -1
@@ -443,7 +443,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
443 ),
444 ),
445 ),
446 - if (sendViewModel.currency == CryptoCurrency.ltc)
446 + if (sendViewModel.currency == CryptoCurrency.ltc && sendViewModel.isMwebEnabled)
447 Observer(
448 builder: (_) => Padding(
449 padding: EdgeInsets.only(top: 14),
lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart
+16 -7
@@ -17,8 +17,8 @@ class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCar
17
18 abstract class CakePayBuyCardViewModelBase with Store {
19 CakePayBuyCardViewModelBase(
20 - {required this.vendor, required this.cakePayService, required this.sendViewModel})
21 - : walletType = sendViewModel.walletType,
20 + {required this.vendor, required CakePayService cakePayService, required this.sendViewModel})
21 + : _cakePayService = cakePayService, walletType = sendViewModel.walletType,
22 amount = vendor.card!.denominations.isNotEmpty
23 ? double.parse(vendor.card!.denominations.first)
24 : 0,
@@ -31,7 +31,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
31
32 final CakePayVendor vendor;
33 final SendViewModel sendViewModel;
34 - final CakePayService cakePayService;
34 + final CakePayService _cakePayService;
35 final double max;
36 final double min;
37 final CakePayCard card;
@@ -48,6 +48,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
48 String simulatedResponse = '';
49
50 bool get isDenominationSelected => card.denominations.isNotEmpty;
51 +
52 + Future<bool> get isUserLogged async => await _cakePayService.isLogged();
53
54 @observable
55 double amount;
@@ -86,7 +88,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
88 case WalletType.bitcoin:
89 return [CakePayPaymentMethod.BTC];
90 case WalletType.litecoin:
89 - return [CakePayPaymentMethod.LTC, CakePayPaymentMethod.LTC_MWEB];
91 + return [CakePayPaymentMethod.LTC,if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB];
92 case WalletType.monero:
93 return [CakePayPaymentMethod.XMR];
94 default:
@@ -95,7 +97,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
97 }
98
99 @action
98 - void chooseMethod(CakePayPaymentMethod method) => selectedPaymentMethod = method;
100 + void chooseMethod(CakePayPaymentMethod method) {
101 + selectedPaymentMethod = method;
102 + if (walletType == WalletType.litecoin) {
103 + sendViewModel.setAllowMwebCoins(method == CakePayPaymentMethod.LTC_MWEB);
104 + }
105 + }
106
107 @action
108 void onQuantityChanged(int? input) => quantity = input ?? 1;
@@ -141,7 +148,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
148 FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.');
149 }
150 try {
144 - order = await cakePayService.createOrder(
151 + order = await _cakePayService.createOrder(
152 cardId: card.id,
153 price: amount.toString(),
154 quantity: quantity,
@@ -183,7 +190,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
190 }
191
192 try {
186 - simulatedResponse = await cakePayService.simulatePayment(orderId: order!.orderId);
193 + simulatedResponse = await _cakePayService.simulatePayment(orderId: order!.orderId);
194 sendViewModel.state = TransactionCommitted();
195
196 } catch (e) {
@@ -210,6 +217,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
217 formattedRemainingTime = formatDuration(remainingTime!);
218 }
219 }
220 +
221 + Future<void> logout() async => await _cakePayService.logout();
222
223 void _startExpirationTimer() {
224 _timer?.cancel();
lib/view_model/send/send_view_model.dart
+2
@@ -114,6 +114,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
114
115 bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled;
116
117 + bool get isMwebEnabled => balanceViewModel.mwebEnabled;
118 +
119 @action
120 void setShowAddressBookPopup(bool value) {
121 _settingsStore.showAddressBookPopupEnabled = value;