Delete CakePay Auth-token if the API returns 401 (#3538)

* delete cakepay auth token on 401

* fix cakepay auth refresh

* fix Cake Pay error handling
This commit is contained in:
Patrick
2026-09-03 01:58:45 +08:00
committed by GitHub
parent ea4d752914
commit 73a739411f
10 changed files with 160 additions and 46 deletions
@@ -0,0 +1,6 @@
class CakePayUnauthorizedException implements Exception {
const CakePayUnauthorizedException();
@override
String toString() => 'Cake Pay session is no longer valid, please log in again.';
}
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
@@ -497,19 +498,37 @@ class CakePayBuyCardPage extends BasePage {
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
try {
await cakePayBuyCardViewModel.createOrder();
} catch (_) {
try {
final isLogged = await cakePayBuyCardViewModel.isUserLogged;
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
await cakePayBuyCardViewModel.logout();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
}
return;
}
await cakePayBuyCardViewModel.createOrder();
} on CakePayUnauthorizedException {
cakePayBuyCardViewModel.isSimulatingFlow = false;
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
}
} catch (error) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
_sendViewModel.state = FailureState(
_sendViewModel.translateErrorMessage(
error,
cakePayBuyCardViewModel.walletType,
_sendViewModel.wallet.currency,
),
);
} finally {
cakePayBuyCardViewModel.isPurchasing = false;
}
cakePayBuyCardViewModel.isPurchasing = false;
}
BuildContext? dialogContext;
@@ -477,11 +477,18 @@ class _ShopTabState extends State<_ShopTab> {
if (i >= vendors.length) return const _Loading();
final vendor = vendors[i];
return CardItem(
logoUrl: vendor.card?.cardImageUrl,
title: vendor.name,
subTitle: vendor.card?.description ?? '',
onTap: () => Navigator.pushNamed(context, Routes.cakePayBuyCardPage,
arguments: [vendor]));
logoUrl: vendor.card?.cardImageUrl,
title: vendor.name,
subTitle: vendor.card?.description ?? '',
onTap: () async {
await Navigator.pushNamed(
context,
Routes.cakePayBuyCardPage,
arguments: [vendor],
);
await viewModel.checkAuth();
},
);
},
),
],
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
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';
@@ -129,6 +130,10 @@ class CakePayApi {
body: body,
);
if (response.statusCode == 401) {
throw const CakePayUnauthorizedException();
}
if (response.statusCode == 201) {
final data = json.decode(response.body) as Map<String, dynamic>;
return CakePayOrder.fromMap(data);
@@ -168,6 +173,10 @@ class CakePayApi {
headers: headers,
);
if (response.statusCode == 401) {
throw const CakePayUnauthorizedException();
}
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
@@ -203,6 +212,10 @@ class CakePayApi {
printV('Response: ${response.statusCode}');
if (response.statusCode == 401) {
throw const CakePayUnauthorizedException();
}
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
+42 -19
View File
@@ -1,4 +1,5 @@
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
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';
@@ -95,29 +96,51 @@ class CakePayService {
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,
return _withUserToken(
(token) => cakePayApi.createOrder(
apiKey: cakePayApiKey,
cardId: cardId,
price: price,
quantity: quantity,
token: token,
userEmail: userEmail,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
),
);
}
Future<CakePayOrder> findOrderById({required String orderId}) async {
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.getOrderById(orderId: orderId, token: token);
}
Future<CakePayOrder> findOrderById({required String orderId}) => _withUserToken(
(token) => cakePayApi.getOrderById(
orderId: orderId,
token: token,
),
);
///Simulate Purchase Gift Card
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);
Future<String> simulatePayment({required String orderId}) => _withUserToken(
(token) => cakePayApi.simulatePayment(
CSRFToken: CSRFToken,
authorization: authorization,
token: token,
orderId: orderId,
),
);
/// Wrapper for requests with user token
Future<T> _withUserToken<T>(Future<T> Function(String token) request) async {
try {
final token = await secureStorage.read(key: cakePayUserTokenKey) ?? '';
if (token.isEmpty) {
throw const CakePayUnauthorizedException();
}
return await request(token);
} on CakePayUnauthorizedException {
await logout();
rethrow;
}
}
}
@@ -1,10 +1,12 @@
import 'package:cake_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/cake_pay_order_list_card.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
@@ -14,6 +16,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
import 'package:mobx/mobx.dart';
class OrderDetailsPage extends BasePage {
OrderDetailsPage(this.orderDetailsViewModel);
@@ -43,11 +46,44 @@ class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
final OrderDetailsViewModel orderDetailsViewModel;
final MaterialThemeBase currentTheme;
ReactionDisposer? _sessionExpiredReaction;
@override
void initState() {
super.initState();
_sessionExpiredReaction = reaction<bool>(
(_) => orderDetailsViewModel.isCakePaySessionExpired,
(isExpired) {
if (!isExpired) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
showPopUp<void>(
context: context,
builder: (dialogContext) => AlertWithOneAction(
alertTitle: S.of(dialogContext).error,
alertContent: S.of(dialogContext).cake_pay_session_expired,
buttonText: S.of(dialogContext).ok,
buttonAction: () => Navigator.of(dialogContext).pop(),
),
);
});
},
fireImmediately: true,
);
}
@override
void dispose() {
super.dispose();
_sessionExpiredReaction?.call();
orderDetailsViewModel.timer?.cancel();
super.dispose();
}
@override
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_source_description.dart';
@@ -221,6 +222,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
orders.add(orderRecord);
updateRemainingTime();
_startExpirationTimer();
} on CakePayUnauthorizedException {
rethrow;
} catch (e) {
sendViewModel.state = FailureState(
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
@@ -275,8 +278,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
}
}
Future<void> logout() async => await _cakePayService.logout();
void _startExpirationTimer() {
_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
@@ -78,7 +78,6 @@ abstract class CakePayCardsListViewModelBase with Store {
};
String searchString;
String? username;
int page;
late Country _initialSelectedCountry;
@@ -87,6 +86,9 @@ abstract class CakePayCardsListViewModelBase with Store {
late bool _initialDisplayDenominationsCards;
late bool _initialDisplayCustomValueCards;
@observable
String? username;
@observable
List<CakePayCard> userCards;
@@ -189,12 +191,9 @@ abstract class CakePayCardsListViewModelBase with Store {
@action
Future<void> checkAuth() async {
authFuture = ObservableFuture(cakePayService.isLogged());
final logged = await authFuture!;
if (logged) {
username = await cakePayService.getUserEmail();
}
final logged = await cakePayService.isLogged();
username = logged ? await cakePayService.getUserEmail() : null;
authFuture = ObservableFuture.value(logged);
}
@action
+10 -1
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/utilities.dart';
@@ -64,6 +65,9 @@ abstract class OrderDetailsViewModelBase with Store {
@observable
ObservableList<StandartListItem> items;
@observable
bool isCakePaySessionExpired = false;
final CakePayService cakePayService;
final Box<Order> orders;
OrderProvider? _provider;
@@ -74,7 +78,9 @@ abstract class OrderDetailsViewModelBase with Store {
@action
Future<void> _updateOrder() async {
try {
if (_provider == null) return;
if (_provider == null || isCakePaySessionExpired) {
return;
}
final updatedOrderObj = await _provider!.findOrderById(order.id);
final updatedOrder = updatedOrderObj.$1;
@@ -95,6 +101,9 @@ abstract class OrderDetailsViewModelBase with Store {
}
_updateItems();
} on CakePayUnauthorizedException {
timer?.cancel();
isCakePaySessionExpired = true;
} catch (e) {
printV(e.toString());
}
+2 -1
View File
@@ -152,6 +152,7 @@
"cake_pay_card_email_delivered_message": "Your gift card will be delivered via email after the necessary confirmations.",
"cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.",
"cake_pay_save_order": "The card should be sent to your e-mail within 1 business day \n Save your Order ID:",
"cake_pay_session_expired": "Your Cake Pay session is no longer valid. Please log in again.",
"cake_pay_subtitle": "Buy worldwide prepaid cards and gift cards",
"cake_pay_web_cards_subtitle": "Buy worldwide prepaid cards and gift cards",
"cake_pay_web_cards_title": "Cake Pay Web Cards",
@@ -1527,4 +1528,4 @@
"zcash_network_upgrade_desc_1": "Zcash has undergone a network upgrade called Ironwood that requires migration of funds before they can be spent. This migration is automatic, privacy-preserving, and starts as soon as you open your Zcash wallet.",
"zcash_network_upgrade_desc_2": "When the migration finishes, your Available Balance will return to normal, and you can continue to send and receive Zcash as usual.",
"zcash_network_upgrade_desc_3": "Please allow your Zcash wallet to finishing syncing, and then leave the app open until migration finishes which can take a few minutes depending on the balance."
}
}