mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user