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>
This commit is contained in:
Serhii
2025-08-12 16:06:03 +03:00
committed by GitHub
co-authored by Omar Hatem
parent d64a58389b
commit d24cb84e16
6 changed files with 92 additions and 55 deletions
@@ -3,7 +3,6 @@ import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dart';
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
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class CakePayBuyCardPage extends BasePage {
CakePayBuyCardPage(
this.cakePayBuyCardViewModel,
this.cakePayService,
) : _amountFieldFocus = FocusNode(),
this.cakePayBuyCardViewModel
) : _sendViewModel = cakePayBuyCardViewModel.sendViewModel,
_amountFieldFocus = FocusNode(),
_amountController = TextEditingController(),
_quantityFieldFocus = FocusNode(),
_quantityController =
@@ -51,7 +53,7 @@ class CakePayBuyCardPage extends BasePage {
}
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final CakePayService cakePayService;
final SendViewModel _sendViewModel;
bool _effectsInstalled = false;
late final BuildContext _overlayCtx;
@@ -301,6 +303,38 @@ class CakePayBuyCardPage extends BasePage {
methods.length <= 1 || selected == null
? const SizedBox.shrink()
: _buildPaymentMethodWidget(methods, selected),
if (_sendViewModel.walletType == WalletType.litecoin && _sendViewModel.isMwebEnabled)
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
child: GestureDetector(
key: ValueKey('cake_pay_buy_page_unspent_coin_button_key'),
onTap: () {
bool value = _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any;
_sendViewModel.setAllowMwebCoins(!value);
},
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StandardCheckbox(
caption: S.of(context).litecoin_mweb_allow_coins,
captionColor: Theme.of(context).colorScheme.onSurfaceVariant,
borderColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.primary,
value:
_sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any,
onChanged: (bool? value) {
_sendViewModel.setAllowMwebCoins(value ?? false);
},
),
],
),
),
),
),
),
if (FeatureFlag.hasDevOptions)
Padding(
padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
@@ -318,7 +352,7 @@ class CakePayBuyCardPage extends BasePage {
isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
cakePayBuyCardViewModel.isPurchasing,
isLoading:
cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
_sendViewModel.state is IsExecutingState ||
cakePayBuyCardViewModel.isPurchasing,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
@@ -338,7 +372,7 @@ class CakePayBuyCardPage extends BasePage {
text: S.of(context).purchase_gift_card,
isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
cakePayBuyCardViewModel.isPurchasing,
isLoading: cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
isLoading: _sendViewModel.state is IsExecutingState ||
cakePayBuyCardViewModel.isPurchasing,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
@@ -411,7 +445,7 @@ class CakePayBuyCardPage extends BasePage {
}
Future<void> confirmPurchaseFirst(BuildContext context) async {
bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
@@ -458,7 +492,7 @@ class CakePayBuyCardPage extends BasePage {
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
@@ -467,7 +501,7 @@ class CakePayBuyCardPage extends BasePage {
await cakePayBuyCardViewModel.createOrder();
} catch (_) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
await cakePayBuyCardViewModel.cakePayService.logout();
await cakePayBuyCardViewModel.logout();
}
}
cakePayBuyCardViewModel.isPurchasing = false;
@@ -484,11 +518,11 @@ class CakePayBuyCardPage extends BasePage {
_overlayCtx = Navigator.of(context).context;
if (cakePayBuyCardViewModel.sendViewModel.isElectrumWallet) {
bitcoin!.updateFeeRates(cakePayBuyCardViewModel.sendViewModel.wallet);
if (_sendViewModel.isElectrumWallet) {
bitcoin!.updateFeeRates(_sendViewModel.wallet);
}
reaction((_) => cakePayBuyCardViewModel.sendViewModel.state, (ExecutionState state) async {
reaction((_) => _sendViewModel.state, (ExecutionState state) async {
if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop();
if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) {
@@ -521,7 +555,7 @@ class CakePayBuyCardPage extends BasePage {
if (state is IsExecutingState) {
// wait a bit to avoid showing the loading dialog if transaction is failed
await Future.delayed(const Duration(milliseconds: 300));
final currentState = cakePayBuyCardViewModel.sendViewModel.state;
final currentState = _sendViewModel.state;
if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
return;
}
@@ -549,7 +583,7 @@ class CakePayBuyCardPage extends BasePage {
disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) {
if (isExpired) {
cakePayBuyCardViewModel.sendViewModel.state = FailureState('Order expired');
_sendViewModel.state = FailureState('Order expired');
disposer?.call();
}
});
@@ -558,7 +592,7 @@ class CakePayBuyCardPage extends BasePage {
if (context.mounted) {
final order = cakePayBuyCardViewModel.order;
final displayingOutputs = cakePayBuyCardViewModel.sendViewModel.outputs
final displayingOutputs = _sendViewModel.outputs
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
addresses: [o.address],
@@ -584,20 +618,16 @@ class CakePayBuyCardPage extends BasePage {
paymentId: S.of(bottomSheetContext).payment_id,
paymentIdValue: cakePayBuyCardViewModel.order?.orderId,
expirationTime: cakePayBuyCardViewModel.formattedRemainingTime,
walletType: cakePayBuyCardViewModel.sendViewModel.walletType,
titleIconPath:
cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
walletType: _sendViewModel.walletType,
titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath,
currency: _sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount,
amountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
fiatAmountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee,
feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: cakePayBuyCardViewModel
.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount:_sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: displayingOutputs,
footerType: FooterType.slideActionButton,
slideActionButtonText:
@@ -608,10 +638,10 @@ class CakePayBuyCardPage extends BasePage {
Navigator.of(bottomSheetContext).pop(true);
cakePayBuyCardViewModel.isSimulating
? cakePayBuyCardViewModel.simulatePayment()
: cakePayBuyCardViewModel.sendViewModel.commitTransaction(context);
: _sendViewModel.commitTransaction(context);
},
change: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.change,
isOpenCryptoPay: cakePayBuyCardViewModel.sendViewModel.ocpRequest != null,
change: _sendViewModel.pendingTransaction!.change,
isOpenCryptoPay: _sendViewModel.ocpRequest != null,
);
},
);
@@ -619,14 +649,14 @@ class CakePayBuyCardPage extends BasePage {
confirmBottomSheetContext = null;
cakePayBuyCardViewModel.isSimulatingFlow = false;
_handleDispose(disposer);
if (result == null) cakePayBuyCardViewModel.sendViewModel.dismissTransaction();
if (result == null) _sendViewModel.dismissTransaction();
}
});
}
if (state is TransactionCommitted) {
final order = cakePayBuyCardViewModel.order;
final outputsCopy = List<Output>.from(cakePayBuyCardViewModel.sendViewModel.outputs);
final outputsCopy = List<Output>.from(_sendViewModel.outputs);
final displayingOutputs = outputsCopy
.map((o) => o.OutputCopyWithParsedAddress(
@@ -640,7 +670,7 @@ class CakePayBuyCardPage extends BasePage {
))
.toList();
cakePayBuyCardViewModel.sendViewModel.clearOutputs();
_sendViewModel.clearOutputs();
final bool usePageContextLater = context.mounted;
@@ -668,17 +698,14 @@ class CakePayBuyCardPage extends BasePage {
child: Icon(Icons.check, size: 16, color: Colors.white),
),
output: displayingOutputs.first,
currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
currency: _sendViewModel.selectedCryptoCurrency,
amount: S.of(sheetCtx).send_amount,
amountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
fiatAmountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(sheetCtx).send_fee,
feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount:
cakePayBuyCardViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
paymentId: 'Order ID',
paymentIdValue: order?.orderId ?? '',
onClose: () {
@@ -709,7 +736,7 @@ class CakePayBuyCardPage extends BasePage {
content: S.of(bottomSheetContext).proceed_on_device_description,
singleActionButtonText: S.of(context).cancel,
onSingleActionButtonPressed: () {
cakePayBuyCardViewModel.sendViewModel.state = InitialExecutionState();
_sendViewModel.state = InitialExecutionState();
Navigator.of(bottomSheetContext).pop();
},
),
+2 -2
View File
@@ -3,10 +3,10 @@ enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB }
extension CakePayPaymentMethodLabel on CakePayPaymentMethod {
String get label => switch (this) {
CakePayPaymentMethod.BTC => 'Bitcoin',
CakePayPaymentMethod.BTC_LN => 'Bitcoin Lightning',
CakePayPaymentMethod.BTC_LN => 'Lightning',
CakePayPaymentMethod.XMR => 'Monero',
CakePayPaymentMethod.LTC => 'Litecoin',
CakePayPaymentMethod.LTC_MWEB => 'Litecoin MWEB',
CakePayPaymentMethod.LTC_MWEB => 'MWEB',
};
}
+1 -2
View File
@@ -1424,8 +1424,7 @@ Future<void> setup({
getIt.registerFactoryParam<CakePayBuyCardPage, List<dynamic>, void>((List<dynamic> args, _) {
final vendor = args.first as CakePayVendor;
return CakePayBuyCardPage(
getIt.get<CakePayBuyCardViewModel>(param1: vendor), getIt.get<CakePayService>());
return CakePayBuyCardPage(getIt.get<CakePayBuyCardViewModel>(param1: vendor));
});
getIt.registerFactory(() => CakePayCardsPage(getIt.get<CakePayCardsListViewModel>()));
+1 -1
View File
@@ -443,7 +443,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
),
),
),
if (sendViewModel.currency == CryptoCurrency.ltc)
if (sendViewModel.currency == CryptoCurrency.ltc && sendViewModel.isMwebEnabled)
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(top: 14),
@@ -17,8 +17,8 @@ class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCar
abstract class CakePayBuyCardViewModelBase with Store {
CakePayBuyCardViewModelBase(
{required this.vendor, required this.cakePayService, required this.sendViewModel})
: walletType = sendViewModel.walletType,
{required this.vendor, required CakePayService cakePayService, required this.sendViewModel})
: _cakePayService = cakePayService, walletType = sendViewModel.walletType,
amount = vendor.card!.denominations.isNotEmpty
? double.parse(vendor.card!.denominations.first)
: 0,
@@ -31,7 +31,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
final CakePayVendor vendor;
final SendViewModel sendViewModel;
final CakePayService cakePayService;
final CakePayService _cakePayService;
final double max;
final double min;
final CakePayCard card;
@@ -48,6 +48,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
String simulatedResponse = '';
bool get isDenominationSelected => card.denominations.isNotEmpty;
Future<bool> get isUserLogged async => await _cakePayService.isLogged();
@observable
double amount;
@@ -86,7 +88,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
case WalletType.bitcoin:
return [CakePayPaymentMethod.BTC];
case WalletType.litecoin:
return [CakePayPaymentMethod.LTC, CakePayPaymentMethod.LTC_MWEB];
return [CakePayPaymentMethod.LTC,if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB];
case WalletType.monero:
return [CakePayPaymentMethod.XMR];
default:
@@ -95,7 +97,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
}
@action
void chooseMethod(CakePayPaymentMethod method) => selectedPaymentMethod = method;
void chooseMethod(CakePayPaymentMethod method) {
selectedPaymentMethod = method;
if (walletType == WalletType.litecoin) {
sendViewModel.setAllowMwebCoins(method == CakePayPaymentMethod.LTC_MWEB);
}
}
@action
void onQuantityChanged(int? input) => quantity = input ?? 1;
@@ -141,7 +148,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.');
}
try {
order = await cakePayService.createOrder(
order = await _cakePayService.createOrder(
cardId: card.id,
price: amount.toString(),
quantity: quantity,
@@ -183,7 +190,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
}
try {
simulatedResponse = await cakePayService.simulatePayment(orderId: order!.orderId);
simulatedResponse = await _cakePayService.simulatePayment(orderId: order!.orderId);
sendViewModel.state = TransactionCommitted();
} catch (e) {
@@ -210,6 +217,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
formattedRemainingTime = formatDuration(remainingTime!);
}
}
Future<void> logout() async => await _cakePayService.logout();
void _startExpirationTimer() {
_timer?.cancel();
+2
View File
@@ -114,6 +114,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled;
bool get isMwebEnabled => balanceViewModel.mwebEnabled;
@action
void setShowAddressBookPopup(bool value) {
_settingsStore.showAddressBookPopupEnabled = value;