Cw 973 in app gift card redemption flow plus UI (#2285)

* refactor: cleanup cake_pay folder

* refactor: add My Cards tab on the main screen

* ui: add user card item

* refactor: cakePay purchase flow

* feat: add user card info

* refactor: auth flow

* refactor: theme data

* revert

* Update qr_widget.dart

* fix: update with new themes

* minor fixes

* feat: put redemption flow under a feature flag

* feat: implement simulated purchasing flow in debug mode

* feat: support LTC and MWEB payments

* Update savings_page.dart

* fix: UI fixes

* add Cake Pay transaction sent bottom sheet

* localization

* Update cake_pay_buy_card_page.dart

* addressing review comments

* fix marge conflicts
This commit is contained in:
Serhii
2025-07-11 23:56:35 +03:00
committed by GitHub
parent 203aff83f2
commit ef84db5874
96 changed files with 3907 additions and 2638 deletions
@@ -0,0 +1,74 @@
import 'package:cake_wallet/entities/fiat_currency.dart';
class CakePayCard {
final int id;
final String name;
final String? description;
final String? termsAndConditions;
final String? howToUse;
final String? expiryAndValidity;
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;
CakePayCard({
required this.id,
required this.name,
this.description,
this.termsAndConditions,
this.howToUse,
this.expiryAndValidity,
this.cardImageUrl,
this.country,
required this.fiatCurrency,
required this.denominationsUsd,
required this.denominations,
this.minValueUsd,
this.maxValueUsd,
this.minValue,
this.maxValue,
});
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 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() ?? [];
return CakePayCard(
id: json['id'] as int? ?? 0,
name: name,
description: description,
termsAndConditions: termsAndConditions,
howToUse: howToUse,
expiryAndValidity: json['expiry_and_validity'] as String?,
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: json['min_value'] as String?,
maxValue: json['max_value'] as String?,
);
}
static String stripHtmlIfNeeded(String text) {
return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
}
}
+157
View File
@@ -0,0 +1,157 @@
enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB }
extension CakePayPaymentMethodLabel on CakePayPaymentMethod {
String get label => switch (this) {
CakePayPaymentMethod.BTC => 'Bitcoin',
CakePayPaymentMethod.BTC_LN => 'Bitcoin Lightning',
CakePayPaymentMethod.XMR => 'Monero',
CakePayPaymentMethod.LTC => 'Litecoin',
CakePayPaymentMethod.LTC_MWEB => 'Litecoin MWEB',
};
}
class CakePayOrder {
final String orderId;
final List<OrderCard> cards;
final String? externalId;
final double amountUsd;
final String status;
final String? vouchers;
final PaymentData paymentData;
CakePayOrder({
required this.orderId,
required this.cards,
required this.externalId,
required this.amountUsd,
required this.status,
required this.vouchers,
required this.paymentData,
});
factory CakePayOrder.fromMap(Map<String, dynamic> map) {
return CakePayOrder(
orderId: map['order_id'] as String,
cards: (map['cards'] as List<dynamic>)
.map((x) => OrderCard.fromMap(x as Map<String, dynamic>))
.toList(),
externalId: map['external_id'] as String?,
amountUsd: map['amount_usd'] as double,
status: map['status'] as String,
vouchers: map['vouchers'] as String?,
paymentData: PaymentData.fromMap(map['payment_data'] as Map<String, dynamic>));
}
}
class OrderCard {
final int cardId;
final int? externalId;
final String price;
final int quantity;
final String currencyCode;
final String? cardName;
final String? cardImagePath;
OrderCard({
required this.cardId,
required this.externalId,
required this.price,
required this.quantity,
required this.currencyCode,
required this.cardName,
required this.cardImagePath,
});
factory OrderCard.fromMap(Map<String, dynamic> map) {
return OrderCard(
cardId: map['card_id'] as int,
externalId: map['external_id'] as int?,
price: map['price'] as String,
quantity: map['quantity'] as int,
currencyCode: map['currency_code'] as String,
cardName: map['name'] as String?,
cardImagePath: map['card_image_url'] as String?,
);
}
}
class PaymentData {
final CryptoPaymentData btc;
final CryptoPaymentData btc_ln;
final CryptoPaymentData xmr;
final CryptoPaymentData ltc;
final CryptoPaymentData ltc_mweb;
final DateTime invoiceTime;
final DateTime expirationTime;
final int? commission;
PaymentData({
required this.btc,
required this.btc_ln,
required this.xmr,
required this.ltc,
required this.ltc_mweb,
required this.invoiceTime,
required this.expirationTime,
required this.commission,
});
factory PaymentData.fromMap(Map<String, dynamic> map) {
return PaymentData(
btc: CryptoPaymentData.fromMap(map['BTC'] as Map<String, dynamic>),
btc_ln: CryptoPaymentData.fromMap(map['BTC_LN'] as Map<String, dynamic>),
xmr: CryptoPaymentData.fromMap(map['XMR'] as Map<String, dynamic>),
ltc: CryptoPaymentData.fromMap(map['LTC'] as Map<String, dynamic>),
ltc_mweb: CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map<String, dynamic>),
invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int),
expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int),
commission: map['commission'] as int?,
);
}
}
class CryptoPaymentData {
final String price;
final PaymentUrl? paymentUrls;
final String address;
CryptoPaymentData({
required this.price,
this.paymentUrls,
required this.address,
});
factory CryptoPaymentData.fromMap(Map<String, dynamic> map) {
return CryptoPaymentData(
price: map['price'] as String,
paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map<String, dynamic>?),
address: map['address'] as String,
);
}
}
class PaymentUrl {
final String? bip21;
final String? bip72;
final String? bip72b;
final String? bip73;
final String? bolt11;
PaymentUrl({
this.bip21,
this.bip72,
this.bip72b,
this.bip73,
this.bolt11,
});
factory PaymentUrl.fromMap(Map<String, dynamic>? map) {
return PaymentUrl(
bip21: map?['BIP21'] as String?,
bip72: map?['BIP72'] as String?,
bip72b: map?['BIP72b'] as String?,
bip73: map?['BIP73'] as String?,
bolt11: map?['BOLT11'] as String?,
);
}
}
@@ -0,0 +1,6 @@
class CakePayUserCredentials {
const CakePayUserCredentials(this.username, this.token);
final String username;
final String token;
}
@@ -0,0 +1,47 @@
import 'cake_pay_card.dart';
class CakePayVendor {
final int id;
final String name;
final bool unavailable;
final String? cakeWarnings;
final String country;
final CakePayCard? card;
CakePayVendor({
required this.id,
required this.name,
required this.unavailable,
this.cakeWarnings,
required this.country,
this.card,
});
factory CakePayVendor.fromJson(Map<String, dynamic> json, String country) {
final name = stripHtmlIfNeeded(json['name'] as String);
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 (_) {}
}
return CakePayVendor(
id: json['id'] as int,
name: name,
unavailable: json['unavailable'] as bool? ?? false,
cakeWarnings: json['cake_warnings'] as String?,
country: country,
card: cardForVendor,
);
}
static String stripHtmlIfNeeded(String text) {
return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
}
}