diff --git a/lib/cake_pay/src/cake_pay_exceptions.dart b/lib/cake_pay/src/cake_pay_exceptions.dart new file mode 100644 index 000000000..13d84b56d --- /dev/null +++ b/lib/cake_pay/src/cake_pay_exceptions.dart @@ -0,0 +1,6 @@ +class CakePayUnauthorizedException implements Exception { + const CakePayUnauthorizedException(); + + @override + String toString() => 'Cake Pay session is no longer valid, please log in again.'; +} diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index 8e0a470cd..64551ca65 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -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 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; diff --git a/lib/cake_pay/src/cards/cake_pay_cards_page.dart b/lib/cake_pay/src/cards/cake_pay_cards_page.dart index c0b1b7d17..9caee90ee 100644 --- a/lib/cake_pay/src/cards/cake_pay_cards_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_cards_page.dart @@ -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(); + }, + ); }, ), ], diff --git a/lib/cake_pay/src/services/cake_pay_api.dart b/lib/cake_pay/src/services/cake_pay_api.dart index 84cc1f4df..fa3918713 100644 --- a/lib/cake_pay/src/services/cake_pay_api.dart +++ b/lib/cake_pay/src/services/cake_pay_api.dart @@ -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; 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}'); } diff --git a/lib/cake_pay/src/services/cake_pay_service.dart b/lib/cake_pay/src/services/cake_pay_service.dart index 8166b52c2..d5e8a75af 100644 --- a/lib/cake_pay/src/services/cake_pay_service.dart +++ b/lib/cake_pay/src/services/cake_pay_service.dart @@ -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 findOrderById({required String orderId}) async { - final token = (await secureStorage.read(key: cakePayUserTokenKey))!; - return await cakePayApi.getOrderById(orderId: orderId, token: token); - } + Future findOrderById({required String orderId}) => _withUserToken( + (token) => cakePayApi.getOrderById( + orderId: orderId, + token: token, + ), + ); ///Simulate Purchase Gift Card - Future 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 simulatePayment({required String orderId}) => _withUserToken( + (token) => cakePayApi.simulatePayment( + CSRFToken: CSRFToken, + authorization: authorization, + token: token, + orderId: orderId, + ), + ); + + /// Wrapper for requests with user token + Future _withUserToken(Future 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; + } } } diff --git a/lib/src/screens/order_details/order_details_page.dart b/lib/src/screens/order_details/order_details_page.dart index c1463a7cb..45d354424 100644 --- a/lib/src/screens/order_details/order_details_page.dart +++ b/lib/src/screens/order_details/order_details_page.dart @@ -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 { final OrderDetailsViewModel orderDetailsViewModel; final MaterialThemeBase currentTheme; + ReactionDisposer? _sessionExpiredReaction; + + @override + void initState() { + super.initState(); + + _sessionExpiredReaction = reaction( + (_) => orderDetailsViewModel.isCakePaySessionExpired, + (isExpired) { + if (!isExpired) { + return; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + + showPopUp( + 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 diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart index a95e2353d..311354ba1 100644 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart @@ -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 logout() async => await _cakePayService.logout(); - void _startExpirationTimer() { _timer?.cancel(); _timer = Timer.periodic(Duration(seconds: 1), (_) { diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart index 54ecdf794..a2fba48fb 100644 --- a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart @@ -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 userCards; @@ -189,12 +191,9 @@ abstract class CakePayCardsListViewModelBase with Store { @action Future 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 diff --git a/lib/view_model/order_details_view_model.dart b/lib/view_model/order_details_view_model.dart index 2edb4db22..25b61b79e 100644 --- a/lib/view_model/order_details_view_model.dart +++ b/lib/view_model/order_details_view_model.dart @@ -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 items; + @observable + bool isCakePaySessionExpired = false; + final CakePayService cakePayService; final Box orders; OrderProvider? _provider; @@ -74,7 +78,9 @@ abstract class OrderDetailsViewModelBase with Store { @action Future _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()); } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 6e6d3c487..1ea0cf8a2 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -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." -} \ No newline at end of file +}