diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index 77aa40cb1..9511238b2 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -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 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 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.from(cakePayBuyCardViewModel.sendViewModel.outputs); + final outputsCopy = List.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(); }, ), diff --git a/lib/cake_pay/src/models/cake_pay_order.dart b/lib/cake_pay/src/models/cake_pay_order.dart index 52f11272e..ddbeb73aa 100644 --- a/lib/cake_pay/src/models/cake_pay_order.dart +++ b/lib/cake_pay/src/models/cake_pay_order.dart @@ -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', }; } diff --git a/lib/di.dart b/lib/di.dart index f913478c5..e19ee0b3c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1424,8 +1424,7 @@ Future setup({ getIt.registerFactoryParam, void>((List args, _) { final vendor = args.first as CakePayVendor; - return CakePayBuyCardPage( - getIt.get(param1: vendor), getIt.get()); + return CakePayBuyCardPage(getIt.get(param1: vendor)); }); getIt.registerFactory(() => CakePayCardsPage(getIt.get())); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 8c372b161..7260b8d00 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -443,7 +443,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Padding( padding: EdgeInsets.only(top: 14), diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart index 0540b93db..1bbb257cd 100644 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart @@ -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 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 logout() async => await _cakePayService.logout(); void _startExpirationTimer() { _timer?.cancel(); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 3813c65d6..3be2b30db 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -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;