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;