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;