From abb6ff4933ee9844fc1ce72818ef000c6a33b78a Mon Sep 17 00:00:00 2001 From: M Date: Thu, 14 Jul 2022 17:57:40 +0100 Subject: [PATCH 1/3] Add redeem for ionia gift cards --- lib/di.dart | 9 ++- lib/ionia/ionia_api.dart | 81 +++++++++++++++++++ lib/ionia/ionia_gift_card.dart | 1 + lib/ionia/ionia_service.dart | 29 +++++++ .../cards/ionia_gift_card_detail_page.dart | 65 ++++++++++----- .../ionia/ionia_account_view_model.dart | 6 +- .../ionia_gift_card_details_view_model.dart | 35 ++++++++ res/values/strings_en.arb | 2 +- 8 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 lib/view_model/ionia/ionia_gift_card_details_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index d026ff136..4451649b1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -145,6 +145,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/ionia/ionia_token_service.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; final getIt = GetIt.instance; @@ -713,8 +714,14 @@ Future setup( return IoniaBuyGiftCardDetailPage(getIt.get(param1: amount, param2: merchant)); }); + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { + return IoniaGiftCardDetailsViewModel( + ioniaService: getIt.get(), + giftCard: giftCard); + }); + getIt.registerFactoryParam((IoniaGiftCard giftCard, _) { - return IoniaGiftCardDetailPage(giftCard); + return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); }); getIt.registerFactoryParam((List args, _) { diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart index 93960b5fd..da5911c84 100644 --- a/lib/ionia/ionia_api.dart +++ b/lib/ionia/ionia_api.dart @@ -19,6 +19,8 @@ class IoniaApi { static final getMerchantsByFilterUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchantsByFilter'); static final getPurchaseMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/PurchaseGiftCard'); static final getCurrentUserGiftCardSummariesUrl = Uri.https(baseUri, '/$pathPrefix/GetCurrentUserGiftCardSummaries'); + static final changeGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/ChargeGiftCard'); + static final getGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/GetGiftCard'); // Create user @@ -274,4 +276,83 @@ class IoniaApi { return IoniaGiftCard.fromJsonMap(element); }).toList(); } + + // Charge Gift Card + + Future chargeGiftCard({ + @required String username, + @required String password, + @required String clientId, + @required int giftCardId, + @required double amount}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = { + 'Id': giftCardId, + 'Amount': amount}; + final response = await post( + changeGiftCardUrl, + headers: headers, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw Exception('Failed to update Gift Card with ID ${giftCardId};Incorrect response status: ${response.statusCode};'); + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + final data = decodedBody['Data'] as Map; + final msg = data['Message'] as String ?? ''; + + if (msg.isNotEmpty) { + throw Exception(msg); + } + + throw Exception('Failed to update Gift Card with ID ${giftCardId};'); + } + } + + // Get Gift Card + + Future getGiftCard({ + @required String username, + @required String password, + @required String clientId, + @required int id}) async { + final headers = { + 'clientId': clientId, + 'username': username, + 'password': password, + 'Content-Type': 'application/json'}; + final body = {'Id': id}; + final response = await post( + getGiftCardUrl, + headers: headers, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw Exception('Failed to get Gift Card with ID ${id};Incorrect response status: ${response.statusCode};'); + } + + final decodedBody = json.decode(response.body) as Map; + final isSuccessful = decodedBody['Successful'] as bool ?? false; + + if (!isSuccessful) { + final msg = decodedBody['ErrorMessage'] as String ?? ''; + + if (msg.isNotEmpty) { + throw Exception(msg); + } + + throw Exception('Failed to get Gift Card with ID ${id};'); + } + + final data = decodedBody['Data'] as Map; + return IoniaGiftCard.fromJsonMap(data); + } } \ No newline at end of file diff --git a/lib/ionia/ionia_gift_card.dart b/lib/ionia/ionia_gift_card.dart index ee99f7986..df0ee6a52 100644 --- a/lib/ionia/ionia_gift_card.dart +++ b/lib/ionia/ionia_gift_card.dart @@ -33,6 +33,7 @@ class IoniaGiftCard { systemName: element['SystemName'] as String, barcodeUrl: element['BarcodeUrl'] as String, cardNumber: element['CardNumber'] as String, + cardPin: element['CardPin'] as String, tip: element['Tip'] as double, purchaseAmount: element['PurchaseAmount'] as double, actualAmount: element['ActualAmount'] as double, diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart index ceb95f6fc..c00b0ad18 100644 --- a/lib/ionia/ionia_service.dart +++ b/lib/ionia/ionia_service.dart @@ -120,4 +120,33 @@ class IoniaService { final password = await secureStorage.read(key: ioniaPasswordStorageKey); return ioniaApi.getCurrentUserGiftCardSummaries(username: username, password: password, clientId: clientId); } + + // Charge Gift Card + + Future chargeGiftCard({ + @required int giftCardId, + @required double amount}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + await ioniaApi.chargeGiftCard( + username: username, + password: password, + clientId: clientId, + giftCardId: giftCardId, + amount: amount); + } + + // Redeem + + Future redeem(IoniaGiftCard giftCard) async { + await chargeGiftCard(giftCardId: giftCard.id, amount: giftCard.remainingAmount); + } + + // Get Gift Card + + Future getGiftCard({@required int id}) async { + final username = await secureStorage.read(key: ioniaUsernameStorageKey); + final password = await secureStorage.read(key: ioniaPasswordStorageKey); + return ioniaApi.getGiftCard(username: username, password: password, clientId: clientId,id: id); + } } \ No newline at end of file diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart index 4c47f90a7..7f081453a 100644 --- a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -1,20 +1,25 @@ +import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart'; import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class IoniaGiftCardDetailPage extends BasePage { - IoniaGiftCardDetailPage(this.merchant); + IoniaGiftCardDetailPage(this.viewModel); - final IoniaGiftCard merchant; + final IoniaGiftCardDetailsViewModel viewModel; @override Widget leading(BuildContext context) { @@ -48,56 +53,78 @@ class IoniaGiftCardDetailPage extends BasePage { @override Widget middle(BuildContext context) { return Text( - merchant.legalName, + viewModel.giftCard.legalName, style: textLargeSemiBold(color: Theme.of(context).accentTextTheme.display4.backgroundColor), ); } @override Widget body(BuildContext context) { + reaction((_) => viewModel.redeemState, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } + }); + return ScrollableWithBottomSection( contentPadding: EdgeInsets.all(24), content: Column( children: [ - if (merchant.barcodeUrl != null && merchant.barcodeUrl.isNotEmpty) + if (viewModel.giftCard.barcodeUrl != null && viewModel.giftCard.barcodeUrl.isNotEmpty) Padding( padding: const EdgeInsets.symmetric( horizontal: 24.0, vertical: 24, ), - child: SizedBox(height: 96, width: double.infinity, child: Image.network(merchant.barcodeUrl)), + child: SizedBox(height: 96, width: double.infinity, child: Image.network(viewModel.giftCard.barcodeUrl)), ), SizedBox(height: 24), IoniaTile( title: S.of(context).gift_card_number, - subTitle: merchant.cardNumber, + subTitle: viewModel.giftCard.cardNumber, ), Divider(height: 30), IoniaTile( title: S.of(context).pin_number, - subTitle: merchant.cardPin ?? '', + subTitle: viewModel.giftCard.cardPin ?? '', ), Divider(height: 30), - IoniaTile( - title: S.of(context).amount, - subTitle: merchant.remainingAmount.toString() ?? '0', - ), + Observer(builder: (_) => + IoniaTile( + title: S.of(context).amount, + subTitle: viewModel.giftCard.remainingAmount.toString() ?? '0', + )), Divider(height: 50), TextIconButton( label: S.of(context).how_to_use_card, - onTap: () => _showHowToUseCard(context, merchant), + onTap: () => _showHowToUseCard(context, viewModel.giftCard), ), ], ), bottomSection: Padding( padding: EdgeInsets.only(bottom: 12), - child: LoadingPrimaryButton( - isLoading: false, - onPressed: () {}, - text: S.of(context).mark_as_redeemed, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, - )), + child: Observer(builder: (_) { + if (!viewModel.giftCard.isEmpty) { + return LoadingPrimaryButton( + isLoading: viewModel.redeemState is IsExecutingState, + onPressed: () => viewModel.redeem(), + text: S.of(context).mark_as_redeemed, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white); + } + + return Container(); + })), ); } diff --git a/lib/view_model/ionia/ionia_account_view_model.dart b/lib/view_model/ionia/ionia_account_view_model.dart index 1bdc1c9f7..1a57d0acf 100644 --- a/lib/view_model/ionia/ionia_account_view_model.dart +++ b/lib/view_model/ionia/ionia_account_view_model.dart @@ -24,13 +24,13 @@ abstract class IoniaAccountViewModelBase with Store { List merchs; @computed - int get countOfMerch => merchs.where((merch) => merch.isActive).length; + int get countOfMerch => merchs.where((merch) => !merch.isEmpty).length; @computed - List get activeMechs => merchs.where((merch) => merch.isActive).toList(); + List get activeMechs => merchs.where((merch) => !merch.isEmpty).toList(); @computed - List get redeemedMerchs => merchs.where((merch) => !merch.isActive).toList(); + List get redeemedMerchs => merchs.where((merch) => merch.isEmpty).toList(); @action void logout() { diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart new file mode 100644 index 000000000..7c811ad62 --- /dev/null +++ b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:mobx/mobx.dart'; + +part 'ionia_gift_card_details_view_model.g.dart'; + +class IoniaGiftCardDetailsViewModel = IoniaGiftCardDetailsViewModelBase with _$IoniaGiftCardDetailsViewModel; + +abstract class IoniaGiftCardDetailsViewModelBase with Store { + + IoniaGiftCardDetailsViewModelBase({this.ioniaService, this.giftCard}) { + redeemState = InitialExecutionState(); + } + + final IoniaService ioniaService; + + @observable + IoniaGiftCard giftCard; + + @observable + ExecutionState redeemState; + + @action + Future redeem() async { + try { + redeemState = InitialExecutionState(); + await ioniaService.redeem(giftCard); + giftCard = await ioniaService.getGiftCard(id: giftCard.id); + redeemState = ExecutedSuccessfullyState(); + } catch(e) { + redeemState = FailureState(e.toString()); + } + } +} \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ec420ba24..b3e683946 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -620,5 +620,5 @@ "awaiting_payment_confirmation": "Awaiting payment confirmation", "transaction_sent_notice": "If the screen doesn’t proceed after 1 minute, check a block explorer and your email.", "agree": "Agree", - "in_store": "In Store", + "in_store": "In Store" } From b9cc776614ecdbe3daccf5758eef874c9930ca8a Mon Sep 17 00:00:00 2001 From: M Date: Thu, 14 Jul 2022 19:04:55 +0100 Subject: [PATCH 2/3] Fix navigation after ionia opt redirection. --- lib/src/screens/ionia/auth/ionia_verify_otp_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart index 909d8b89a..d1253c784 100644 --- a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart +++ b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart @@ -143,5 +143,6 @@ class IoniaVerifyIoniaOtp extends BasePage { } void _onOtpSuccessful(BuildContext context) => - Navigator.pushNamedAndRemoveUntil(context, Routes.ioniaManageCardsPage, ModalRoute.withName(Routes.dashboard)); + Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); } From ce479849ea68165e30cc1f43b46cabe162922a4b Mon Sep 17 00:00:00 2001 From: M Date: Thu, 14 Jul 2022 19:59:34 +0100 Subject: [PATCH 3/3] Fix update gift cards list. --- .../ionia/cards/ionia_account_cards_page.dart | 26 ++++++++++++++----- .../ionia/cards/ionia_account_page.dart | 5 +++- .../ionia/ionia_account_view_model.dart | 17 +++++++----- .../ionia_gift_card_details_view_model.dart | 2 +- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart index 5e752e5fa..f1d08a75e 100644 --- a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart +++ b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/routes.dart'; @@ -104,11 +106,23 @@ class _IoniaCardTabsState extends State<_IoniaCardTabs> with SingleTickerProvide _IoniaCardListView( emptyText: S.of(context).gift_card_balance_note, merchList: viewModel.activeMechs, - ), + onTap: (giftCard) { + Navigator.pushNamed( + context, + Routes.ioniaGiftCardDetailPage, + arguments: [giftCard]) + .then((_) => viewModel.updateUserGiftCards()); + }), _IoniaCardListView( emptyText: S.of(context).gift_card_redeemed_note, merchList: viewModel.redeemedMerchs, - ), + onTap: (giftCard) { + Navigator.pushNamed( + context, + Routes.ioniaGiftCardDetailPage, + arguments: [giftCard]) + .then((_) => viewModel.updateUserGiftCards()); + }), ], ); }), @@ -124,10 +138,12 @@ class _IoniaCardListView extends StatelessWidget { Key key, @required this.emptyText, @required this.merchList, + @required this.onTap, }) : super(key: key); final String emptyText; final List merchList; + final void Function(IoniaGiftCard giftCard) onTap; @override Widget build(BuildContext context) { @@ -148,11 +164,7 @@ class _IoniaCardListView extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 16), child: CardItem( - onTap: () => Navigator.pushNamed( - context, - Routes.ioniaGiftCardDetailPage, - arguments: [merchant], - ), + onTap: () => onTap?.call(merchant), title: merchant.legalName, backgroundColor: Theme.of(context).accentTextTheme.display4.backgroundColor.withOpacity(0.1), discount: 0, diff --git a/lib/src/screens/ionia/cards/ionia_account_page.dart b/lib/src/screens/ionia/cards/ionia_account_page.dart index c16aef002..2a34dc74c 100644 --- a/lib/src/screens/ionia/cards/ionia_account_page.dart +++ b/lib/src/screens/ionia/cards/ionia_account_page.dart @@ -47,7 +47,10 @@ class IoniaAccountPage extends BasePage { ), )), InkWell( - onTap: () => Navigator.pushNamed(context, Routes.ioniaAccountCardsPage), + onTap: () { + Navigator.pushNamed(context, Routes.ioniaAccountCardsPage) + .then((_) => ioniaAccountViewModel.updateUserGiftCards()); + }, child: Padding( padding: const EdgeInsets.all(8.0), child: Text( diff --git a/lib/view_model/ionia/ionia_account_view_model.dart b/lib/view_model/ionia/ionia_account_view_model.dart index 1a57d0acf..a4875ec61 100644 --- a/lib/view_model/ionia/ionia_account_view_model.dart +++ b/lib/view_model/ionia/ionia_account_view_model.dart @@ -10,9 +10,9 @@ class IoniaAccountViewModel = IoniaAccountViewModelBase with _$IoniaAccountViewM abstract class IoniaAccountViewModelBase with Store { IoniaAccountViewModelBase({this.ioniaService}) { email = ''; - merchs = []; + giftCards = []; ioniaService.getUserEmail().then((email) => this.email = email); - ioniaService.getCurrentUserGiftCardSummaries().then((merchs) => this.merchs = merchs); + updateUserGiftCards(); } final IoniaService ioniaService; @@ -21,19 +21,24 @@ abstract class IoniaAccountViewModelBase with Store { String email; @observable - List merchs; + List giftCards; @computed - int get countOfMerch => merchs.where((merch) => !merch.isEmpty).length; + int get countOfMerch => giftCards.where((giftCard) => !giftCard.isEmpty).length; @computed - List get activeMechs => merchs.where((merch) => !merch.isEmpty).toList(); + List get activeMechs => giftCards.where((giftCard) => !giftCard.isEmpty).toList(); @computed - List get redeemedMerchs => merchs.where((merch) => merch.isEmpty).toList(); + List get redeemedMerchs => giftCards.where((giftCard) => giftCard.isEmpty).toList(); @action void logout() { ioniaService.logout(); } + + @action + Future updateUserGiftCards() async { + giftCards = await ioniaService.getCurrentUserGiftCardSummaries(); + } } diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart index 7c811ad62..e6138bb53 100644 --- a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart +++ b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart @@ -24,7 +24,7 @@ abstract class IoniaGiftCardDetailsViewModelBase with Store { @action Future redeem() async { try { - redeemState = InitialExecutionState(); + redeemState = IsExecutingState(); await ioniaService.redeem(giftCard); giftCard = await ioniaService.getGiftCard(id: giftCard.id); redeemState = ExecutedSuccessfullyState();