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