mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
Cw 973 in app gift card redemption flow plus UI (#2285)
* refactor: cleanup cake_pay folder * refactor: add My Cards tab on the main screen * ui: add user card item * refactor: cakePay purchase flow * feat: add user card info * refactor: auth flow * refactor: theme data * revert * Update qr_widget.dart * fix: update with new themes * minor fixes * feat: put redemption flow under a feature flag * feat: implement simulated purchasing flow in debug mode * feat: support LTC and MWEB payments * Update savings_page.dart * fix: UI fixes * add Cake Pay transaction sent bottom sheet * localization * Update cake_pay_buy_card_page.dart * addressing review comments * fix marge conflicts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_account_view_model.g.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_auth_view_model.g.dart';
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
|
||||
import 'dart:async';
|
||||
|
||||
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/models/cake_pay_vendor.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.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/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_buy_card_view_model.g.dart';
|
||||
@@ -7,20 +16,36 @@ part 'cake_pay_buy_card_view_model.g.dart';
|
||||
class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCardViewModel;
|
||||
|
||||
abstract class CakePayBuyCardViewModelBase with Store {
|
||||
CakePayBuyCardViewModelBase({required this.vendor})
|
||||
: amount = vendor.card!.denominations.isNotEmpty
|
||||
CakePayBuyCardViewModelBase(
|
||||
{required this.vendor, required this.cakePayService, required this.sendViewModel})
|
||||
: walletType = sendViewModel.walletType,
|
||||
amount = vendor.card!.denominations.isNotEmpty
|
||||
? double.parse(vendor.card!.denominations.first)
|
||||
: 0,
|
||||
quantity = 1,
|
||||
min = double.parse(vendor.card!.minValue ?? '0'),
|
||||
max = double.parse(vendor.card!.maxValue ?? '0'),
|
||||
card = vendor.card!;
|
||||
card = vendor.card! {
|
||||
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
|
||||
}
|
||||
|
||||
final CakePayVendor vendor;
|
||||
final CakePayCard card;
|
||||
|
||||
final double min;
|
||||
final SendViewModel sendViewModel;
|
||||
final CakePayService cakePayService;
|
||||
final double max;
|
||||
final double min;
|
||||
final CakePayCard card;
|
||||
final WalletType walletType;
|
||||
|
||||
CakePayOrder? order;
|
||||
Timer? _timer;
|
||||
DateTime? expirationTime;
|
||||
Duration? remainingTime;
|
||||
bool confirmsNoVpn = false;
|
||||
bool confirmsVoidedRefund = false;
|
||||
bool confirmsTermsAgreed = false;
|
||||
|
||||
String simulatedResponse = '';
|
||||
|
||||
bool get isDenominationSelected => card.denominations.isNotEmpty;
|
||||
|
||||
@@ -30,13 +55,48 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
@observable
|
||||
int quantity;
|
||||
|
||||
@observable
|
||||
bool isPurchasing = false;
|
||||
|
||||
@observable
|
||||
bool isSimulatingFlow = false;
|
||||
|
||||
@observable
|
||||
bool isOrderExpired = false;
|
||||
|
||||
@observable
|
||||
String formattedRemainingTime = '';
|
||||
|
||||
@computed
|
||||
bool get isEnablePurchase =>
|
||||
bool get isAmountSufficient =>
|
||||
(amount >= min && amount <= max) || (isDenominationSelected && quantity > 0);
|
||||
|
||||
@observable
|
||||
CakePayPaymentMethod? selectedPaymentMethod;
|
||||
|
||||
@computed
|
||||
double get totalAmount => amount * quantity;
|
||||
|
||||
@computed
|
||||
bool get isSimulating => isSimulatingFlow && FeatureFlag.hasDevOptions;
|
||||
|
||||
@computed
|
||||
List<CakePayPaymentMethod> get availableMethods {
|
||||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
return [CakePayPaymentMethod.BTC];
|
||||
case WalletType.litecoin:
|
||||
return [CakePayPaymentMethod.LTC, CakePayPaymentMethod.LTC_MWEB];
|
||||
case WalletType.monero:
|
||||
return [CakePayPaymentMethod.XMR];
|
||||
default:
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void chooseMethod(CakePayPaymentMethod method) => selectedPaymentMethod = method;
|
||||
|
||||
@action
|
||||
void onQuantityChanged(int? input) => quantity = input ?? 1;
|
||||
|
||||
@@ -45,4 +105,135 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
if (input.isEmpty) return;
|
||||
amount = double.parse(input.replaceAll(',', '.'));
|
||||
}
|
||||
|
||||
CryptoPaymentData? getPaymentDataFor(CakePayPaymentMethod? method) {
|
||||
if (order == null || method == null) return null;
|
||||
|
||||
|
||||
final data = switch (method) {
|
||||
CakePayPaymentMethod.BTC => order?.paymentData.btc,
|
||||
CakePayPaymentMethod.XMR => order?.paymentData.xmr,
|
||||
CakePayPaymentMethod.LTC => order?.paymentData.ltc,
|
||||
CakePayPaymentMethod.LTC_MWEB => order?.paymentData.ltc_mweb,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (data == null) return null;
|
||||
|
||||
final bip21 = data.paymentUrls?.bip21;
|
||||
if (bip21 != null && bip21.isNotEmpty) {
|
||||
final uri = Uri.parse(bip21);
|
||||
final addr = uri.path;
|
||||
final price = uri.queryParameters['amount'] ?? data.price;
|
||||
|
||||
return CryptoPaymentData(price: price, address: addr);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> createOrder() async {
|
||||
if (walletType != WalletType.bitcoin &&
|
||||
walletType != WalletType.monero &&
|
||||
walletType != WalletType.litecoin) {
|
||||
sendViewModel.state =
|
||||
FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.');
|
||||
}
|
||||
try {
|
||||
order = await cakePayService.createOrder(
|
||||
cardId: card.id,
|
||||
price: amount.toString(),
|
||||
quantity: quantity,
|
||||
confirmsNoVpn: confirmsNoVpn,
|
||||
confirmsVoidedRefund: confirmsVoidedRefund,
|
||||
confirmsTermsAgreed: confirmsTermsAgreed,
|
||||
);
|
||||
await confirmSending();
|
||||
expirationTime = order!.paymentData.expirationTime;
|
||||
updateRemainingTime();
|
||||
_startExpirationTimer();
|
||||
} catch (e) {
|
||||
sendViewModel.state = FailureState(
|
||||
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> confirmSending() async {
|
||||
final cryptoPaymentData = getPaymentDataFor(selectedPaymentMethod);
|
||||
if (order == null || cryptoPaymentData == null) return;
|
||||
|
||||
try {
|
||||
sendViewModel.clearOutputs();
|
||||
final output = sendViewModel.outputs.first;
|
||||
output.address = cryptoPaymentData.address;
|
||||
output.setCryptoAmount(cryptoPaymentData.price);
|
||||
|
||||
await sendViewModel.createTransaction();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> simulatePayment() async {
|
||||
if (order == null) {
|
||||
throw Exception('Order is not created yet.');
|
||||
}
|
||||
|
||||
try {
|
||||
simulatedResponse = await cakePayService.simulatePayment(orderId: order!.orderId);
|
||||
sendViewModel.state = TransactionCommitted();
|
||||
|
||||
} catch (e) {
|
||||
sendViewModel.state = FailureState(
|
||||
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void updateRemainingTime() {
|
||||
if (expirationTime == null) {
|
||||
formattedRemainingTime = '';
|
||||
return;
|
||||
}
|
||||
|
||||
remainingTime = expirationTime!.difference(DateTime.now());
|
||||
|
||||
isOrderExpired = remainingTime!.isNegative;
|
||||
|
||||
if (isOrderExpired) {
|
||||
disposeExpirationTimer();
|
||||
sendViewModel.state = FailureState('Order has expired.');
|
||||
} else {
|
||||
formattedRemainingTime = formatDuration(remainingTime!);
|
||||
}
|
||||
}
|
||||
|
||||
void _startExpirationTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||
updateRemainingTime();
|
||||
});
|
||||
}
|
||||
|
||||
String formatDuration(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds
|
||||
.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void disposeExpirationTimer() {
|
||||
_timer?.cancel();
|
||||
remainingTime = null;
|
||||
formattedRemainingTime = '';
|
||||
expirationTime = null;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disposeExpirationTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
|
||||
import 'package:cake_wallet/entities/country.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
@@ -17,8 +20,8 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
CakePayCardsListViewModelBase({
|
||||
required this.cakePayService,
|
||||
required this.settingsStore,
|
||||
}) : cardState = CakePayCardsStateNoCards(),
|
||||
cakePayVendors = [],
|
||||
}) : cakePayVendors = [],
|
||||
userCards = [],
|
||||
availableCountries = [],
|
||||
page = 1,
|
||||
displayPrepaidCards = true,
|
||||
@@ -28,8 +31,11 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
scrollOffsetFromTop = 0.0,
|
||||
vendorsState = InitialCakePayVendorLoadingState(),
|
||||
createCardState = CakePayCreateCardState(),
|
||||
userCardState = UserCakePayCardsStateInitial(),
|
||||
searchString = '',
|
||||
searchMyCardsString = '',
|
||||
CakePayVendorList = <CakePayVendor>[] {
|
||||
checkAuth();
|
||||
initialization();
|
||||
}
|
||||
|
||||
@@ -43,6 +49,7 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
void initialization() async {
|
||||
await getCountries();
|
||||
getVendors();
|
||||
getUserCards();
|
||||
}
|
||||
|
||||
final CakePayService cakePayService;
|
||||
@@ -51,7 +58,7 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
List<CakePayVendor> CakePayVendorList;
|
||||
|
||||
Map<String, List<FilterItem>> get createFilterItems => {
|
||||
S.current.filter_by: [
|
||||
'Card Type': [
|
||||
FilterItem(
|
||||
value: () => displayPrepaidCards,
|
||||
caption: S.current.prepaid_cards,
|
||||
@@ -74,6 +81,7 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
};
|
||||
|
||||
String searchString;
|
||||
String? username;
|
||||
int page;
|
||||
|
||||
late Country _initialSelectedCountry;
|
||||
@@ -82,6 +90,12 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
late bool _initialDisplayDenominationsCards;
|
||||
late bool _initialDisplayCustomValueCards;
|
||||
|
||||
@observable
|
||||
List<CakePayCard> userCards;
|
||||
|
||||
@observable
|
||||
String searchMyCardsString;
|
||||
|
||||
@observable
|
||||
double scrollOffsetFromTop;
|
||||
|
||||
@@ -89,7 +103,7 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
CakePayCreateCardState createCardState;
|
||||
|
||||
@observable
|
||||
CakePayCardsState cardState;
|
||||
UserCakePayCardsState userCardState;
|
||||
|
||||
@observable
|
||||
CakePayVendorState vendorsState;
|
||||
@@ -118,12 +132,29 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
@observable
|
||||
bool displayCustomValueCards;
|
||||
|
||||
@observable
|
||||
ObservableFuture<bool>? authFuture;
|
||||
|
||||
@computed
|
||||
bool? get isUserAuthenticated =>
|
||||
authFuture?.status == FutureStatus.fulfilled ? authFuture?.value : null;
|
||||
|
||||
@computed
|
||||
Country get selectedCountry =>
|
||||
settingsStore.selectedCakePayCountry ?? _getInitialCountry(settingsStore.fiatCurrency);
|
||||
|
||||
@computed
|
||||
bool get shouldShowCountryPicker => settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
|
||||
bool get shouldShowCountryPicker =>
|
||||
settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
|
||||
|
||||
@computed
|
||||
List<CakePayCard> get filteredUserCards {
|
||||
final query = searchMyCardsString.trim().toLowerCase();
|
||||
if (query.isEmpty) return userCards;
|
||||
return userCards
|
||||
.where((card) => card.name.toLowerCase().contains(query))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
|
||||
bool get hasFiltersChanged {
|
||||
@@ -134,7 +165,6 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
displayCustomValueCards != _initialDisplayCustomValueCards;
|
||||
}
|
||||
|
||||
|
||||
Future<void> getCountries() async {
|
||||
try {
|
||||
availableCountries = await cakePayService.getCountries();
|
||||
@@ -143,6 +173,44 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getUserCards() async {
|
||||
//Dummy user cards // TODO: fetch from API
|
||||
userCardState = UserCakePayCardsStateFetching();
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
final vendorsCard = cakePayVendors
|
||||
.where((vendor) => vendor.card != null)
|
||||
.map((vendor) => vendor.card!)
|
||||
.toList();
|
||||
userCards = vendorsCard.sublist(0, 10);
|
||||
|
||||
vendorsCard.forEach((card) {
|
||||
if (card.name.toLowerCase().contains('amazon.com')) {
|
||||
userCards.add(card);
|
||||
}
|
||||
});
|
||||
|
||||
userCardState = UserCakePayCardsStateSuccess();
|
||||
if (userCards.isEmpty) {
|
||||
userCardState = UserCakePayCardsStateNoCards();
|
||||
}
|
||||
} catch (e) {
|
||||
userCardState = UserCakePayCardsStateFailure(
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> checkAuth() async {
|
||||
authFuture = ObservableFuture(cakePayService.isLogged());
|
||||
|
||||
final logged = await authFuture!;
|
||||
if (logged) {
|
||||
username = await cakePayService.getUserEmail();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> getVendors({
|
||||
String? text,
|
||||
@@ -219,6 +287,9 @@ abstract class CakePayCardsListViewModelBase with Store {
|
||||
settingsStore.selectedCakePayCountry = country;
|
||||
}
|
||||
|
||||
@action
|
||||
void setMyCardsQuery(String text) => searchMyCardsString = text;
|
||||
|
||||
@action
|
||||
void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards;
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart';
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_purchase_view_model.g.dart';
|
||||
|
||||
class CakePayPurchaseViewModel = CakePayPurchaseViewModelBase with _$CakePayPurchaseViewModel;
|
||||
|
||||
abstract class CakePayPurchaseViewModelBase with Store {
|
||||
CakePayPurchaseViewModelBase({
|
||||
required this.cakePayService,
|
||||
required this.paymentCredential,
|
||||
required this.card,
|
||||
required this.sendViewModel,
|
||||
}) : walletType = sendViewModel.walletType;
|
||||
|
||||
final WalletType walletType;
|
||||
|
||||
final PaymentCredential paymentCredential;
|
||||
|
||||
final CakePayCard card;
|
||||
|
||||
final SendViewModel sendViewModel;
|
||||
|
||||
final CakePayService cakePayService;
|
||||
|
||||
CakePayOrder? order;
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
DateTime? expirationTime;
|
||||
|
||||
Duration? remainingTime;
|
||||
|
||||
String? get userName => paymentCredential.userName;
|
||||
|
||||
double get amount => paymentCredential.amount;
|
||||
|
||||
int get quantity => paymentCredential.quantity;
|
||||
|
||||
double get totalAmount => paymentCredential.totalAmount;
|
||||
|
||||
String get fiatCurrency => paymentCredential.fiatCurrency;
|
||||
|
||||
bool confirmsNoVpn = false;
|
||||
bool confirmsVoidedRefund = false;
|
||||
bool confirmsTermsAgreed = false;
|
||||
|
||||
@observable
|
||||
bool isPurchasing = false;
|
||||
|
||||
CryptoPaymentData? get cryptoPaymentData {
|
||||
if (order == null) return null;
|
||||
|
||||
if (WalletType.monero == walletType) {
|
||||
return order!.paymentData.xmr;
|
||||
}
|
||||
|
||||
if (WalletType.bitcoin == walletType) {
|
||||
final paymentUrls = order!.paymentData.btc.paymentUrls!.bip21;
|
||||
|
||||
final uri = Uri.parse(paymentUrls!);
|
||||
|
||||
final address = uri.path;
|
||||
final price = uri.queryParameters['amount'];
|
||||
|
||||
return CryptoPaymentData(
|
||||
address: address,
|
||||
price: price ?? '0',
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@observable
|
||||
bool isOrderExpired = false;
|
||||
|
||||
@observable
|
||||
String formattedRemainingTime = '';
|
||||
|
||||
@action
|
||||
Future<void> createOrder() async {
|
||||
if (walletType != WalletType.bitcoin && walletType != WalletType.monero) {
|
||||
sendViewModel.state = FailureState('Unsupported wallet type, please use Bitcoin or Monero.');
|
||||
}
|
||||
try {
|
||||
order = await cakePayService.createOrder(
|
||||
cardId: card.id,
|
||||
price: paymentCredential.amount.toString(),
|
||||
quantity: paymentCredential.quantity,
|
||||
confirmsNoVpn: confirmsNoVpn,
|
||||
confirmsVoidedRefund: confirmsVoidedRefund,
|
||||
confirmsTermsAgreed: confirmsTermsAgreed,
|
||||
);
|
||||
await confirmSending();
|
||||
expirationTime = order!.paymentData.expirationTime;
|
||||
updateRemainingTime();
|
||||
_startExpirationTimer();
|
||||
} catch (e) {
|
||||
sendViewModel.state = FailureState(
|
||||
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> confirmSending() async {
|
||||
final cryptoPaymentData = this.cryptoPaymentData;
|
||||
try {
|
||||
if (order == null || cryptoPaymentData == null) return;
|
||||
|
||||
sendViewModel.clearOutputs();
|
||||
final output = sendViewModel.outputs.first;
|
||||
output.address = cryptoPaymentData.address;
|
||||
output.setCryptoAmount(cryptoPaymentData.price);
|
||||
|
||||
await sendViewModel.createTransaction();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void updateRemainingTime() {
|
||||
if (expirationTime == null) {
|
||||
formattedRemainingTime = '';
|
||||
return;
|
||||
}
|
||||
|
||||
remainingTime = expirationTime!.difference(DateTime.now());
|
||||
|
||||
isOrderExpired = remainingTime!.isNegative;
|
||||
|
||||
if (isOrderExpired) {
|
||||
disposeExpirationTimer();
|
||||
sendViewModel.state = FailureState('Order has expired.');
|
||||
} else {
|
||||
formattedRemainingTime = formatDuration(remainingTime!);
|
||||
}
|
||||
}
|
||||
|
||||
void _startExpirationTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||
updateRemainingTime();
|
||||
});
|
||||
}
|
||||
|
||||
String formatDuration(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void disposeExpirationTimer() {
|
||||
_timer?.cancel();
|
||||
remainingTime = null;
|
||||
formattedRemainingTime = '';
|
||||
expirationTime = null;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disposeExpirationTimer();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_features_view_model.g.dart';
|
||||
|
||||
@@ -1,67 +1,57 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DropdownFilterList extends StatefulWidget {
|
||||
DropdownFilterList({
|
||||
class DropdownFilterList extends StatelessWidget {
|
||||
const DropdownFilterList({
|
||||
Key? key,
|
||||
required this.items,
|
||||
this.itemPrefix,
|
||||
this.textStyle,
|
||||
required this.caption,
|
||||
required this.selectedItem,
|
||||
required this.onItemSelected,
|
||||
this.itemPrefix,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<String> items;
|
||||
final String? itemPrefix;
|
||||
final TextStyle? textStyle;
|
||||
final String caption;
|
||||
final String selectedItem;
|
||||
final Function(String) onItemSelected;
|
||||
|
||||
@override
|
||||
_DropdownFilterListState createState() => _DropdownFilterListState();
|
||||
}
|
||||
|
||||
class _DropdownFilterListState extends State<DropdownFilterList> {
|
||||
String? selectedValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedValue = widget.selectedItem;
|
||||
}
|
||||
final String? itemPrefix;
|
||||
final ValueChanged<String> onItemSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonHideUnderline(
|
||||
child: Container(
|
||||
child: DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
icon: Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Icon(Icons.arrow_drop_down, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: DropdownButton<String>(
|
||||
isDense: true,
|
||||
dropdownColor: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
items: widget.items
|
||||
.map((item) => DropdownMenuItem<String>(
|
||||
alignment: Alignment.bottomCenter,
|
||||
value: item,
|
||||
child: AutoSizeText('${widget.itemPrefix ?? ''} $item', style: widget.textStyle),
|
||||
))
|
||||
selectedItemBuilder: (context) => items
|
||||
.map(
|
||||
(item) => Text(
|
||||
'${itemPrefix ?? ''} $item',
|
||||
style: Theme.of(context).textTheme.titleMedium!,
|
||||
maxLines: 1,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: selectedValue,
|
||||
onChanged: (newValue) {
|
||||
setState(() => selectedValue = newValue);
|
||||
widget.onItemSelected(newValue!);
|
||||
items: items
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'${itemPrefix ?? ''} $item',
|
||||
style: (const TextStyle()).copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: selectedItem,
|
||||
onChanged: (value) {
|
||||
if (value != null) onItemSelected(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
icon: Icon(Icons.keyboard_arrow_down_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,3 +335,29 @@ abstract class OutputBase with Store {
|
||||
note = parsedAddress.description;
|
||||
}
|
||||
}
|
||||
|
||||
extension OutputCopyWith on Output {
|
||||
Output OutputCopyWithParsedAddress({
|
||||
ParsedAddress? parsedAddress,
|
||||
String? fiatAmount,
|
||||
}) {
|
||||
final clone = Output(
|
||||
_wallet,
|
||||
_settingsStore,
|
||||
_fiatConversationStore,
|
||||
cryptoCurrencyHandler,
|
||||
);
|
||||
|
||||
clone
|
||||
..cryptoAmount = cryptoAmount
|
||||
..cryptoFullBalance = cryptoFullBalance
|
||||
..note = note
|
||||
..sendAll = sendAll
|
||||
..memo = memo
|
||||
..stealthAddress = stealthAddress
|
||||
..parsedAddress = parsedAddress ?? this.parsedAddress
|
||||
..fiatAmount = fiatAmount ?? this.fiatAmount;
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user