CW-1139-Migrate-CakePay-mobile-to-use-the-new-backend-APIs (#2416)

* migrate to new backend API

* hide value_type filter

* Improve vendor card parsing

* Update country.dart

* Update lib/cake_pay/src/models/cake_pay_card.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Update lib/cake_pay/src/models/cake_pay_card.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* address review comments

* fix min limit value

* Update lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor denomination handling and min value logic

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2025-08-13 09:07:30 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 75b811f9de
commit 328422cfb5
13 changed files with 283 additions and 216 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -157,10 +157,10 @@ class CakePayBuyCardPage extends BasePage {
),
bottomCardChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: card.denominations.isNotEmpty
child: card.denominationItems.isNotEmpty
? DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominations,
denominations: card.denominationItems,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
@@ -335,7 +335,7 @@ class CakePayBuyCardPage extends BasePage {
),
),
),
if (FeatureFlag.hasDevOptions)
if (FeatureFlag.hasDevOptions && FeatureFlag.isCakePayPurchaseSimulationEnabled)
Padding(
padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
child: LoadingPrimaryButton(
@@ -303,7 +303,7 @@ class _MyCardsTabState extends State<_MyCardsTab> {
final cards = viewModel.filteredUserCards;
if (viewModel.userCardState is UserCakePayCardsStateFetching) return const _Loading();
if (viewModel.userCardState is UserCakePayCardsStateNoCards)
return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
return Center(child: Text(S.of(context).no_cards_found));
final showThumb = cards.length > 6;
final userCardsList = Stack(
@@ -467,7 +467,7 @@ class _ShopTabState extends State<_ShopTab> {
}
if (vendors.isEmpty)
return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
return Center(child: Text(S.of(context).no_cards_found));
final loadingMore = viewModel.isLoadingNextPage;
final showThumb = vendors.length > 3;
+47 -18
View File
@@ -10,12 +10,11 @@ class CakePayCard {
final String? cardImageUrl;
final String? country;
final FiatCurrency fiatCurrency;
final List<String> denominationsUsd;
final List<String> denominations;
final String? minValueUsd;
final String? maxValueUsd;
final String? minValue;
final String? maxValue;
final List<Denomination> denominationItems;
CakePayCard({
required this.id,
@@ -27,31 +26,37 @@ class CakePayCard {
this.cardImageUrl,
this.country,
required this.fiatCurrency,
required this.denominationsUsd,
required this.denominations,
this.minValueUsd,
this.maxValueUsd,
this.minValue,
this.maxValue,
});
List<Denomination>? denominationItems,
}) : denominationItems = denominationItems ?? const [];
factory CakePayCard.fromJson(Map<String, dynamic> json) {
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? '');
final parsedMinValue = _toDouble(json['min_value'] as String?);
final minValue = fiatCurrency == FiatCurrency.usd && parsedMinValue != null && parsedMinValue < 10.00
? '10.00'
: json['min_value'] as String?;
String? minValue = json['min_value'] as String?;
final List<String> denominationsUsd =
(json['denominations_usd'] as List?)?.map((e) => e.toString()).toList() ?? [];
final List<String> denominations =
(json['denominations'] as List?)?.map((e) => e.toString()).toList() ?? [];
final parsedMinValueLocal = _toDouble(json['min_value']);
final parsedMinValueUsd = _toDouble(json['min_value_usd']);
if (parsedMinValueLocal != null && parsedMinValueLocal > 0 && parsedMinValueUsd != null && parsedMinValueUsd > 0 && parsedMinValueUsd < 10.0) {
final rate = parsedMinValueLocal / parsedMinValueUsd;
final minLocalValueLimit = 10.0 * rate;
minValue = minLocalValueLimit.toStringAsFixed(2);
}
final raw = (json['denominations'] as List?) ?? const [];
final denominations = <Denomination>[];
for (final item in raw) {
if (item is Map) {
denominations.add(Denomination.fromJson(Map<String, dynamic>.from(item)));
}
}
return CakePayCard(
id: json['id'] as int? ?? 0,
@@ -63,22 +68,46 @@ class CakePayCard {
cardImageUrl: json['card_image_url'] as String?,
country: json['country'] as String?,
fiatCurrency: fiatCurrency,
denominationsUsd: denominationsUsd,
denominations: denominations,
minValueUsd: json['min_value_usd'] as String?,
maxValueUsd: json['max_value_usd'] as String?,
minValue: minValue,
maxValue: json['max_value'] as String?,
denominationItems: denominations,
);
}
static String stripHtmlIfNeeded(String text) {
return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
}
static double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
}
class Denomination {
final double value;
final int? cardId;
final double? usdValue;
Denomination({
required this.value,
required this.cardId,
this.usdValue,
});
static double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
factory Denomination.fromJson(Map<String, dynamic> json) {
return Denomination(
value: _toDouble(json['value']) ?? 0,
cardId: json['card_id'] is int ? json['card_id'] as int : int.tryParse('${json['card_id']}'),
usdValue: _toDouble(json['usd_value']),
);
}
}
+16 -8
View File
@@ -1,9 +1,12 @@
import 'package:cake_wallet/entities/country.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'cake_pay_card.dart';
class CakePayVendor {
final int id;
final String name;
final bool unavailable;
final bool available;
final String? cakeWarnings;
final String country;
final CakePayCard? card;
@@ -11,30 +14,35 @@ class CakePayVendor {
CakePayVendor({
required this.id,
required this.name,
required this.unavailable,
required this.available,
this.cakeWarnings,
required this.country,
this.card,
});
factory CakePayVendor.fromJson(Map<String, dynamic> json, String country) {
factory CakePayVendor.fromJson(Map<String, dynamic> json) {
final name = stripHtmlIfNeeded(json['name'] as String);
final parsedCountry = json['country'] as String;
final country = Country.normalizeName(parsedCountry);
var cardsJson = json['cards'] as List?;
CakePayCard? cardForVendor;
if (cardsJson != null && cardsJson.isNotEmpty) {
try {
cardForVendor = CakePayCard.fromJson(cardsJson
.where((element) => element['country'] == country)
.first as Map<String, dynamic>);
} catch (_) {}
cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
return Country.normalizeName(card['country'] as String) == country;
}) as Map<String, dynamic>);
} catch (e) {
printV('Error parsing card for vendor: $e');
}
}
return CakePayVendor(
id: json['id'] as int,
name: name,
unavailable: json['unavailable'] as bool? ?? false,
available: json['available'] as bool? ?? false,
cakeWarnings: json['cake_warnings'] as String?,
country: country,
card: cardForVendor,
+42 -56
View File
@@ -3,25 +3,24 @@ import 'dart:convert';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_user_credentials.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/entities/country.dart';
class CakePayApi {
static const testBaseUri = false;
static const testBaseUri = FeatureFlag.hasDevOptions;
static const baseTestCakePayUri = 'test.cakepay.com';
static const baseProdCakePayUri = 'buy.cakepay.com';
static const baseTestCakePayUri = 'api-stg.cakepay.com';
static const baseProdCakePayUri = 'api-prod.cakepay.com';
static const baseCakePayUri = testBaseUri ? baseTestCakePayUri : baseProdCakePayUri;
static const vendorsPath = '/api/vendors';
static const countriesPath = '/api/countries';
static const authPath = '/api/auth';
static final verifyEmailPath = '/api/verify';
static final logoutPath = '/api/logout';
static final createOrderPath = '/api/order';
static final simulatePaymentPath = '/api/simulate_payment';
static const vendorsPath = '/api/marketplace/vendors';
static const authPath = '/api/accounts/auth';
static final verifyEmailPath = '/api/accounts/auth/verify';
static final logoutPath = '/api/accounts/logout';
static final createOrderPath = '/api/orders/order';
static final simulatePaymentPath = '/api/orders/simulate-payment';
/// AuthenticateUser
Future<String> authenticateUser({required String email, required String apiKey}) async {
@@ -110,7 +109,7 @@ class CakePayApi {
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Api-Key $apiKey',
'Authorization': 'Token $token',
};
final body = json.encode({
@@ -118,7 +117,6 @@ class CakePayApi {
'price': price,
'quantity': quantity,
'user_email': userEmail,
'token': token,
'send_email': true,
'confirms_no_vpn': confirmsNoVpn,
'confirms_voided_refund': confirmsVoidedRefund,
@@ -159,16 +157,27 @@ class CakePayApi {
///Simulate Payment
Future<String> simulatePayment(
{required String CSRFToken, required String authorization, required String orderId}) async {
final uri = Uri.https(baseCakePayUri, simulatePaymentPath + '/$orderId');
{required String CSRFToken,
required String authorization,
required String orderId,
required String token}) async {
final uri = Uri.https(baseCakePayUri, simulatePaymentPath);
final headers = {
'accept': 'application/json',
'authorization': authorization,
'X-CSRFToken': CSRFToken,
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Token $token',
};
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
final body = json.encode({
'order_id': orderId,
});
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: body,
);
printV('Response: ${response.statusCode}');
@@ -205,54 +214,31 @@ class CakePayApi {
}
}
/// Get Countries
Future<List<Country>> getCountries({required String apiKey}) async {
final uri = Uri.https(baseCakePayUri, countriesPath);
final headers = {
'accept': 'application/json',
'Authorization': 'Api-Key $apiKey',
};
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as List;
return bodyJson
.map<String>((country) => country['name'] as String)
.map((name) => Country.fromCakePayName(name))
.whereType<Country>()
.toList();
}
/// Get Vendors
Future<List<CakePayVendor>> getVendors({
required String apiKey,
required String country,
int? page,
String? countryCode,
required int page,
required String countryCode,
String? country,
String? search,
List<String>? vendorIds,
bool? giftCards,
bool? prepaidCards,
bool? giftCards = true,
bool? prepaidCards = true,
bool? onDemand,
bool? custom,
}) async {
var queryParams = {
'page': page?.toString(),
'country': country,
'page': page.toString(),
'country_code': countryCode,
'search': search,
'vendor_ids': vendorIds?.join(','),
'gift_cards': giftCards?.toString(),
'prepaid_cards': prepaidCards?.toString(),
'on_demand': onDemand?.toString(),
'custom': custom?.toString(),
if (search != null && search.isNotEmpty) 'search': search,
if (vendorIds != null && vendorIds.isNotEmpty) 'vendor_ids': vendorIds.join(','),
};
if (giftCards == false || prepaidCards == false) {
queryParams['gift_cards'] = giftCards.toString();
queryParams['prepaid_cards'] = prepaidCards.toString();
}
final uri = Uri.https(baseCakePayUri, vendorsPath, queryParams);
var headers = {
@@ -275,7 +261,7 @@ class CakePayApi {
}
return (bodyJson['results'] as List)
.map((e) => CakePayVendor.fromJson(e as Map<String, dynamic>, country))
.map((e) => CakePayVendor.fromJson(e as Map<String, dynamic>))
.toList();
}
}
@@ -23,15 +23,11 @@ class CakePayService {
final SecureStorage secureStorage;
final CakePayApi cakePayApi;
/// Get Available Countries
Future<List<Country>> getCountries() async =>
await cakePayApi.getCountries(apiKey: cakePayApiKey);
/// Get Vendors
Future<List<CakePayVendor>> getVendors({
required String country,
int? page,
String? countryCode,
required int page,
required String countryCode,
String? country,
String? search,
List<String>? vendorIds,
bool? giftCards,
@@ -114,6 +110,9 @@ class CakePayService {
}
///Simulate Purchase Gift Card
Future<String> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
CSRFToken: CSRFToken, authorization: authorization, orderId: orderId);
Future<String> simulatePayment({required String orderId}) async {
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.simulatePayment(
CSRFToken: CSRFToken, authorization: authorization, token: token, orderId: orderId);
}
}
@@ -1,3 +1,4 @@
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
import 'package:cake_wallet/typography.dart';
@@ -6,20 +7,21 @@ import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dar
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class DenominationsAmountWidget extends StatelessWidget {
const DenominationsAmountWidget(
{required this.fiatCurrency,
required this.denominations,
required this.amountFieldFocus,
required this.amountController,
required this.quantityFieldFocus,
required this.quantityController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
required this.onQuantityChanged});
class DenominationsAmountWidget extends StatefulWidget {
const DenominationsAmountWidget({
required this.fiatCurrency,
required this.denominations,
required this.amountFieldFocus,
required this.amountController,
required this.quantityFieldFocus,
required this.quantityController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
required this.onQuantityChanged,
});
final String fiatCurrency;
final List<String> denominations;
final List<Denomination> denominations;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final FocusNode quantityFieldFocus;
@@ -28,6 +30,28 @@ class DenominationsAmountWidget extends StatelessWidget {
final Function(String) onAmountChanged;
final Function(int?) onQuantityChanged;
@override
State<DenominationsAmountWidget> createState() => _DenominationsAmountWidgetState();
}
class _DenominationsAmountWidgetState extends State<DenominationsAmountWidget> {
late (String, int?) _selected;
@override
void initState() {
super.initState();
final first = widget.denominations.first;
final amount = widget.amountController.text.isNotEmpty
? widget.amountController.text
: first.value.toString();
_selected = (amount, first.cardId);
widget.cakePayBuyCardViewModel.selectedDenomination = _selected;
widget.amountController.text = _selected.$1;
widget.onAmountChanged(_selected.$1);
}
@override
Widget build(BuildContext context) {
return Container(
@@ -41,24 +65,36 @@ class DenominationsAmountWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
DropdownFilterList(
items: denominations,
itemPrefix: fiatCurrency,
selectedItem: denominations.first,
onItemSelected: (value) {
amountController.text = value;
onAmountChanged(value);
}),
items: widget.denominations
.map((e) => e.value.toString())
.toList(),
itemPrefix: widget.fiatCurrency,
selectedItem: _selected.$1,
onItemSelected: (value) {
setState(() => _selected = (value, widget.denominations
.firstWhere((e) => e.value.toString() == value)
.cardId));
widget.amountController.text = value;
widget.onAmountChanged(value);
widget.cakePayBuyCardViewModel.selectedDenomination = (_selected.$1, _selected.$2);
},
),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant)),
top: BorderSide(
width: 1.0,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
child: Text(
S.of(context).value,
maxLines: 2,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
child: Text(S.of(context).value,
maxLines: 2,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
),
@@ -70,51 +106,65 @@ class DenominationsAmountWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
NumberTextField(
controller: quantityController,
focusNode: quantityFieldFocus,
min: 1,
max: 99,
onChanged: (value) => onQuantityChanged(value)),
controller: widget.quantityController,
focusNode: widget.quantityFieldFocus,
min: 1,
max: 99,
onChanged: (value) => widget.onQuantityChanged(value),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
width: 1.0,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
child: Text(S.of(context).quantity,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
child: Text(
S.of(context).quantity,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
],
),
),
Spacer(),
Expanded(
flex: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Observer(
builder: (_) => Text('$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
maxLines: 1, style: Theme.of(context).textTheme.titleMedium!)),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
flex: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Observer(
builder: (_) => Text(
'${widget.fiatCurrency} ${widget.cakePayBuyCardViewModel.totalAmount}',
maxLines: 1,
style: Theme.of(context).textTheme.titleMedium!,
),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
child: Text(S.of(context).total,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
)),
child: Text(
S.of(context).total,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
],
),
),
],
),
);
+13 -18
View File
@@ -10,6 +10,9 @@ class Country extends EnumerableItem<String> with Serializable<String> {
static List<Country> get all => _all.values.toList();
static List<Country> get allForCakePay => _all.values
.where((element) => element.countryCode != 'EU' && element.countryCode != 'AQ').toList();
static const afghanistan = Country(code: 'afg', countryCode: 'AF', fullName: "Afghanistan");
static const andorra = Country(code: 'and', countryCode: 'AD', fullName: "Andorra");
static const angola = Country(code: 'ago', countryCode: 'AO', fullName: "Angola");
@@ -352,30 +355,22 @@ class Country extends EnumerableItem<String> with Serializable<String> {
'Curaçao': "Curacao",
};
static String normalizeName(String name) {
final key = name.trim();
return _cakePayNames[key] ?? key;
}
static Country deserialize({required String raw}) => _all[raw]!;
static final Map<String, Country> countryByName = {
for (var country in _all.values) country.fullName: country,
};
static Country? fromCakePayName(String name) {
final normalizedName = _cakePayNames[name] ?? name;
return countryByName[normalizedName];
}
static String getCakePayName(Country country) {
return _cakePayNames.entries
.firstWhere(
(entry) => entry.value == country.fullName,
orElse: () => MapEntry(country.fullName, country.fullName),
)
.key;
}
static Country? fromCode(String countryCode) {
return _all.values.firstWhereOrNull((element) => element.raw == countryCode.toLowerCase());
}
static Country? fromCountryCode(String countryCode) {
return _all.values.firstWhereOrNull((element) => element.countryCode == countryCode.toUpperCase());
}
@override
bool operator ==(Object other) => other is Country && other.raw == raw;
+1
View File
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
class FeatureFlag {
static const bool isCakePayEnabled = false;
static const bool isCakePayPurchaseSimulationEnabled = true;
static const bool isCakePayRedemptionFlowEnabled = false;
static const bool isExolixEnabled = true;
static const bool isBackgroundSyncEnabled = true;
@@ -19,16 +19,21 @@ abstract class CakePayBuyCardViewModelBase with Store {
CakePayBuyCardViewModelBase(
{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)
amount = vendor.card!.denominationItems.isNotEmpty
? vendor.card!.denominationItems.first.value
: 0,
quantity = 1,
min = double.parse(vendor.card!.minValue ?? '0'),
max = double.parse(vendor.card!.maxValue ?? '0'),
card = vendor.card! {
card = vendor.card!,
min = _toDouble(vendor.card!.minValue) ?? 0,
max = _toDouble(vendor.card!.maxValue) ?? 0 {
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
}
static double? _toDouble(String? value) {
if (value == null || value.isEmpty) return null;
return double.tryParse(value.replaceAll(',', '.'));
}
final CakePayVendor vendor;
final SendViewModel sendViewModel;
final CakePayService _cakePayService;
@@ -44,10 +49,13 @@ abstract class CakePayBuyCardViewModelBase with Store {
bool confirmsNoVpn = false;
bool confirmsVoidedRefund = false;
bool confirmsTermsAgreed = false;
(String, int?) selectedDenomination = ('', null);
String simulatedResponse = '';
bool get isDenominationSelected => card.denominations.isNotEmpty;
bool get isDenominationSelected =>
card.denominationItems.isNotEmpty &&
card.denominationItems.any((item) => item.value == amount);
Future<bool> get isUserLogged async => await _cakePayService.isLogged();
@@ -80,7 +88,10 @@ abstract class CakePayBuyCardViewModelBase with Store {
double get totalAmount => amount * quantity;
@computed
bool get isSimulating => isSimulatingFlow && FeatureFlag.hasDevOptions;
bool get isSimulating =>
isSimulatingFlow &&
FeatureFlag.hasDevOptions &&
FeatureFlag.isCakePayPurchaseSimulationEnabled;
@computed
List<CakePayPaymentMethod> get availableMethods {
@@ -116,7 +127,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
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,
@@ -149,8 +159,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
}
try {
order = await _cakePayService.createOrder(
cardId: card.id,
price: amount.toString(),
cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
quantity: quantity,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
@@ -192,7 +202,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
try {
simulatedResponse = await _cakePayService.simulatePayment(orderId: order!.orderId);
sendViewModel.state = TransactionCommitted();
} catch (e) {
sendViewModel.state = FailureState(
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
@@ -231,8 +240,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
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')}';
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
void disposeExpirationTimer() {
@@ -22,7 +22,7 @@ abstract class CakePayCardsListViewModelBase with Store {
required this.settingsStore,
}) : cakePayVendors = [],
userCards = [],
availableCountries = [],
availableCountries = ObservableList<Country>.of(Country.allForCakePay),
page = 1,
displayPrepaidCards = true,
displayGiftCards = true,
@@ -33,8 +33,7 @@ abstract class CakePayCardsListViewModelBase with Store {
createCardState = CakePayCreateCardState(),
userCardState = UserCakePayCardsStateInitial(),
searchString = '',
searchMyCardsString = '',
CakePayVendorList = <CakePayVendor>[] {
searchMyCardsString = '' {
checkAuth();
initialization();
}
@@ -47,7 +46,6 @@ abstract class CakePayCardsListViewModelBase with Store {
}
void initialization() async {
await getCountries();
getVendors();
getUserCards();
}
@@ -55,8 +53,6 @@ abstract class CakePayCardsListViewModelBase with Store {
final CakePayService cakePayService;
final SettingsStore settingsStore;
List<CakePayVendor> CakePayVendorList;
Map<String, List<FilterItem>> get createFilterItems => {
'Card Type': [
FilterItem(
@@ -68,16 +64,17 @@ abstract class CakePayCardsListViewModelBase with Store {
caption: S.current.gift_cards,
onChanged: toggleGiftCards),
],
S.current.value_type: [
FilterItem(
value: () => displayDenominationsCards,
caption: S.current.denominations,
onChanged: toggleDenominationsCards),
FilterItem(
value: () => displayCustomValueCards,
caption: S.current.custom_value,
onChanged: toggleCustomValueCards),
],
// Uncomment if will be added to backend
// S.current.value_type: [
// FilterItem(
// value: () => displayDenominationsCards,
// caption: S.current.denominations,
// onChanged: toggleDenominationsCards),
// FilterItem(
// value: () => displayCustomValueCards,
// caption: S.current.custom_value,
// onChanged: toggleCustomValueCards),
// ],
};
String searchString;
@@ -165,14 +162,6 @@ abstract class CakePayCardsListViewModelBase with Store {
displayCustomValueCards != _initialDisplayCustomValueCards;
}
Future<void> getCountries() async {
try {
availableCountries = await cakePayService.getCountries();
} catch (e) {
printV(e);
}
}
Future<void> getUserCards() async {
//Dummy user cards // TODO: fetch from API
userCardState = UserCakePayCardsStateFetching();
@@ -195,9 +184,7 @@ abstract class CakePayCardsListViewModelBase with Store {
userCardState = UserCakePayCardsStateNoCards();
}
} catch (e) {
userCardState = UserCakePayCardsStateFailure(
error: e.toString(),
);
userCardState = UserCakePayCardsStateFailure(error: e.toString());
}
}
@@ -220,15 +207,17 @@ abstract class CakePayCardsListViewModelBase with Store {
try {
searchString = text ?? '';
var newVendors = await cakePayService.getVendors(
country: Country.getCakePayName(selectedCountry),
countryCode: selectedCountry.countryCode,
page: currentPage ?? page,
search: searchString,
giftCards: displayGiftCards,
prepaidCards: displayPrepaidCards,
custom: displayCustomValueCards,
onDemand: displayDenominationsCards);
prepaidCards: displayPrepaidCards);
cakePayVendors = CakePayVendorList = newVendors;
cakePayVendors = newVendors.where((vendor) {
if (vendor.card == null) return false;
if (vendor.available != true) return false;
return true;
}).toList();
} catch (e) {
printV(e);
}
@@ -245,15 +234,17 @@ abstract class CakePayCardsListViewModelBase with Store {
page++;
try {
var newVendors = await cakePayService.getVendors(
country: Country.getCakePayName(selectedCountry),
countryCode: selectedCountry.countryCode,
page: page,
search: searchString,
giftCards: displayGiftCards,
prepaidCards: displayPrepaidCards,
custom: displayCustomValueCards,
onDemand: displayDenominationsCards);
prepaidCards: displayPrepaidCards);
cakePayVendors.addAll(newVendors);
cakePayVendors.addAll(newVendors.where((vendor) {
if (vendor.card == null) return false;
if (vendor.available != true) return false;
return true;
}).toList());
} catch (error) {
if (error.toString().contains('detail":"Invalid page."')) {
hasMoreDataToFetch = false;