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>

Serhii committed Aug 13, 2025 at 09:07 UTC 328422cfb566630a49494fb9335dafc33aea7f8d
13 files changed +281 -214
assets/images/flags/bfa.png
Binary files /dev/null and b/assets/images/flags/bfa.png differ
assets/images/flags/bhs.png
Binary files /dev/null and b/assets/images/flags/bhs.png differ
lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+3 -3
@@ -157,10 +157,10 @@ class CakePayBuyCardPage extends BasePage {
157 ),
158 bottomCardChild: Padding(
159 padding: const EdgeInsets.symmetric(horizontal: 24),
160 - child: card.denominations.isNotEmpty
160 + child: card.denominationItems.isNotEmpty
161 ? DenominationsAmountWidget(
162 fiatCurrency: card.fiatCurrency.title,
163 - denominations: card.denominations,
163 + denominations: card.denominationItems,
164 amountFieldFocus: _amountFieldFocus,
165 amountController: _amountController,
166 quantityFieldFocus: _quantityFieldFocus,
@@ -335,7 +335,7 @@ class CakePayBuyCardPage extends BasePage {
335 ),
336 ),
337 ),
338 - if (FeatureFlag.hasDevOptions)
338 + if (FeatureFlag.hasDevOptions && FeatureFlag.isCakePayPurchaseSimulationEnabled)
339 Padding(
340 padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
341 child: LoadingPrimaryButton(
lib/cake_pay/src/cards/cake_pay_cards_page.dart
+2 -2
@@ -303,7 +303,7 @@ class _MyCardsTabState extends State<_MyCardsTab> {
303 final cards = viewModel.filteredUserCards;
304 if (viewModel.userCardState is UserCakePayCardsStateFetching) return const _Loading();
305 if (viewModel.userCardState is UserCakePayCardsStateNoCards)
306 - return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
306 + return Center(child: Text(S.of(context).no_cards_found));
307
308 final showThumb = cards.length > 6;
309 final userCardsList = Stack(
@@ -467,7 +467,7 @@ class _ShopTabState extends State<_ShopTab> {
467 }
468
469 if (vendors.isEmpty)
470 - return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
470 + return Center(child: Text(S.of(context).no_cards_found));
471
472 final loadingMore = viewModel.isLoadingNextPage;
473 final showThumb = vendors.length > 3;
lib/cake_pay/src/models/cake_pay_card.dart
+46 -17
@@ -10,12 +10,11 @@ class CakePayCard {
10 final String? cardImageUrl;
11 final String? country;
12 final FiatCurrency fiatCurrency;
13 - final List<String> denominationsUsd;
14 - final List<String> denominations;
13 final String? minValueUsd;
14 final String? maxValueUsd;
15 final String? minValue;
16 final String? maxValue;
17 + final List<Denomination> denominationItems;
18
19 CakePayCard({
20 required this.id,
@@ -27,31 +26,37 @@ class CakePayCard {
26 this.cardImageUrl,
27 this.country,
28 required this.fiatCurrency,
30 - required this.denominationsUsd,
31 - required this.denominations,
29 this.minValueUsd,
30 this.maxValueUsd,
31 this.minValue,
32 this.maxValue,
36 - });
33 + List<Denomination>? denominationItems,
34 + }) : denominationItems = denominationItems ?? const [];
35
36 factory CakePayCard.fromJson(Map<String, dynamic> json) {
39 -
37 final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
38 final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
39 final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
40 final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
44 -
41 final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? '');
46 - final parsedMinValue = _toDouble(json['min_value'] as String?);
47 - final minValue = fiatCurrency == FiatCurrency.usd && parsedMinValue != null && parsedMinValue < 10.00
48 - ? '10.00'
49 - : json['min_value'] as String?;
42 + String? minValue = json['min_value'] as String?;
43 +
44 + final parsedMinValueLocal = _toDouble(json['min_value']);
45 + final parsedMinValueUsd = _toDouble(json['min_value_usd']);
46 +
47 + if (parsedMinValueLocal != null && parsedMinValueLocal > 0 && parsedMinValueUsd != null && parsedMinValueUsd > 0 && parsedMinValueUsd < 10.0) {
48 + final rate = parsedMinValueLocal / parsedMinValueUsd;
49 + final minLocalValueLimit = 10.0 * rate;
50 + minValue = minLocalValueLimit.toStringAsFixed(2);
51 + }
52
51 - final List<String> denominationsUsd =
52 - (json['denominations_usd'] as List?)?.map((e) => e.toString()).toList() ?? [];
53 - final List<String> denominations =
54 - (json['denominations'] as List?)?.map((e) => e.toString()).toList() ?? [];
53 + final raw = (json['denominations'] as List?) ?? const [];
54 + final denominations = <Denomination>[];
55 + for (final item in raw) {
56 + if (item is Map) {
57 + denominations.add(Denomination.fromJson(Map<String, dynamic>.from(item)));
58 + }
59 + }
60
61 return CakePayCard(
62 id: json['id'] as int? ?? 0,
@@ -63,22 +68,46 @@ class CakePayCard {
68 cardImageUrl: json['card_image_url'] as String?,
69 country: json['country'] as String?,
70 fiatCurrency: fiatCurrency,
66 - denominationsUsd: denominationsUsd,
67 - denominations: denominations,
71 minValueUsd: json['min_value_usd'] as String?,
72 maxValueUsd: json['max_value_usd'] as String?,
73 minValue: minValue,
74 maxValue: json['max_value'] as String?,
75 + denominationItems: denominations,
76 );
77 }
78
79 static String stripHtmlIfNeeded(String text) {
80 return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
81 }
82 + static double? _toDouble(dynamic v) {
83 + if (v == null) return null;
84 + if (v is num) return v.toDouble();
85 + return double.tryParse(v.toString());
86 + }
87 +}
88 +
89 +class Denomination {
90 + final double value;
91 + final int? cardId;
92 + final double? usdValue;
93 +
94 + Denomination({
95 + required this.value,
96 + required this.cardId,
97 + this.usdValue,
98 + });
99
100 static double? _toDouble(dynamic v) {
101 if (v == null) return null;
102 if (v is num) return v.toDouble();
103 return double.tryParse(v.toString());
104 }
105 +
106 + factory Denomination.fromJson(Map<String, dynamic> json) {
107 + return Denomination(
108 + value: _toDouble(json['value']) ?? 0,
109 + cardId: json['card_id'] is int ? json['card_id'] as int : int.tryParse('${json['card_id']}'),
110 + usdValue: _toDouble(json['usd_value']),
111 + );
112 + }
113 }
lib/cake_pay/src/models/cake_pay_vendor.dart
+16 -8
@@ -1,9 +1,12 @@
1 +import 'package:cake_wallet/entities/country.dart';
2 +import 'package:cw_core/utils/print_verbose.dart';
3 +
4 import 'cake_pay_card.dart';
5
6 class CakePayVendor {
7 final int id;
8 final String name;
6 - final bool unavailable;
9 + final bool available;
10 final String? cakeWarnings;
11 final String country;
12 final CakePayCard? card;
@@ -11,30 +14,35 @@ class CakePayVendor {
14 CakePayVendor({
15 required this.id,
16 required this.name,
14 - required this.unavailable,
17 + required this.available,
18 this.cakeWarnings,
19 required this.country,
20 this.card,
21 });
22
20 - factory CakePayVendor.fromJson(Map<String, dynamic> json, String country) {
23 + factory CakePayVendor.fromJson(Map<String, dynamic> json) {
24 final name = stripHtmlIfNeeded(json['name'] as String);
25
26 + final parsedCountry = json['country'] as String;
27 + final country = Country.normalizeName(parsedCountry);
28 +
29 var cardsJson = json['cards'] as List?;
30 CakePayCard? cardForVendor;
31
32 if (cardsJson != null && cardsJson.isNotEmpty) {
33 try {
28 - cardForVendor = CakePayCard.fromJson(cardsJson
29 - .where((element) => element['country'] == country)
30 - .first as Map<String, dynamic>);
31 - } catch (_) {}
34 + cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
35 + return Country.normalizeName(card['country'] as String) == country;
36 + }) as Map<String, dynamic>);
37 + } catch (e) {
38 + printV('Error parsing card for vendor: $e');
39 + }
40 }
41
42 return CakePayVendor(
43 id: json['id'] as int,
44 name: name,
37 - unavailable: json['unavailable'] as bool? ?? false,
45 + available: json['available'] as bool? ?? false,
46 cakeWarnings: json['cake_warnings'] as String?,
47 country: country,
48 card: cardForVendor,
lib/cake_pay/src/services/cake_pay_api.dart
+42 -56
@@ -3,25 +3,24 @@ import 'dart:convert';
3 import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
4 import 'package:cake_wallet/cake_pay/src/models/cake_pay_user_credentials.dart';
5 import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
6 +import 'package:cake_wallet/utils/feature_flag.dart';
7 import 'package:cw_core/utils/proxy_wrapper.dart';
8 import 'package:cw_core/utils/print_verbose.dart';
8 -import 'package:cake_wallet/entities/country.dart';
9
10 class CakePayApi {
11 - static const testBaseUri = false;
11 + static const testBaseUri = FeatureFlag.hasDevOptions;
12
13 - static const baseTestCakePayUri = 'test.cakepay.com';
14 - static const baseProdCakePayUri = 'buy.cakepay.com';
13 + static const baseTestCakePayUri = 'api-stg.cakepay.com';
14 + static const baseProdCakePayUri = 'api-prod.cakepay.com';
15
16 static const baseCakePayUri = testBaseUri ? baseTestCakePayUri : baseProdCakePayUri;
17
18 - static const vendorsPath = '/api/vendors';
19 - static const countriesPath = '/api/countries';
20 - static const authPath = '/api/auth';
21 - static final verifyEmailPath = '/api/verify';
22 - static final logoutPath = '/api/logout';
23 - static final createOrderPath = '/api/order';
24 - static final simulatePaymentPath = '/api/simulate_payment';
18 + static const vendorsPath = '/api/marketplace/vendors';
19 + static const authPath = '/api/accounts/auth';
20 + static final verifyEmailPath = '/api/accounts/auth/verify';
21 + static final logoutPath = '/api/accounts/logout';
22 + static final createOrderPath = '/api/orders/order';
23 + static final simulatePaymentPath = '/api/orders/simulate-payment';
24
25 /// AuthenticateUser
26 Future<String> authenticateUser({required String email, required String apiKey}) async {
@@ -110,7 +109,7 @@ class CakePayApi {
109 final headers = {
110 'Accept': 'application/json',
111 'Content-Type': 'application/json',
113 - 'Authorization': 'Api-Key $apiKey',
112 + 'Authorization': 'Token $token',
113 };
114
115 final body = json.encode({
@@ -118,7 +117,6 @@ class CakePayApi {
117 'price': price,
118 'quantity': quantity,
119 'user_email': userEmail,
121 - 'token': token,
120 'send_email': true,
121 'confirms_no_vpn': confirmsNoVpn,
122 'confirms_voided_refund': confirmsVoidedRefund,
@@ -159,16 +157,27 @@ class CakePayApi {
157
158 ///Simulate Payment
159 Future<String> simulatePayment(
162 - {required String CSRFToken, required String authorization, required String orderId}) async {
163 - final uri = Uri.https(baseCakePayUri, simulatePaymentPath + '/$orderId');
160 + {required String CSRFToken,
161 + required String authorization,
162 + required String orderId,
163 + required String token}) async {
164 + final uri = Uri.https(baseCakePayUri, simulatePaymentPath);
165
166 final headers = {
166 - 'accept': 'application/json',
167 - 'authorization': authorization,
168 - 'X-CSRFToken': CSRFToken,
167 + 'Accept': 'application/json',
168 + 'Content-Type': 'application/json',
169 + 'Authorization': 'Token $token',
170 };
171
171 - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
172 + final body = json.encode({
173 + 'order_id': orderId,
174 + });
175 +
176 + final response = await ProxyWrapper().post(
177 + clearnetUri: uri,
178 + headers: headers,
179 + body: body,
180 + );
181
182 printV('Response: ${response.statusCode}');
183
@@ -205,54 +214,31 @@ class CakePayApi {
214 }
215 }
216
208 - /// Get Countries
209 - Future<List<Country>> getCountries({required String apiKey}) async {
210 - final uri = Uri.https(baseCakePayUri, countriesPath);
211 -
212 - final headers = {
213 - 'accept': 'application/json',
214 - 'Authorization': 'Api-Key $apiKey',
215 - };
216 -
217 - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
218 -
219 - if (response.statusCode != 200) {
220 - throw Exception('Unexpected http status: ${response.statusCode}');
221 - }
222 -
223 - final bodyJson = json.decode(response.body) as List;
224 - return bodyJson
225 - .map<String>((country) => country['name'] as String)
226 - .map((name) => Country.fromCakePayName(name))
227 - .whereType<Country>()
228 - .toList();
229 - }
230 -
217 /// Get Vendors
218 Future<List<CakePayVendor>> getVendors({
219 required String apiKey,
234 - required String country,
235 - int? page,
236 - String? countryCode,
220 + required int page,
221 + required String countryCode,
222 + String? country,
223 String? search,
224 List<String>? vendorIds,
239 - bool? giftCards,
240 - bool? prepaidCards,
225 + bool? giftCards = true,
226 + bool? prepaidCards = true,
227 bool? onDemand,
228 bool? custom,
229 }) async {
230 var queryParams = {
245 - 'page': page?.toString(),
246 - 'country': country,
231 + 'page': page.toString(),
232 'country_code': countryCode,
248 - 'search': search,
249 - 'vendor_ids': vendorIds?.join(','),
250 - 'gift_cards': giftCards?.toString(),
251 - 'prepaid_cards': prepaidCards?.toString(),
252 - 'on_demand': onDemand?.toString(),
253 - 'custom': custom?.toString(),
233 + if (search != null && search.isNotEmpty) 'search': search,
234 + if (vendorIds != null && vendorIds.isNotEmpty) 'vendor_ids': vendorIds.join(','),
235 };
236
237 + if (giftCards == false || prepaidCards == false) {
238 + queryParams['gift_cards'] = giftCards.toString();
239 + queryParams['prepaid_cards'] = prepaidCards.toString();
240 + }
241 +
242 final uri = Uri.https(baseCakePayUri, vendorsPath, queryParams);
243
244 var headers = {
@@ -275,7 +261,7 @@ class CakePayApi {
261 }
262
263 return (bodyJson['results'] as List)
278 - .map((e) => CakePayVendor.fromJson(e as Map<String, dynamic>, country))
264 + .map((e) => CakePayVendor.fromJson(e as Map<String, dynamic>))
265 .toList();
266 }
267 }
lib/cake_pay/src/services/cake_pay_service.dart
+8 -9
@@ -23,15 +23,11 @@ class CakePayService {
23 final SecureStorage secureStorage;
24 final CakePayApi cakePayApi;
25
26 - /// Get Available Countries
27 - Future<List<Country>> getCountries() async =>
28 - await cakePayApi.getCountries(apiKey: cakePayApiKey);
29 -
26 /// Get Vendors
27 Future<List<CakePayVendor>> getVendors({
32 - required String country,
33 - int? page,
34 - String? countryCode,
28 + required int page,
29 + required String countryCode,
30 + String? country,
31 String? search,
32 List<String>? vendorIds,
33 bool? giftCards,
@@ -114,6 +110,9 @@ class CakePayService {
110 }
111
112 ///Simulate Purchase Gift Card
117 - Future<String> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
118 - CSRFToken: CSRFToken, authorization: authorization, orderId: orderId);
113 + Future<String> simulatePayment({required String orderId}) async {
114 + final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
115 + return await cakePayApi.simulatePayment(
116 + CSRFToken: CSRFToken, authorization: authorization, token: token, orderId: orderId);
117 + }
118 }
lib/cake_pay/src/widgets/denominations_amount_widget.dart
+102 -52
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
4 import 'package:cake_wallet/typography.dart';
@@ -6,20 +7,21 @@ import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dar
7 import 'package:flutter/material.dart';
8 import 'package:flutter_mobx/flutter_mobx.dart';
9
9 -class DenominationsAmountWidget extends StatelessWidget {
10 - const DenominationsAmountWidget(
11 - {required this.fiatCurrency,
12 - required this.denominations,
13 - required this.amountFieldFocus,
14 - required this.amountController,
15 - required this.quantityFieldFocus,
16 - required this.quantityController,
17 - required this.cakePayBuyCardViewModel,
18 - required this.onAmountChanged,
19 - required this.onQuantityChanged});
10 +class DenominationsAmountWidget extends StatefulWidget {
11 + const DenominationsAmountWidget({
12 + required this.fiatCurrency,
13 + required this.denominations,
14 + required this.amountFieldFocus,
15 + required this.amountController,
16 + required this.quantityFieldFocus,
17 + required this.quantityController,
18 + required this.cakePayBuyCardViewModel,
19 + required this.onAmountChanged,
20 + required this.onQuantityChanged,
21 + });
22
23 final String fiatCurrency;
22 - final List<String> denominations;
24 + final List<Denomination> denominations;
25 final FocusNode amountFieldFocus;
26 final TextEditingController amountController;
27 final FocusNode quantityFieldFocus;
@@ -28,6 +30,28 @@ class DenominationsAmountWidget extends StatelessWidget {
30 final Function(String) onAmountChanged;
31 final Function(int?) onQuantityChanged;
32
33 + @override
34 + State<DenominationsAmountWidget> createState() => _DenominationsAmountWidgetState();
35 +}
36 +
37 +class _DenominationsAmountWidgetState extends State<DenominationsAmountWidget> {
38 + late (String, int?) _selected;
39 +
40 + @override
41 + void initState() {
42 + super.initState();
43 +
44 + final first = widget.denominations.first;
45 + final amount = widget.amountController.text.isNotEmpty
46 + ? widget.amountController.text
47 + : first.value.toString();
48 + _selected = (amount, first.cardId);
49 + widget.cakePayBuyCardViewModel.selectedDenomination = _selected;
50 +
51 + widget.amountController.text = _selected.$1;
52 + widget.onAmountChanged(_selected.$1);
53 + }
54 +
55 @override
56 Widget build(BuildContext context) {
57 return Container(
@@ -41,24 +65,36 @@ class DenominationsAmountWidget extends StatelessWidget {
65 mainAxisSize: MainAxisSize.min,
66 children: [
67 DropdownFilterList(
44 - items: denominations,
45 - itemPrefix: fiatCurrency,
46 - selectedItem: denominations.first,
47 - onItemSelected: (value) {
48 - amountController.text = value;
49 - onAmountChanged(value);
50 - }),
68 + items: widget.denominations
69 + .map((e) => e.value.toString())
70 + .toList(),
71 + itemPrefix: widget.fiatCurrency,
72 + selectedItem: _selected.$1,
73 + onItemSelected: (value) {
74 + setState(() => _selected = (value, widget.denominations
75 + .firstWhere((e) => e.value.toString() == value)
76 + .cardId));
77 + widget.amountController.text = value;
78 + widget.onAmountChanged(value);
79 + widget.cakePayBuyCardViewModel.selectedDenomination = (_selected.$1, _selected.$2);
80 + },
81 + ),
82 const SizedBox(height: 4),
83 Container(
84 width: double.infinity,
85 decoration: BoxDecoration(
86 border: Border(
56 - top: BorderSide(
57 - width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant)),
87 + top: BorderSide(
88 + width: 1.0,
89 + color: Theme.of(context).colorScheme.onSurfaceVariant,
90 + ),
91 + ),
92 + ),
93 + child: Text(
94 + S.of(context).value,
95 + maxLines: 2,
96 + style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
97 ),
59 - child: Text(S.of(context).value,
60 - maxLines: 2,
61 - style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
98 ),
99 ],
100 ),
@@ -70,51 +106,65 @@ class DenominationsAmountWidget extends StatelessWidget {
106 mainAxisSize: MainAxisSize.min,
107 children: [
108 NumberTextField(
73 - controller: quantityController,
74 - focusNode: quantityFieldFocus,
75 - min: 1,
76 - max: 99,
77 - onChanged: (value) => onQuantityChanged(value)),
109 + controller: widget.quantityController,
110 + focusNode: widget.quantityFieldFocus,
111 + min: 1,
112 + max: 99,
113 + onChanged: (value) => widget.onQuantityChanged(value),
114 + ),
115 const SizedBox(height: 4),
116 Container(
117 width: double.infinity,
118 decoration: BoxDecoration(
119 border: Border(
120 top: BorderSide(
84 - width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
121 + width: 1.0,
122 + color: Theme.of(context).colorScheme.onSurfaceVariant,
123 + ),
124 ),
125 ),
87 - child: Text(S.of(context).quantity,
88 - maxLines: 1,
89 - style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
126 + child: Text(
127 + S.of(context).quantity,
128 + maxLines: 1,
129 + style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
130 + ),
131 ),
132 ],
133 ),
134 ),
135 Spacer(),
136 Expanded(
96 - flex: 8,
97 - child: Column(
98 - mainAxisSize: MainAxisSize.min,
99 - children: [
100 - Observer(
101 - builder: (_) => Text('$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
102 - maxLines: 1, style: Theme.of(context).textTheme.titleMedium!)),
103 - const SizedBox(height: 4),
104 - Container(
105 - width: double.infinity,
106 - decoration: BoxDecoration(
107 - border: Border(
108 - top: BorderSide(
109 - width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
137 + flex: 8,
138 + child: Column(
139 + mainAxisSize: MainAxisSize.min,
140 + children: [
141 + Observer(
142 + builder: (_) => Text(
143 + '${widget.fiatCurrency} ${widget.cakePayBuyCardViewModel.totalAmount}',
144 + maxLines: 1,
145 + style: Theme.of(context).textTheme.titleMedium!,
146 + ),
147 + ),
148 + const SizedBox(height: 4),
149 + Container(
150 + width: double.infinity,
151 + decoration: BoxDecoration(
152 + border: Border(
153 + top: BorderSide(
154 + width: 1.0,
155 + color: Theme.of(context).colorScheme.onSurfaceVariant,
156 ),
157 ),
112 - child: Text(S.of(context).total,
113 - maxLines: 1,
114 - style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
158 ),
116 - ],
117 - )),
159 + child: Text(
160 + S.of(context).total,
161 + maxLines: 1,
162 + style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
163 + ),
164 + ),
165 + ],
166 + ),
167 + ),
168 ],
169 ),
170 );
lib/entities/country.dart
+12 -17
@@ -10,6 +10,9 @@ class Country extends EnumerableItem<String> with Serializable<String> {
10
11 static List<Country> get all => _all.values.toList();
12
13 + static List<Country> get allForCakePay => _all.values
14 + .where((element) => element.countryCode != 'EU' && element.countryCode != 'AQ').toList();
15 +
16 static const afghanistan = Country(code: 'afg', countryCode: 'AF', fullName: "Afghanistan");
17 static const andorra = Country(code: 'and', countryCode: 'AD', fullName: "Andorra");
18 static const angola = Country(code: 'ago', countryCode: 'AO', fullName: "Angola");
@@ -352,30 +355,22 @@ class Country extends EnumerableItem<String> with Serializable<String> {
355 'Curaçao': "Curacao",
356 };
357
355 - static Country deserialize({required String raw}) => _all[raw]!;
356 -
357 - static final Map<String, Country> countryByName = {
358 - for (var country in _all.values) country.fullName: country,
359 - };
360 -
361 - static Country? fromCakePayName(String name) {
362 - final normalizedName = _cakePayNames[name] ?? name;
363 - return countryByName[normalizedName];
358 + static String normalizeName(String name) {
359 + final key = name.trim();
360 + return _cakePayNames[key] ?? key;
361 }
362
366 - static String getCakePayName(Country country) {
367 - return _cakePayNames.entries
368 - .firstWhere(
369 - (entry) => entry.value == country.fullName,
370 - orElse: () => MapEntry(country.fullName, country.fullName),
371 - )
372 - .key;
373 - }
363 + static Country deserialize({required String raw}) => _all[raw]!;
364
365 static Country? fromCode(String countryCode) {
366 return _all.values.firstWhereOrNull((element) => element.raw == countryCode.toLowerCase());
367 }
368
369 +
370 + static Country? fromCountryCode(String countryCode) {
371 + return _all.values.firstWhereOrNull((element) => element.countryCode == countryCode.toUpperCase());
372 + }
373 +
374 @override
375 bool operator ==(Object other) => other is Country && other.raw == raw;
376
lib/utils/feature_flag.dart
+1
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
4
5 class FeatureFlag {
6 static const bool isCakePayEnabled = false;
7 + static const bool isCakePayPurchaseSimulationEnabled = true;
8 static const bool isCakePayRedemptionFlowEnabled = false;
9 static const bool isExolixEnabled = true;
10 static const bool isBackgroundSyncEnabled = true;
lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart
+21 -13
@@ -19,16 +19,21 @@ abstract class CakePayBuyCardViewModelBase with Store {
19 CakePayBuyCardViewModelBase(
20 {required this.vendor, required CakePayService cakePayService, required this.sendViewModel})
21 : _cakePayService = cakePayService, walletType = sendViewModel.walletType,
22 - amount = vendor.card!.denominations.isNotEmpty
23 - ? double.parse(vendor.card!.denominations.first)
22 + amount = vendor.card!.denominationItems.isNotEmpty
23 + ? vendor.card!.denominationItems.first.value
24 : 0,
25 quantity = 1,
26 - min = double.parse(vendor.card!.minValue ?? '0'),
27 - max = double.parse(vendor.card!.maxValue ?? '0'),
28 - card = vendor.card! {
26 + card = vendor.card!,
27 + min = _toDouble(vendor.card!.minValue) ?? 0,
28 + max = _toDouble(vendor.card!.maxValue) ?? 0 {
29 selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
30 }
31
32 + static double? _toDouble(String? value) {
33 + if (value == null || value.isEmpty) return null;
34 + return double.tryParse(value.replaceAll(',', '.'));
35 + }
36 +
37 final CakePayVendor vendor;
38 final SendViewModel sendViewModel;
39 final CakePayService _cakePayService;
@@ -44,10 +49,13 @@ abstract class CakePayBuyCardViewModelBase with Store {
49 bool confirmsNoVpn = false;
50 bool confirmsVoidedRefund = false;
51 bool confirmsTermsAgreed = false;
52 + (String, int?) selectedDenomination = ('', null);
53
54 String simulatedResponse = '';
55
50 - bool get isDenominationSelected => card.denominations.isNotEmpty;
56 + bool get isDenominationSelected =>
57 + card.denominationItems.isNotEmpty &&
58 + card.denominationItems.any((item) => item.value == amount);
59
60 Future<bool> get isUserLogged async => await _cakePayService.isLogged();
61
@@ -80,7 +88,10 @@ abstract class CakePayBuyCardViewModelBase with Store {
88 double get totalAmount => amount * quantity;
89
90 @computed
83 - bool get isSimulating => isSimulatingFlow && FeatureFlag.hasDevOptions;
91 + bool get isSimulating =>
92 + isSimulatingFlow &&
93 + FeatureFlag.hasDevOptions &&
94 + FeatureFlag.isCakePayPurchaseSimulationEnabled;
95
96 @computed
97 List<CakePayPaymentMethod> get availableMethods {
@@ -116,7 +127,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
127 CryptoPaymentData? getPaymentDataFor(CakePayPaymentMethod? method) {
128 if (order == null || method == null) return null;
129
119 -
130 final data = switch (method) {
131 CakePayPaymentMethod.BTC => order?.paymentData.btc,
132 CakePayPaymentMethod.XMR => order?.paymentData.xmr,
@@ -149,8 +159,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
159 }
160 try {
161 order = await _cakePayService.createOrder(
152 - cardId: card.id,
153 - price: amount.toString(),
162 + cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
163 + price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
164 quantity: quantity,
165 confirmsNoVpn: confirmsNoVpn,
166 confirmsVoidedRefund: confirmsVoidedRefund,
@@ -192,7 +202,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
202 try {
203 simulatedResponse = await _cakePayService.simulatePayment(orderId: order!.orderId);
204 sendViewModel.state = TransactionCommitted();
195 -
205 } catch (e) {
206 sendViewModel.state = FailureState(
207 sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
@@ -231,8 +240,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
240 final hours = duration.inHours;
241 final minutes = duration.inMinutes.remainder(60);
242 final seconds = duration.inSeconds.remainder(60);
234 - return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds
235 - .toString().padLeft(2, '0')}';
243 + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
244 }
245
246 void disposeExpirationTimer() {
lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart
+28 -37
@@ -22,7 +22,7 @@ abstract class CakePayCardsListViewModelBase with Store {
22 required this.settingsStore,
23 }) : cakePayVendors = [],
24 userCards = [],
25 - availableCountries = [],
25 + availableCountries = ObservableList<Country>.of(Country.allForCakePay),
26 page = 1,
27 displayPrepaidCards = true,
28 displayGiftCards = true,
@@ -33,8 +33,7 @@ abstract class CakePayCardsListViewModelBase with Store {
33 createCardState = CakePayCreateCardState(),
34 userCardState = UserCakePayCardsStateInitial(),
35 searchString = '',
36 - searchMyCardsString = '',
37 - CakePayVendorList = <CakePayVendor>[] {
36 + searchMyCardsString = '' {
37 checkAuth();
38 initialization();
39 }
@@ -47,7 +46,6 @@ abstract class CakePayCardsListViewModelBase with Store {
46 }
47
48 void initialization() async {
50 - await getCountries();
49 getVendors();
50 getUserCards();
51 }
@@ -55,8 +53,6 @@ abstract class CakePayCardsListViewModelBase with Store {
53 final CakePayService cakePayService;
54 final SettingsStore settingsStore;
55
58 - List<CakePayVendor> CakePayVendorList;
59 -
56 Map<String, List<FilterItem>> get createFilterItems => {
57 'Card Type': [
58 FilterItem(
@@ -68,16 +64,17 @@ abstract class CakePayCardsListViewModelBase with Store {
64 caption: S.current.gift_cards,
65 onChanged: toggleGiftCards),
66 ],
71 - S.current.value_type: [
72 - FilterItem(
73 - value: () => displayDenominationsCards,
74 - caption: S.current.denominations,
75 - onChanged: toggleDenominationsCards),
76 - FilterItem(
77 - value: () => displayCustomValueCards,
78 - caption: S.current.custom_value,
79 - onChanged: toggleCustomValueCards),
80 - ],
67 + // Uncomment if will be added to backend
68 + // S.current.value_type: [
69 + // FilterItem(
70 + // value: () => displayDenominationsCards,
71 + // caption: S.current.denominations,
72 + // onChanged: toggleDenominationsCards),
73 + // FilterItem(
74 + // value: () => displayCustomValueCards,
75 + // caption: S.current.custom_value,
76 + // onChanged: toggleCustomValueCards),
77 + // ],
78 };
79
80 String searchString;
@@ -165,14 +162,6 @@ abstract class CakePayCardsListViewModelBase with Store {
162 displayCustomValueCards != _initialDisplayCustomValueCards;
163 }
164
168 - Future<void> getCountries() async {
169 - try {
170 - availableCountries = await cakePayService.getCountries();
171 - } catch (e) {
172 - printV(e);
173 - }
174 - }
175 -
165 Future<void> getUserCards() async {
166 //Dummy user cards // TODO: fetch from API
167 userCardState = UserCakePayCardsStateFetching();
@@ -195,9 +184,7 @@ abstract class CakePayCardsListViewModelBase with Store {
184 userCardState = UserCakePayCardsStateNoCards();
185 }
186 } catch (e) {
198 - userCardState = UserCakePayCardsStateFailure(
199 - error: e.toString(),
200 - );
187 + userCardState = UserCakePayCardsStateFailure(error: e.toString());
188 }
189 }
190
@@ -220,15 +207,17 @@ abstract class CakePayCardsListViewModelBase with Store {
207 try {
208 searchString = text ?? '';
209 var newVendors = await cakePayService.getVendors(
223 - country: Country.getCakePayName(selectedCountry),
210 + countryCode: selectedCountry.countryCode,
211 page: currentPage ?? page,
212 search: searchString,
213 giftCards: displayGiftCards,
227 - prepaidCards: displayPrepaidCards,
228 - custom: displayCustomValueCards,
229 - onDemand: displayDenominationsCards);
214 + prepaidCards: displayPrepaidCards);
215
231 - cakePayVendors = CakePayVendorList = newVendors;
216 + cakePayVendors = newVendors.where((vendor) {
217 + if (vendor.card == null) return false;
218 + if (vendor.available != true) return false;
219 + return true;
220 + }).toList();
221 } catch (e) {
222 printV(e);
223 }
@@ -245,15 +234,17 @@ abstract class CakePayCardsListViewModelBase with Store {
234 page++;
235 try {
236 var newVendors = await cakePayService.getVendors(
248 - country: Country.getCakePayName(selectedCountry),
237 + countryCode: selectedCountry.countryCode,
238 page: page,
239 search: searchString,
240 giftCards: displayGiftCards,
252 - prepaidCards: displayPrepaidCards,
253 - custom: displayCustomValueCards,
254 - onDemand: displayDenominationsCards);
241 + prepaidCards: displayPrepaidCards);
242
256 - cakePayVendors.addAll(newVendors);
243 + cakePayVendors.addAll(newVendors.where((vendor) {
244 + if (vendor.card == null) return false;
245 + if (vendor.available != true) return false;
246 + return true;
247 + }).toList());
248 } catch (error) {
249 if (error.toString().contains('detail":"Invalid page."')) {
250 hasMoreDataToFetch = false;