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
+281
View File
@@ -0,0 +1,281 @@
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: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 baseTestCakePayUri = 'test.cakepay.com';
static const baseProdCakePayUri = 'buy.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';
/// AuthenticateUser
Future<String> authenticateUser({required String email, required String apiKey}) async {
try {
final uri = Uri.https(baseCakePayUri, authPath);
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Api-Key $apiKey',
};
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode({'email': email}),
);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) {
return bodyJson['user']['email'] as String;
}
throw Exception('Failed to authenticate user with error: $bodyJson');
} catch (e) {
throw Exception('Failed to authenticate user with error: $e');
}
}
/// Verify email
Future<CakePayUserCredentials> verifyEmail({
required String email,
required String code,
required String apiKey,
}) async {
final uri = Uri.https(baseCakePayUri, verifyEmailPath);
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Api-Key $apiKey',
};
final query = <String, String>{'email': email, 'otp': code};
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(query),
);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
if (bodyJson.containsKey('error')) {
throw Exception(bodyJson['error'] as String);
}
if (bodyJson.containsKey('token')) {
final token = bodyJson['token'] as String;
final userEmail = bodyJson['user']['email'] as String;
return CakePayUserCredentials(userEmail, token);
} else {
throw Exception('E-mail verification failed.');
}
}
/// createOrder
Future<CakePayOrder> createOrder({
required String apiKey,
required int cardId,
required String price,
required int quantity,
required String userEmail,
required String token,
required bool confirmsNoVpn,
required bool confirmsVoidedRefund,
required bool confirmsTermsAgreed,
}) async {
final uri = Uri.https(baseCakePayUri, createOrderPath);
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Api-Key $apiKey',
};
final body = json.encode({
'card_id': cardId,
'price': price,
'quantity': quantity,
'user_email': userEmail,
'token': token,
'send_email': true,
'confirms_no_vpn': confirmsNoVpn,
'confirms_voided_refund': confirmsVoidedRefund,
'confirms_terms_agreed': confirmsTermsAgreed,
});
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: body,
);
if (response.statusCode == 201) {
final data = json.decode(response.body) as Map<String, dynamic>;
return CakePayOrder.fromMap(data);
}
String message = 'Server error ${response.statusCode}';
final isJson = response.headers['content-type']?.contains('application/json') == true ||
response.body.trim().startsWith(RegExp(r'[\{\[]'));
if (isJson) {
try {
final decoded = json.decode(response.body);
if (decoded is List && decoded.isNotEmpty) {
message = decoded.first.toString();
} else if (decoded is Map && decoded['detail'] != null) {
message = decoded['detail'].toString();
} else {
message = decoded.toString();
}
} on FormatException {}
}
throw Exception(message);
}
///Simulate Payment
Future<String> simulatePayment(
{required String CSRFToken, required String authorization, required String orderId}) async {
final uri = Uri.https(baseCakePayUri, simulatePaymentPath + '/$orderId');
final headers = {
'accept': 'application/json',
'authorization': authorization,
'X-CSRFToken': CSRFToken,
};
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
printV('Response: ${response.statusCode}');
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
return 'You just SIMULATED a buying of a gift card with ID: ${bodyJson['order_id']}';
}
/// Logout
Future<void> logoutUser({required String email, required String apiKey}) async {
final uri = Uri.https(baseCakePayUri, logoutPath);
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Api-Key $apiKey',
};
try {
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode({'email': email}),
);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
} catch (e) {
printV('Caught exception: $e');
}
}
/// 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,
String? search,
List<String>? vendorIds,
bool? giftCards,
bool? prepaidCards,
bool? onDemand,
bool? custom,
}) async {
var queryParams = {
'page': page?.toString(),
'country': country,
'country_code': countryCode,
'search': search,
'vendor_ids': vendorIds?.join(','),
'gift_cards': giftCards?.toString(),
'prepaid_cards': prepaidCards?.toString(),
'on_demand': onDemand?.toString(),
'custom': custom?.toString(),
};
final uri = Uri.https(baseCakePayUri, vendorsPath, queryParams);
var headers = {
'accept': 'application/json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Api-Key $apiKey',
};
var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
if (response.statusCode != 200) {
throw Exception(
'Failed to fetch vendors: statusCode - ${response.statusCode}, queryParams -$queryParams, response - ${response.body}');
}
final bodyJson = json.decode(response.body);
if (bodyJson is List<dynamic> && bodyJson.isEmpty) {
return [];
}
return (bodyJson['results'] as List)
.map((e) => CakePayVendor.fromJson(e as Map<String, dynamic>, country))
.toList();
}
}
@@ -0,0 +1,119 @@
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/country.dart';
class CakePayService {
CakePayService(this.secureStorage, this.cakePayApi);
static const cakePayEmailStorageKey = 'cake_pay_email';
static const cakePayUsernameStorageKey = 'cake_pay_username';
static const cakePayUserTokenKey = 'cake_pay_user_token';
static String get testCakePayApiKey => secrets.testCakePayApiKey;
static String get cakePayApiKey => secrets.cakePayApiKey;
static String get CSRFToken => secrets.CSRFToken;
static String get authorization => secrets.authorization;
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,
String? search,
List<String>? vendorIds,
bool? giftCards,
bool? prepaidCards,
bool? onDemand,
bool? custom,
}) async {
final result = await cakePayApi.getVendors(
apiKey: cakePayApiKey,
page: page,
country: country,
countryCode: countryCode,
search: search,
vendorIds: vendorIds,
giftCards: giftCards,
prepaidCards: prepaidCards,
onDemand: onDemand,
custom: custom);
return result;
}
/// LogIn
Future<void> logIn(String email) async {
final userName = await cakePayApi.authenticateUser(email: email, apiKey: cakePayApiKey);
await secureStorage.write(key: cakePayEmailStorageKey, value: userName);
await secureStorage.write(key: cakePayUsernameStorageKey, value: userName);
}
/// Verify email
Future<void> verifyEmail(String code) async {
final email = (await secureStorage.read(key: cakePayEmailStorageKey))!;
final credentials =
await cakePayApi.verifyEmail(email: email, code: code, apiKey: cakePayApiKey);
await secureStorage.write(key: cakePayUserTokenKey, value: credentials.token);
await secureStorage.write(key: cakePayUsernameStorageKey, value: credentials.username);
}
Future<String?> getUserEmail() async {
return (await secureStorage.read(key: cakePayEmailStorageKey));
}
/// Check is user logged
Future<bool> isLogged() async {
final username = await secureStorage.read(key: cakePayUsernameStorageKey) ?? '';
final password = await secureStorage.read(key: cakePayUserTokenKey) ?? '';
return username.isNotEmpty && password.isNotEmpty;
}
/// Logout
Future<void> logout([String? email]) async {
await secureStorage.delete(key: cakePayUsernameStorageKey);
await secureStorage.delete(key: cakePayUserTokenKey);
if (email != null) {
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
}
}
/// Purchase Gift Card
Future<CakePayOrder> createOrder({
required int cardId,
required String price,
required int quantity,
required bool confirmsNoVpn,
required bool confirmsVoidedRefund,
required bool confirmsTermsAgreed,
}) async {
final userEmail = (await secureStorage.read(key: cakePayEmailStorageKey))!;
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.createOrder(
apiKey: cakePayApiKey,
cardId: cardId,
price: price,
quantity: quantity,
token: token,
userEmail: userEmail,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
);
}
///Simulate Purchase Gift Card
Future<String> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
CSRFToken: CSRFToken, authorization: authorization, orderId: orderId);
}