diff --git a/assets/images/flags/bfa.png b/assets/images/flags/bfa.png new file mode 100644 index 000000000..863106419 Binary files /dev/null and b/assets/images/flags/bfa.png differ diff --git a/assets/images/flags/bhs.png b/assets/images/flags/bhs.png new file mode 100644 index 000000000..f1bef3db5 Binary files /dev/null and b/assets/images/flags/bhs.png differ 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 9511238b2..db218ed5e 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 @@ -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( diff --git a/lib/cake_pay/src/cards/cake_pay_cards_page.dart b/lib/cake_pay/src/cards/cake_pay_cards_page.dart index 63320e1c4..adc850f55 100644 --- a/lib/cake_pay/src/cards/cake_pay_cards_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_cards_page.dart @@ -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; diff --git a/lib/cake_pay/src/models/cake_pay_card.dart b/lib/cake_pay/src/models/cake_pay_card.dart index 5d1c65a3b..f65d0d311 100644 --- a/lib/cake_pay/src/models/cake_pay_card.dart +++ b/lib/cake_pay/src/models/cake_pay_card.dart @@ -10,12 +10,11 @@ class CakePayCard { final String? cardImageUrl; final String? country; final FiatCurrency fiatCurrency; - final List denominationsUsd; - final List denominations; final String? minValueUsd; final String? maxValueUsd; final String? minValue; final String? maxValue; + final List 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? denominationItems, + }) : denominationItems = denominationItems ?? const []; factory CakePayCard.fromJson(Map 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 denominationsUsd = - (json['denominations_usd'] as List?)?.map((e) => e.toString()).toList() ?? []; - final List 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 = []; + for (final item in raw) { + if (item is Map) { + denominations.add(Denomination.fromJson(Map.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 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']), + ); + } +} diff --git a/lib/cake_pay/src/models/cake_pay_vendor.dart b/lib/cake_pay/src/models/cake_pay_vendor.dart index 8ad305da0..2f2f865d4 100644 --- a/lib/cake_pay/src/models/cake_pay_vendor.dart +++ b/lib/cake_pay/src/models/cake_pay_vendor.dart @@ -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 json, String country) { + factory CakePayVendor.fromJson(Map 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); - } catch (_) {} + cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) { + return Country.normalizeName(card['country'] as String) == country; + }) as Map); + } 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, diff --git a/lib/cake_pay/src/services/cake_pay_api.dart b/lib/cake_pay/src/services/cake_pay_api.dart index c61a068f1..f8e3849d8 100644 --- a/lib/cake_pay/src/services/cake_pay_api.dart +++ b/lib/cake_pay/src/services/cake_pay_api.dart @@ -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 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 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> 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((country) => country['name'] as String) - .map((name) => Country.fromCakePayName(name)) - .whereType() - .toList(); - } - /// Get Vendors Future> getVendors({ required String apiKey, - required String country, - int? page, - String? countryCode, + required int page, + required String countryCode, + String? country, String? search, List? 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, country)) + .map((e) => CakePayVendor.fromJson(e as Map)) .toList(); } } diff --git a/lib/cake_pay/src/services/cake_pay_service.dart b/lib/cake_pay/src/services/cake_pay_service.dart index d9c75b124..51a910d05 100644 --- a/lib/cake_pay/src/services/cake_pay_service.dart +++ b/lib/cake_pay/src/services/cake_pay_service.dart @@ -23,15 +23,11 @@ class CakePayService { final SecureStorage secureStorage; final CakePayApi cakePayApi; - /// Get Available Countries - Future> getCountries() async => - await cakePayApi.getCountries(apiKey: cakePayApiKey); - /// Get Vendors Future> getVendors({ - required String country, - int? page, - String? countryCode, + required int page, + required String countryCode, + String? country, String? search, List? vendorIds, bool? giftCards, @@ -114,6 +110,9 @@ class CakePayService { } ///Simulate Purchase Gift Card - Future simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment( - CSRFToken: CSRFToken, authorization: authorization, orderId: orderId); + Future simulatePayment({required String orderId}) async { + final token = (await secureStorage.read(key: cakePayUserTokenKey))!; + return await cakePayApi.simulatePayment( + CSRFToken: CSRFToken, authorization: authorization, token: token, orderId: orderId); + } } diff --git a/lib/cake_pay/src/widgets/denominations_amount_widget.dart b/lib/cake_pay/src/widgets/denominations_amount_widget.dart index 854c057ea..b4ab1e248 100644 --- a/lib/cake_pay/src/widgets/denominations_amount_widget.dart +++ b/lib/cake_pay/src/widgets/denominations_amount_widget.dart @@ -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 denominations; + final List 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 createState() => _DenominationsAmountWidgetState(); +} + +class _DenominationsAmountWidgetState extends State { + 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), + ), + ), + ], + ), + ), ], ), ); diff --git a/lib/entities/country.dart b/lib/entities/country.dart index 63eee9a18..3066d32c3 100644 --- a/lib/entities/country.dart +++ b/lib/entities/country.dart @@ -10,6 +10,9 @@ class Country extends EnumerableItem with Serializable { static List get all => _all.values.toList(); + static List 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 with Serializable { '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 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; diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 3dacd5fe1..b1aaec696 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -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; 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 1bbb257cd..382f794b9 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 @@ -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 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 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() { diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart index cbcfca858..80649ad9e 100644 --- a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart @@ -22,7 +22,7 @@ abstract class CakePayCardsListViewModelBase with Store { required this.settingsStore, }) : cakePayVendors = [], userCards = [], - availableCountries = [], + availableCountries = ObservableList.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 = [] { + 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 CakePayVendorList; - Map> 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 getCountries() async { - try { - availableCountries = await cakePayService.getCountries(); - } catch (e) { - printV(e); - } - } - Future 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;