From 84cb1d926c1a4227ee44524b32dfdc9d1aae7574 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Thu, 10 Nov 2022 11:07:27 +0100 Subject: [PATCH 01/12] Initial work to support free products in the app --- ...elopment.xml => _template__of_Flutter.xml} | 4 +- lib/payment/free_product_service.dart | 46 +++++++++++++++++ lib/payment/mobilepay_service.dart | 50 +++---------------- lib/payment/payment_handler.dart | 46 +++++++++++++++-- .../components/purchase/purchase_overlay.dart | 2 +- .../buy_ticket_bottom_modal_sheet.dart | 38 +++++++++++--- openapi/coffeecard_api_v2.swagger.json | 42 +++++++++++++--- 7 files changed, 160 insertions(+), 68 deletions(-) rename .idea/runConfigurations/{development.xml => _template__of_Flutter.xml} (64%) create mode 100644 lib/payment/free_product_service.dart diff --git a/.idea/runConfigurations/development.xml b/.idea/runConfigurations/_template__of_Flutter.xml similarity index 64% rename from .idea/runConfigurations/development.xml rename to .idea/runConfigurations/_template__of_Flutter.xml index c613891b1..ffae9332d 100644 --- a/.idea/runConfigurations/development.xml +++ b/.idea/runConfigurations/_template__of_Flutter.xml @@ -1,7 +1,7 @@ - + - + \ No newline at end of file diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart new file mode 100644 index 000000000..97e80ff9f --- /dev/null +++ b/lib/payment/free_product_service.dart @@ -0,0 +1,46 @@ + +import 'package:coffeecard/data/repositories/utils/request_types.dart'; +import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; +import 'package:coffeecard/models/purchase/payment.dart'; +import 'package:coffeecard/models/purchase/payment_status.dart'; +import 'package:coffeecard/payment/payment_handler.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/widgets.dart'; + +class FreeProductService extends PaymentHandler { + final PurchaseRepository _repository; + final BuildContext _context; + + const FreeProductService({ + required super.repository, + required super.context, + }) : _repository = repository, + _context = context; + + @override + Future> initPurchase(int productId) async { + final response = + await _repository.initiatePurchase(productId, PaymentType.freepurchase); + + return response.fold((l) => Left(l), (r) { + final paymentDetails = FreeProductPaymentDetails.fromJsonFactory( + r.paymentDetails, + ); + + return Right( + Payment( + id: r.id, + paymentId: paymentDetails.paymentId!, + status: PaymentStatus.completed, + deeplink: '', + purchaseTime: r.dateCreated, + price: r.totalAmount, + productId: r.productId, + productName: r.productName, + ), + ); + }); + } +} diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index eb9892049..e498b5d66 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -12,11 +12,15 @@ import 'package:dartz/dartz.dart'; import 'package:flutter/widgets.dart'; import 'package:url_launcher/url_launcher.dart'; -class MobilePayService implements PaymentHandler { +class MobilePayService extends PaymentHandler { final PurchaseRepository _repository; final BuildContext _context; - MobilePayService(this._repository, this._context); + MobilePayService({ + required super.repository, + required super.context, + }) : _repository = repository, + _context = context; @override Future> initPurchase(int productId) async { @@ -45,7 +49,6 @@ class MobilePayService implements PaymentHandler { ); } - @override Future invokePaymentMethod(Uri uri) async { if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); @@ -65,45 +68,4 @@ class MobilePayService implements PaymentHandler { } } } - - @override - Future> verifyPurchase( - int purchaseId, - ) async { - // Call API endpoint, receive PaymentStatus - final either = await _repository.getPurchase(purchaseId); - - return either.map((purchase) { - final paymentDetails = - MobilePayPaymentDetails.fromJsonFactory(purchase.paymentDetails); - - final status = _mapPaymentStateToStatus(paymentDetails.state); - if (status == PaymentStatus.completed) { - return PaymentStatus.completed; - } - - // TODO(marfavi): Cover more cases for PaymentStatus, https://github.com/AnalogIO/coffeecard_app/issues/385 - return PaymentStatus.error; - }); - } - - PaymentStatus _mapPaymentStateToStatus(String? state) { - PaymentStatus status; - switch (state) { - case 'Initiated': - status = PaymentStatus.awaitingPayment; - break; - case 'Reserved': - status = PaymentStatus.reserved; - break; - case 'Captured': - status = PaymentStatus.completed; - break; - // Cases (cancelledByMerchant, cancelledBySystem, cancelledByUser) - default: - status = PaymentStatus.rejectedPayment; - break; - } - return status; - } } diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index baed185a8..3370e4a06 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -1,7 +1,10 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; +import 'package:coffeecard/payment/free_product_service.dart'; import 'package:coffeecard/payment/mobilepay_service.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:dartz/dartz.dart'; @@ -9,13 +12,28 @@ import 'package:flutter/widgets.dart'; enum InternalPaymentType { mobilePay, + applePay, + free, } abstract class PaymentHandler { - factory PaymentHandler(InternalPaymentType type, BuildContext context) { - switch (type) { + final PurchaseRepository _repository; + final BuildContext _context; + + const PaymentHandler({ + required PurchaseRepository repository, + required BuildContext context, + }) : _repository = repository, + _context = context; + + static PaymentHandler createPaymentHandler(InternalPaymentType paymentType, BuildContext context){ + final repository = sl.get(); + + switch(paymentType) { case InternalPaymentType.mobilePay: - return MobilePayService(sl.get(), context); + return MobilePayService(repository: repository, context: context); + case InternalPaymentType.free: + return FreeProductService(repository: repository, context: context); default: throw UnimplementedError(); } @@ -23,7 +41,25 @@ abstract class PaymentHandler { Future> initPurchase(int productId); - Future> verifyPurchase(int purchaseId); + Future> verifyPurchase(int purchaseId) async { + // Call API endpoint, receive PaymentStatus + final either = await _repository.getPurchase(purchaseId); + + return either.fold( + (error) => Left(error), + (purchase) { + final paymentDetails = FreeProductPaymentDetails.fromJson( + purchase.paymentDetails, + ); + + final status = purchaseStatusFromJson(paymentDetails.purchaseStatus); + if (status == PurchaseStatus.completed) { + return const Right(PaymentStatus.completed); + } + // TODO: Cover more cases for PurchaseStatus + return const Right(PaymentStatus.error); + } + ); + } - Future invokePaymentMethod(Uri uri); } diff --git a/lib/widgets/components/purchase/purchase_overlay.dart b/lib/widgets/components/purchase/purchase_overlay.dart index c61c3880d..85d6363d6 100644 --- a/lib/widgets/components/purchase/purchase_overlay.dart +++ b/lib/widgets/components/purchase/purchase_overlay.dart @@ -22,7 +22,7 @@ Future showPurchaseOverlay({ onWillPop: () async => false, child: BlocProvider( create: (context) => PurchaseCubit( - paymentHandler: PaymentHandler(paymentType, context), + paymentHandler: PaymentHandler.createPaymentHandler(paymentType, context), product: product, ), child: BlocListener( diff --git a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart index 0d980258b..1e111dcd6 100644 --- a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart +++ b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart @@ -65,7 +65,7 @@ class _ModalContent extends StatelessWidget { ), const Gap(4), Text( - Strings.paymentConfirmationBottom(product.price), + product.price != 0 ? Strings.paymentConfirmationBottom(product.price) : 'Get your free product', style: AppTextStyle.price, ), const Gap(12), @@ -84,14 +84,38 @@ class _BottomModalSheetButtonBar extends StatefulWidget { final Product product; @override - State<_BottomModalSheetButtonBar> createState() => - _BottomModalSheetButtonBarState(); + State<_BottomModalSheetButtonBar> createState() => _BottomModalSheetButtonBarState(); } -class _BottomModalSheetButtonBarState - extends State<_BottomModalSheetButtonBar> { +class _BottomModalSheetButtonBarState extends State<_BottomModalSheetButtonBar> { @override Widget build(BuildContext context) { + if (widget.product.price == 0) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _BottomModalSheetButton( + text: 'Redeem product', + productId: widget.product.id, + price: widget.product.price, + onTap: () async { + final payment = await showPurchaseOverlay( + paymentType: InternalPaymentType.free, + product: widget.product, + context: context, + ); + + if (!mounted) return; + // Remove this bottom modal sheet. + Navigator.pop( + context, + payment, + ); + }, + ), + ], + ); + } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -163,9 +187,7 @@ class _BottomModalSheetButton extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( - child: (disabled && disabledText != null) - ? _withDisabledText(_button, disabledText!) - : _button, + child: (disabled && disabledText != null) ? _withDisabledText(_button, disabledText!) : _button, ); } } diff --git a/openapi/coffeecard_api_v2.swagger.json b/openapi/coffeecard_api_v2.swagger.json index 9f1cf5983..93b3fd2ce 100644 --- a/openapi/coffeecard_api_v2.swagger.json +++ b/openapi/coffeecard_api_v2.swagger.json @@ -1271,6 +1271,8 @@ "required": [ "paymentType", "orderId", + "paymentId", + "purchaseStatus", "discriminator" ], "properties": { @@ -1289,6 +1291,21 @@ "minLength": 1, "example": "f5cb3e0f-3b9b-4f50-8c4f-a7450f300a5c" }, + "paymentId": { + "type": "string", + "description": "Id for a payment", + "minLength": 1, + "example": "186d2b31-ff25-4414-9fd1-bfe9807fa8b7" + }, + "purchaseStatus": { + "description": "The status of the payment for the purchase", + "example": "PurchaseStatus.PendingPayment", + "oneOf": [ + { + "$ref": "#/components/schemas/PurchaseStatus" + } + ] + }, "discriminator": { "type": "string" } @@ -1337,12 +1354,6 @@ "minLength": 1, "example": "mobilepay://merchant_payments?payment_id=186d2b31-ff25-4414-9fd1-bfe9807fa8b7" }, - "paymentId": { - "type": "string", - "description": "MobilePay Id for a payment", - "minLength": 1, - "example": "186d2b31-ff25-4414-9fd1-bfe9807fa8b7" - }, "state": { "type": "string", "description": "MobilePay state", @@ -1356,7 +1367,7 @@ } ] }, - "FreePurchasePaymentDetails": { + "FreeProductPaymentDetails": { "allOf": [ { "$ref": "#/components/schemas/PaymentDetails" @@ -1383,6 +1394,21 @@ } ] }, + "MessageResponseDto": { + "type": "object", + "description": "Simple response class with a string message", + "example": { + "message": "Successful completion" + }, + "additionalProperties": false, + "properties": { + "message": { + "type": "string", + "description": "Message with API response", + "example": "Successful completion" + } + } + }, "InitiatePurchaseResponse": { "type": "object", "description": "Response object to a purchase request containing purchase information and reference to payment provider", @@ -1647,4 +1673,4 @@ } } } -} \ No newline at end of file +} From 9ddb1f692e76cf1923d1c5ecd81a47b98909cd45 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Thu, 10 Nov 2022 12:22:25 +0100 Subject: [PATCH 02/12] Updated purchase cubit to support the freeProduct handler --- lib/cubits/purchase/purchase_cubit.dart | 63 ++++++++++++++----- lib/payment/free_product_service.dart | 1 - lib/payment/payment_handler.dart | 36 +++++------ .../components/purchase/purchase_overlay.dart | 3 +- .../components/purchase/purchase_process.dart | 2 +- .../buy_ticket_bottom_modal_sheet.dart | 14 +++-- openapi/coffeecard_api_v2.swagger.json | 1 + 7 files changed, 78 insertions(+), 42 deletions(-) diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart index 62f313e4e..56589b124 100644 --- a/lib/cubits/purchase/purchase_cubit.dart +++ b/lib/cubits/purchase/purchase_cubit.dart @@ -2,9 +2,12 @@ import 'package:bloc/bloc.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/models/ticket/product.dart'; +import 'package:coffeecard/payment/free_product_service.dart'; +import 'package:coffeecard/payment/mobilepay_service.dart'; import 'package:coffeecard/payment/payment_handler.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; part 'purchase_state.dart'; @@ -16,25 +19,53 @@ class PurchaseCubit extends Cubit { PurchaseCubit({required this.paymentHandler, required this.product}) : super(const PurchaseInitial()); - Future payWithMobilePay() async { + Future _payWithApplePay() async { + // TODO: implement me + throw UnimplementedError(); + } + + Future _payWithMobilePay(MobilePayService service) async { + final either = await service.initPurchase(product.id); + + either.fold( + (error) => emit(PurchaseError(error.message)), + (payment) async { + if (payment.status != PaymentStatus.error) { + emit(PurchaseProcessing(payment)); + await service.invokePaymentMethod(Uri.parse(payment.deeplink)); + } else { + emit(PurchasePaymentRejected(payment)); + } + }, + ); + } + + Future _payWithFreeProduct(FreeProductService service) async { + final either = await service.initPurchase(product.id); + either.fold((error) => emit(PurchaseError(error.message)), (payment) { + if (payment.status != PaymentStatus.error) { + emit(PurchaseProcessing(payment)); + verifyPurchase(); + } else { + emit(PurchasePaymentRejected(payment)); + } + }); + } + + Future pay() async { + sl().beginCheckoutEvent(product); + if (state is PurchaseInitial) { - sl().beginCheckoutEvent(product); emit(const PurchaseStarted()); + final service = paymentHandler; - final either = await paymentHandler.initPurchase(product.id); - - either.fold( - (error) => emit(PurchaseError(error.reason)), - (payment) async { - if (payment.status != PaymentStatus.error) { - emit(PurchaseProcessing(payment)); - await paymentHandler - .invokePaymentMethod(Uri.parse(payment.deeplink)); - } else { - emit(PurchasePaymentRejected(payment)); - } - }, - ); + if (service is MobilePayService) { + _payWithMobilePay(service); + } else if (service is FreeProductService) { + _payWithFreeProduct(service); + } else { + emit(const PurchaseError('Unknown payment handler')); + } } } diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index 97e80ff9f..609a157ca 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -1,4 +1,3 @@ - import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index 3370e4a06..f9b00c8f3 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -26,10 +26,11 @@ abstract class PaymentHandler { }) : _repository = repository, _context = context; - static PaymentHandler createPaymentHandler(InternalPaymentType paymentType, BuildContext context){ + static PaymentHandler createPaymentHandler( + InternalPaymentType paymentType, BuildContext context) { final repository = sl.get(); - switch(paymentType) { + switch (paymentType) { case InternalPaymentType.mobilePay: return MobilePayService(repository: repository, context: context); case InternalPaymentType.free: @@ -41,25 +42,22 @@ abstract class PaymentHandler { Future> initPurchase(int productId); - Future> verifyPurchase(int purchaseId) async { + Future> verifyPurchase( + int purchaseId) async { // Call API endpoint, receive PaymentStatus final either = await _repository.getPurchase(purchaseId); - return either.fold( - (error) => Left(error), - (purchase) { - final paymentDetails = FreeProductPaymentDetails.fromJson( - purchase.paymentDetails, - ); - - final status = purchaseStatusFromJson(paymentDetails.purchaseStatus); - if (status == PurchaseStatus.completed) { - return const Right(PaymentStatus.completed); - } - // TODO: Cover more cases for PurchaseStatus - return const Right(PaymentStatus.error); - } - ); + return either.fold((error) => Left(error), (purchase) { + final paymentDetails = FreeProductPaymentDetails.fromJson( + purchase.paymentDetails, + ); + + final status = purchaseStatusFromJson(paymentDetails.purchaseStatus); + if (status == PurchaseStatus.completed) { + return const Right(PaymentStatus.completed); + } + // TODO: Cover more cases for PurchaseStatus + return const Right(PaymentStatus.error); + }); } - } diff --git a/lib/widgets/components/purchase/purchase_overlay.dart b/lib/widgets/components/purchase/purchase_overlay.dart index 85d6363d6..d2b6a7228 100644 --- a/lib/widgets/components/purchase/purchase_overlay.dart +++ b/lib/widgets/components/purchase/purchase_overlay.dart @@ -22,7 +22,8 @@ Future showPurchaseOverlay({ onWillPop: () async => false, child: BlocProvider( create: (context) => PurchaseCubit( - paymentHandler: PaymentHandler.createPaymentHandler(paymentType, context), + paymentHandler: + PaymentHandler.createPaymentHandler(paymentType, context), product: product, ), child: BlocListener( diff --git a/lib/widgets/components/purchase/purchase_process.dart b/lib/widgets/components/purchase/purchase_process.dart index a079a7b11..2e5a9275f 100644 --- a/lib/widgets/components/purchase/purchase_process.dart +++ b/lib/widgets/components/purchase/purchase_process.dart @@ -48,7 +48,7 @@ class _PurchaseProcessState extends State } if (state is PurchaseInitial) { // Not related to previous check, hence a separate if statement - cubit.payWithMobilePay(); + cubit.pay(); return makeLoadingDialog( title: Strings.purchaseTalking, ); diff --git a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart index 1e111dcd6..e99eafa2e 100644 --- a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart +++ b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart @@ -65,7 +65,9 @@ class _ModalContent extends StatelessWidget { ), const Gap(4), Text( - product.price != 0 ? Strings.paymentConfirmationBottom(product.price) : 'Get your free product', + product.price != 0 + ? Strings.paymentConfirmationBottom(product.price) + : 'Get your free product', style: AppTextStyle.price, ), const Gap(12), @@ -84,10 +86,12 @@ class _BottomModalSheetButtonBar extends StatefulWidget { final Product product; @override - State<_BottomModalSheetButtonBar> createState() => _BottomModalSheetButtonBarState(); + State<_BottomModalSheetButtonBar> createState() => + _BottomModalSheetButtonBarState(); } -class _BottomModalSheetButtonBarState extends State<_BottomModalSheetButtonBar> { +class _BottomModalSheetButtonBarState + extends State<_BottomModalSheetButtonBar> { @override Widget build(BuildContext context) { if (widget.product.price == 0) { @@ -187,7 +191,9 @@ class _BottomModalSheetButton extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( - child: (disabled && disabledText != null) ? _withDisabledText(_button, disabledText!) : _button, + child: (disabled && disabledText != null) + ? _withDisabledText(_button, disabledText!) + : _button, ); } } diff --git a/openapi/coffeecard_api_v2.swagger.json b/openapi/coffeecard_api_v2.swagger.json index 93b3fd2ce..f39cb42de 100644 --- a/openapi/coffeecard_api_v2.swagger.json +++ b/openapi/coffeecard_api_v2.swagger.json @@ -1541,6 +1541,7 @@ }, "dateUsed": { "type": "string", + "nullable": true, "description": "Used date time for ticket in Utc format", "format": "date-time", "nullable": true, From e803aeecb911ad34af82d3e09697a64eebee0691 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 6 Dec 2022 17:16:15 +0100 Subject: [PATCH 03/12] Updated to latest version of OpenApiSpec --- lib/models/purchase/payment.dart | 3 -- lib/payment/free_product_service.dart | 12 ++------ lib/payment/mobilepay_service.dart | 15 ++++++--- openapi/coffeecard_api_v2.swagger.json | 42 +++++--------------------- 4 files changed, 21 insertions(+), 51 deletions(-) diff --git a/lib/models/purchase/payment.dart b/lib/models/purchase/payment.dart index 175421330..1a5faf686 100644 --- a/lib/models/purchase/payment.dart +++ b/lib/models/purchase/payment.dart @@ -2,7 +2,6 @@ import 'package:coffeecard/models/purchase/payment_status.dart'; class Payment { final int id; - final String paymentId; final PaymentStatus status; final String deeplink; final int price; @@ -14,7 +13,6 @@ class Payment { required this.id, required this.price, required this.purchaseTime, - required this.paymentId, required this.status, required this.deeplink, required this.productId, @@ -33,7 +31,6 @@ class Payment { }) { return Payment( id: id ?? this.id, - paymentId: paymentId ?? this.paymentId, status: status ?? this.status, deeplink: deeplink ?? this.deeplink, price: price ?? this.price, diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index 609a157ca..20a8b281e 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -23,15 +23,9 @@ class FreeProductService extends PaymentHandler { final response = await _repository.initiatePurchase(productId, PaymentType.freepurchase); - return response.fold((l) => Left(l), (r) { - final paymentDetails = FreeProductPaymentDetails.fromJsonFactory( - r.paymentDetails, - ); - - return Right( + return response.fold((l) => Left(l), (r) => Right( Payment( id: r.id, - paymentId: paymentDetails.paymentId!, status: PaymentStatus.completed, deeplink: '', purchaseTime: r.dateCreated, @@ -39,7 +33,7 @@ class FreeProductService extends PaymentHandler { productId: r.productId, productName: r.productName, ), - ); - }); + ), + ); } } diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index e498b5d66..f5e38fa3f 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -23,11 +23,16 @@ class MobilePayService extends PaymentHandler { _context = context; @override - Future> initPurchase(int productId) async { - final response = await _repository.initiatePurchase( - productId, - PaymentType.mobilepay, - ); + Future> initPurchase(int productId) async { + final Either response; + try { + response = await _repository.initiatePurchase( + productId, + PaymentType.mobilepay, + ); + } catch (e) { + return Left(RequestFailure(e.toString())); + } return response.map( (response) { diff --git a/openapi/coffeecard_api_v2.swagger.json b/openapi/coffeecard_api_v2.swagger.json index f39cb42de..715ef3dfc 100644 --- a/openapi/coffeecard_api_v2.swagger.json +++ b/openapi/coffeecard_api_v2.swagger.json @@ -1271,8 +1271,6 @@ "required": [ "paymentType", "orderId", - "paymentId", - "purchaseStatus", "discriminator" ], "properties": { @@ -1291,21 +1289,6 @@ "minLength": 1, "example": "f5cb3e0f-3b9b-4f50-8c4f-a7450f300a5c" }, - "paymentId": { - "type": "string", - "description": "Id for a payment", - "minLength": 1, - "example": "186d2b31-ff25-4414-9fd1-bfe9807fa8b7" - }, - "purchaseStatus": { - "description": "The status of the payment for the purchase", - "example": "PurchaseStatus.PendingPayment", - "oneOf": [ - { - "$ref": "#/components/schemas/PurchaseStatus" - } - ] - }, "discriminator": { "type": "string" } @@ -1354,6 +1337,12 @@ "minLength": 1, "example": "mobilepay://merchant_payments?payment_id=186d2b31-ff25-4414-9fd1-bfe9807fa8b7" }, + "paymentId": { + "type": "string", + "description": "MobilePay Id for a payment", + "minLength": 1, + "example": "186d2b31-ff25-4414-9fd1-bfe9807fa8b7" + }, "state": { "type": "string", "description": "MobilePay state", @@ -1367,7 +1356,7 @@ } ] }, - "FreeProductPaymentDetails": { + "FreePurchasePaymentDetails": { "allOf": [ { "$ref": "#/components/schemas/PaymentDetails" @@ -1394,21 +1383,6 @@ } ] }, - "MessageResponseDto": { - "type": "object", - "description": "Simple response class with a string message", - "example": { - "message": "Successful completion" - }, - "additionalProperties": false, - "properties": { - "message": { - "type": "string", - "description": "Message with API response", - "example": "Successful completion" - } - } - }, "InitiatePurchaseResponse": { "type": "object", "description": "Response object to a purchase request containing purchase information and reference to payment provider", @@ -1541,10 +1515,10 @@ }, "dateUsed": { "type": "string", - "nullable": true, "description": "Used date time for ticket in Utc format", "format": "date-time", "nullable": true, + "minLength": 1, "example": "2022-01-09T21:03:52.2283208Z" }, "productId": { From 761edffebc8b3d4b935fb2fef3209f28ac9731df Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 14 Feb 2023 16:40:21 +0100 Subject: [PATCH 04/12] Receipts now use v2 endpoint, user can purchase free products in the app --- lib/cubits/purchase/purchase_cubit.dart | 4 +- lib/cubits/receipt/receipt_cubit.dart | 2 +- .../repositories/v1/receipt_repository.dart | 40 ---------- .../repositories/v2/receipt_repository.dart | 75 +++++++++++++++++++ lib/models/receipts/receipt.dart | 13 ++-- lib/payment/free_product_service.dart | 1 - lib/service_locator.dart | 12 ++- lib/widgets/pages/home_page.dart | 5 +- openapi/coffeecard_api_v2.swagger.json | 1 - test/cubits/receipt/receipt_cubit_test.dart | 3 +- 10 files changed, 101 insertions(+), 55 deletions(-) create mode 100644 lib/data/repositories/v2/receipt_repository.dart diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart index 56589b124..1c0739c07 100644 --- a/lib/cubits/purchase/purchase_cubit.dart +++ b/lib/cubits/purchase/purchase_cubit.dart @@ -44,8 +44,8 @@ class PurchaseCubit extends Cubit { final either = await service.initPurchase(product.id); either.fold((error) => emit(PurchaseError(error.message)), (payment) { if (payment.status != PaymentStatus.error) { - emit(PurchaseProcessing(payment)); - verifyPurchase(); + emit(PurchaseCompleted(payment)); + //verifyPurchase(); } else { emit(PurchasePaymentRejected(payment)); } diff --git a/lib/cubits/receipt/receipt_cubit.dart b/lib/cubits/receipt/receipt_cubit.dart index 274eca0f5..8ca7bcf56 100644 --- a/lib/cubits/receipt/receipt_cubit.dart +++ b/lib/cubits/receipt/receipt_cubit.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/data/repositories/v1/receipt_repository.dart'; +import 'package:coffeecard/data/repositories/v2/receipt_repository.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/data/repositories/v1/receipt_repository.dart b/lib/data/repositories/v1/receipt_repository.dart index f978fbbf8..e69de29bb 100644 --- a/lib/data/repositories/v1/receipt_repository.dart +++ b/lib/data/repositories/v1/receipt_repository.dart @@ -1,40 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; -import 'package:coffeecard/models/receipts/receipt.dart'; -import 'package:dartz/dartz.dart'; - -class ReceiptRepository { - ReceiptRepository({ - required this.apiV1, - required this.executor, - }); - - final CoffeecardApi apiV1; - final NetworkRequestExecutor executor; - - /// Retrieves all of the users receipts - /// This includes both their used tickets and purchased tickets - Future>> getUserReceipts() async { - final usedTicketsEither = await executor( - () => apiV1.apiV1TicketsGet(used: true), - ); - - final purchasedTicketsEither = await executor( - apiV1.apiV1PurchasesGet, - ); - - return usedTicketsEither.bind( - (usedTickets) => purchasedTicketsEither.map( - (purchasedTickets) { - final allTickets = [ - ...usedTickets.map(Receipt.fromTicketDTO), - ...purchasedTickets.map(Receipt.fromPurchaseDTO) - ]; - allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); - return allTickets; - }, - ), - ); - } -} diff --git a/lib/data/repositories/v2/receipt_repository.dart b/lib/data/repositories/v2/receipt_repository.dart new file mode 100644 index 000000000..082c471e0 --- /dev/null +++ b/lib/data/repositories/v2/receipt_repository.dart @@ -0,0 +1,75 @@ +import 'package:coffeecard/data/repositories/utils/executor.dart'; +import 'package:coffeecard/data/repositories/utils/request_types.dart'; +import 'package:coffeecard/data/repositories/v1/product_repository.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:coffeecard/models/receipts/receipt.dart'; +import 'package:coffeecard/models/ticket/product.dart'; +import 'package:dartz/dartz.dart'; + +class ReceiptRepository { + ReceiptRepository({ + required this.productRepository, + required this.apiV2, + required this.executor, + }); + + final CoffeecardApiV2 apiV2; + final ProductRepository productRepository; + final Executor executor; + + /// Retrieves all of the users receipts + /// This includes both their used tickets and purchased tickets + Future>> getUserReceipts() async { + final productsEither = await productRepository.getProducts(); + + var productMap = {}; + productsEither.map((products) => + productMap = {for (var product in products) product.id: product}); + + if (productsEither.isLeft()) { + return Left( + (productsEither as Left>).value); + } + + final usedTicketsFutureEither = executor.execute( + () => apiV2.apiV2TicketsGet(includeUsed: true), + (dto) => dto.map(Receipt.fromTicketResponse), + ); + + final purchasedTicketsFutureEither = executor.execute( + apiV2.apiV2PurchasesGet, + (dto) => dto.map( + (purchase) => Receipt.fromSimplePurchaseResponse( + purchase, + productMap.containsKey(purchase.productId) + ? productMap[purchase.productId]! + : const Product( + price: 0, + amount: 0, + name: 'Unknown product', + id: 0, + description: + 'We could not find this product in our database'), // Fixme better default + ), + ), + ); + + final usedTicketsEither = await usedTicketsFutureEither; + + return usedTicketsEither.fold( + (l) => Left(l), + (usedTickets) async { + final purchasedTicketsEither = await purchasedTicketsFutureEither; + + return purchasedTicketsEither.fold( + (l) => Left(l), + (purchasedTickets) { + final allTickets = [...usedTickets, ...purchasedTickets]; + allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); + return Right(allTickets); + }, + ); + }, + ); + } +} diff --git a/lib/models/receipts/receipt.dart b/lib/models/receipts/receipt.dart index 6bac01d6b..fbae967c6 100644 --- a/lib/models/receipts/receipt.dart +++ b/lib/models/receipts/receipt.dart @@ -1,4 +1,5 @@ -import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; +import 'package:coffeecard/models/ticket/product.dart'; enum TransactionType { purchase, ticketSwipe, placeholder } @@ -21,7 +22,7 @@ class Receipt { }); /// Creates a receipt from a used ticket DTO - Receipt.fromTicketDTO(TicketDto dto) + Receipt.fromTicketResponse(TicketResponse dto) : productName = dto.productName, transactionType = TransactionType.ticketSwipe, timeUsed = dto @@ -31,11 +32,11 @@ class Receipt { id = dto.id; /// Creates a receipt from a purchase DTO - Receipt.fromPurchaseDTO(PurchaseDto dto) - : productName = dto.productName, + Receipt.fromSimplePurchaseResponse(SimplePurchaseResponse dto, Product product) + : productName = product.name, transactionType = TransactionType.purchase, timeUsed = dto.dateCreated, - price = dto.price, - amountPurchased = dto.numberOfTickets, + price = product.price, + amountPurchased = product.amount, id = dto.id; } diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index 20a8b281e..02da4fa3f 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -1,7 +1,6 @@ import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; -import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/payment_handler.dart'; diff --git a/lib/service_locator.dart b/lib/service_locator.dart index b5884bd36..caa29f6db 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -5,12 +5,13 @@ import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart import 'package:coffeecard/data/repositories/external/contributor_repository.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; -import 'package:coffeecard/data/repositories/v1/receipt_repository.dart'; +import 'package:coffeecard/data/repositories/v1/programme_repository.dart'; import 'package:coffeecard/data/repositories/v1/ticket_repository.dart'; import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; +import 'package:coffeecard/data/repositories/v2/receipt_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/occupation/data/datasources/occupation_remote_data_source.dart'; @@ -93,7 +94,14 @@ void configureServices() { ); // Repositories - // v1 + sl.registerFactory( + () => OccupationRepository( + productRepository: sl(), + apiV2: sl(), + executor: sl(), + ), + ); + sl.registerFactory( () => ReceiptRepository( apiV1: sl(), diff --git a/lib/widgets/pages/home_page.dart b/lib/widgets/pages/home_page.dart index 263b87a0c..4f040f3d3 100644 --- a/lib/widgets/pages/home_page.dart +++ b/lib/widgets/pages/home_page.dart @@ -6,9 +6,12 @@ import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; import 'package:coffeecard/cubits/tickets/tickets_cubit.dart'; -import 'package:coffeecard/data/repositories/v1/receipt_repository.dart'; +import 'package:coffeecard/cubits/user/user_cubit.dart'; +import 'package:coffeecard/data/repositories/shared/account_repository.dart'; +import 'package:coffeecard/data/repositories/v1/occupation_repository.dart'; import 'package:coffeecard/data/repositories/v1/ticket_repository.dart'; import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; +import 'package:coffeecard/data/repositories/v2/receipt_repository.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/service_locator.dart'; diff --git a/openapi/coffeecard_api_v2.swagger.json b/openapi/coffeecard_api_v2.swagger.json index 715ef3dfc..419531b96 100644 --- a/openapi/coffeecard_api_v2.swagger.json +++ b/openapi/coffeecard_api_v2.swagger.json @@ -1518,7 +1518,6 @@ "description": "Used date time for ticket in Utc format", "format": "date-time", "nullable": true, - "minLength": 1, "example": "2022-01-09T21:03:52.2283208Z" }, "productId": { diff --git a/test/cubits/receipt/receipt_cubit_test.dart b/test/cubits/receipt/receipt_cubit_test.dart index 4bd71cfb4..a133eab8f 100644 --- a/test/cubits/receipt/receipt_cubit_test.dart +++ b/test/cubits/receipt/receipt_cubit_test.dart @@ -2,7 +2,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; -import 'package:coffeecard/data/repositories/v1/receipt_repository.dart'; +import 'package:coffeecard/data/repositories/utils/request_types.dart'; +import 'package:coffeecard/data/repositories/v2/receipt_repository.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; From 6101aebfb89b300cfb5cbd73da66c1a71e240cdf Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 14 Feb 2023 18:14:20 +0100 Subject: [PATCH 05/12] Fixes after rebase --- lib/payment/mobilepay_service.dart | 3 ++- lib/service_locator.dart | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index f5e38fa3f..f504b93f1 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:coffeecard/models/purchase/initiate_purchase.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/payment_handler.dart'; @@ -24,7 +25,7 @@ class MobilePayService extends PaymentHandler { @override Future> initPurchase(int productId) async { - final Either response; + final Either response; try { response = await _repository.initiatePurchase( productId, diff --git a/lib/service_locator.dart b/lib/service_locator.dart index caa29f6db..e03b49904 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -5,7 +5,6 @@ import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart import 'package:coffeecard/data/repositories/external/contributor_repository.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; -import 'package:coffeecard/data/repositories/v1/programme_repository.dart'; import 'package:coffeecard/data/repositories/v1/ticket_repository.dart'; import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; @@ -96,15 +95,15 @@ void configureServices() { // Repositories sl.registerFactory( () => OccupationRepository( - productRepository: sl(), - apiV2: sl(), + apiV1: sl(), executor: sl(), ), ); sl.registerFactory( () => ReceiptRepository( - apiV1: sl(), + productRepository: sl(), + apiV2: sl(), executor: sl(), ), ); From 553d1a6e727b98213b73abc10f2c8a6027cd77ae Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Sat, 18 Mar 2023 13:30:41 +0100 Subject: [PATCH 06/12] Better handling of purchase status Fixing some linting issues. --- .../repositories/v2/receipt_repository.dart | 13 ++++++---- lib/models/purchase/payment_status.dart | 24 +++++++++++++++--- lib/models/purchase/single_purchase.dart | 9 ++++--- lib/models/receipts/receipt.dart | 6 +++-- lib/payment/free_product_service.dart | 4 ++- lib/payment/mobilepay_service.dart | 1 + lib/payment/payment_handler.dart | 25 +++++++------------ 7 files changed, 52 insertions(+), 30 deletions(-) diff --git a/lib/data/repositories/v2/receipt_repository.dart b/lib/data/repositories/v2/receipt_repository.dart index 082c471e0..dee80535f 100644 --- a/lib/data/repositories/v2/receipt_repository.dart +++ b/lib/data/repositories/v2/receipt_repository.dart @@ -23,12 +23,15 @@ class ReceiptRepository { final productsEither = await productRepository.getProducts(); var productMap = {}; - productsEither.map((products) => - productMap = {for (var product in products) product.id: product}); + productsEither.map( + (products) => + productMap = {for (var product in products) product.id: product}, + ); if (productsEither.isLeft()) { return Left( - (productsEither as Left>).value); + (productsEither as Left>).value, + ); } final usedTicketsFutureEither = executor.execute( @@ -48,8 +51,8 @@ class ReceiptRepository { amount: 0, name: 'Unknown product', id: 0, - description: - 'We could not find this product in our database'), // Fixme better default + description: 'We could not find this product in our database', + ), // Fixme better default ), ), ); diff --git a/lib/models/purchase/payment_status.dart b/lib/models/purchase/payment_status.dart index 995ed7d90..3a8ae8350 100644 --- a/lib/models/purchase/payment_status.dart +++ b/lib/models/purchase/payment_status.dart @@ -1,3 +1,5 @@ +import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; + enum PaymentStatus { /// Payment is completed completed, @@ -8,11 +10,27 @@ enum PaymentStatus { /// Payment is not yet complete reserved, - // User has not approved the purchase + /// User has not approved the purchase awaitingPayment, - // User has rejected payment + /// User has rejected payment rejectedPayment, - awaitingCompletionAfterRetry, + /// Payment has been refunded + refunded; + + static PaymentStatus fromPurchaseStatus(PurchaseStatus status) { + switch (status) { + case PurchaseStatus.swaggerGeneratedUnknown: + return PaymentStatus.error; + case PurchaseStatus.completed: + return PaymentStatus.completed; + case PurchaseStatus.cancelled: + return PaymentStatus.rejectedPayment; + case PurchaseStatus.pendingpayment: + return PaymentStatus.awaitingPayment; + case PurchaseStatus.refunded: + return PaymentStatus.refunded; + } + } } diff --git a/lib/models/purchase/single_purchase.dart b/lib/models/purchase/single_purchase.dart index 7879868e6..2f62ca7e5 100644 --- a/lib/models/purchase/single_purchase.dart +++ b/lib/models/purchase/single_purchase.dart @@ -1,17 +1,18 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:coffeecard/models/purchase/payment_status.dart'; class SinglePurchase { final int id; final int totalAmount; final Map paymentDetails; - final String purchaseStatus; + final PaymentStatus status; final DateTime dateCreated; const SinglePurchase({ required this.id, required this.totalAmount, required this.paymentDetails, - required this.purchaseStatus, + required this.status, required this.dateCreated, }); @@ -19,6 +20,8 @@ class SinglePurchase { : id = dto.id, totalAmount = dto.totalAmount, paymentDetails = dto.paymentDetails as Map, - purchaseStatus = dto.purchaseStatus as String, + status = PaymentStatus.fromPurchaseStatus( + purchaseStatusFromJson(dto.purchaseStatus), + ), dateCreated = dto.dateCreated; } diff --git a/lib/models/receipts/receipt.dart b/lib/models/receipts/receipt.dart index fbae967c6..80c9d9290 100644 --- a/lib/models/receipts/receipt.dart +++ b/lib/models/receipts/receipt.dart @@ -32,8 +32,10 @@ class Receipt { id = dto.id; /// Creates a receipt from a purchase DTO - Receipt.fromSimplePurchaseResponse(SimplePurchaseResponse dto, Product product) - : productName = product.name, + Receipt.fromSimplePurchaseResponse( + SimplePurchaseResponse dto, + Product product, + ) : productName = product.name, transactionType = TransactionType.purchase, timeUsed = dto.dateCreated, price = product.price, diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index 02da4fa3f..0cb2539ac 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -22,7 +22,9 @@ class FreeProductService extends PaymentHandler { final response = await _repository.initiatePurchase(productId, PaymentType.freepurchase); - return response.fold((l) => Left(l), (r) => Right( + return response.fold( + (l) => Left(l), + (r) => Right( Payment( id: r.id, status: PaymentStatus.completed, diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index f504b93f1..3f0029fc1 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:coffeecard/core/errors/failures.dart'; diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index f9b00c8f3..07796ad83 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -1,7 +1,5 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; -import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; -import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/free_product_service.dart'; @@ -27,7 +25,9 @@ abstract class PaymentHandler { _context = context; static PaymentHandler createPaymentHandler( - InternalPaymentType paymentType, BuildContext context) { + InternalPaymentType paymentType, + BuildContext context, + ) { final repository = sl.get(); switch (paymentType) { @@ -43,21 +43,14 @@ abstract class PaymentHandler { Future> initPurchase(int productId); Future> verifyPurchase( - int purchaseId) async { + int purchaseId, + ) async { // Call API endpoint, receive PaymentStatus final either = await _repository.getPurchase(purchaseId); - return either.fold((error) => Left(error), (purchase) { - final paymentDetails = FreeProductPaymentDetails.fromJson( - purchase.paymentDetails, - ); - - final status = purchaseStatusFromJson(paymentDetails.purchaseStatus); - if (status == PurchaseStatus.completed) { - return const Right(PaymentStatus.completed); - } - // TODO: Cover more cases for PurchaseStatus - return const Right(PaymentStatus.error); - }); + return either.fold( + (error) => Left(error), + (purchase) => Right(purchase.status), + ); } } From da0c7e8c495d97ff3692122843ec56a8f5437e04 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Wed, 29 Mar 2023 12:32:53 +0200 Subject: [PATCH 07/12] Fixes after rebase --- lib/cubits/purchase/purchase_cubit.dart | 10 +--- .../repositories/v1/receipt_repository.dart | 1 + .../repositories/v2/receipt_repository.dart | 56 +++++-------------- lib/models/receipts/receipt.dart | 8 +-- lib/payment/free_product_service.dart | 9 +-- lib/payment/mobilepay_service.dart | 18 ++---- lib/payment/payment_handler.dart | 9 +-- .../tickets/swipe_ticket_confirm.dart | 2 + openapi/coffeecard_api_v2.swagger.json | 2 +- test/cubits/receipt/receipt_cubit_test.dart | 1 - 10 files changed, 38 insertions(+), 78 deletions(-) diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart index 1c0739c07..5931fac79 100644 --- a/lib/cubits/purchase/purchase_cubit.dart +++ b/lib/cubits/purchase/purchase_cubit.dart @@ -7,7 +7,6 @@ import 'package:coffeecard/payment/mobilepay_service.dart'; import 'package:coffeecard/payment/payment_handler.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; -import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; part 'purchase_state.dart'; @@ -19,16 +18,11 @@ class PurchaseCubit extends Cubit { PurchaseCubit({required this.paymentHandler, required this.product}) : super(const PurchaseInitial()); - Future _payWithApplePay() async { - // TODO: implement me - throw UnimplementedError(); - } - Future _payWithMobilePay(MobilePayService service) async { final either = await service.initPurchase(product.id); either.fold( - (error) => emit(PurchaseError(error.message)), + (error) => emit(PurchaseError(error.reason)), (payment) async { if (payment.status != PaymentStatus.error) { emit(PurchaseProcessing(payment)); @@ -42,7 +36,7 @@ class PurchaseCubit extends Cubit { Future _payWithFreeProduct(FreeProductService service) async { final either = await service.initPurchase(product.id); - either.fold((error) => emit(PurchaseError(error.message)), (payment) { + either.fold((error) => emit(PurchaseError(error.reason)), (payment) { if (payment.status != PaymentStatus.error) { emit(PurchaseCompleted(payment)); //verifyPurchase(); diff --git a/lib/data/repositories/v1/receipt_repository.dart b/lib/data/repositories/v1/receipt_repository.dart index e69de29bb..8b1378917 100644 --- a/lib/data/repositories/v1/receipt_repository.dart +++ b/lib/data/repositories/v1/receipt_repository.dart @@ -0,0 +1 @@ + diff --git a/lib/data/repositories/v2/receipt_repository.dart b/lib/data/repositories/v2/receipt_repository.dart index dee80535f..bd0fbc68b 100644 --- a/lib/data/repositories/v2/receipt_repository.dart +++ b/lib/data/repositories/v2/receipt_repository.dart @@ -1,9 +1,8 @@ -import 'package:coffeecard/data/repositories/utils/executor.dart'; -import 'package:coffeecard/data/repositories/utils/request_types.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:dartz/dartz.dart'; class ReceiptRepository { @@ -15,61 +14,36 @@ class ReceiptRepository { final CoffeecardApiV2 apiV2; final ProductRepository productRepository; - final Executor executor; + final NetworkRequestExecutor executor; /// Retrieves all of the users receipts /// This includes both their used tickets and purchased tickets - Future>> getUserReceipts() async { - final productsEither = await productRepository.getProducts(); - - var productMap = {}; - productsEither.map( - (products) => - productMap = {for (var product in products) product.id: product}, - ); - - if (productsEither.isLeft()) { - return Left( - (productsEither as Left>).value, - ); - } - - final usedTicketsFutureEither = executor.execute( + Future>> getUserReceipts() async { + final usedTicketsFutureEither = executor( () => apiV2.apiV2TicketsGet(includeUsed: true), - (dto) => dto.map(Receipt.fromTicketResponse), ); - - final purchasedTicketsFutureEither = executor.execute( + final purchasedTicketsFutureEither = executor( apiV2.apiV2PurchasesGet, - (dto) => dto.map( + ); + + final usedTicketsEither = (await usedTicketsFutureEither) + .map((dto) => dto.map(Receipt.fromTicketResponse)); + final purchasedTicketsEither = (await purchasedTicketsFutureEither).map( + (r) => r.map( (purchase) => Receipt.fromSimplePurchaseResponse( purchase, - productMap.containsKey(purchase.productId) - ? productMap[purchase.productId]! - : const Product( - price: 0, - amount: 0, - name: 'Unknown product', - id: 0, - description: 'We could not find this product in our database', - ), // Fixme better default ), ), ); - final usedTicketsEither = await usedTicketsFutureEither; - return usedTicketsEither.fold( (l) => Left(l), - (usedTickets) async { - final purchasedTicketsEither = await purchasedTicketsFutureEither; - - return purchasedTicketsEither.fold( - (l) => Left(l), + (usedTickets) { + return purchasedTicketsEither.map( (purchasedTickets) { final allTickets = [...usedTickets, ...purchasedTickets]; allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); - return Right(allTickets); + return allTickets; }, ); }, diff --git a/lib/models/receipts/receipt.dart b/lib/models/receipts/receipt.dart index 80c9d9290..ceab956d0 100644 --- a/lib/models/receipts/receipt.dart +++ b/lib/models/receipts/receipt.dart @@ -1,5 +1,4 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; -import 'package:coffeecard/models/ticket/product.dart'; enum TransactionType { purchase, ticketSwipe, placeholder } @@ -34,11 +33,10 @@ class Receipt { /// Creates a receipt from a purchase DTO Receipt.fromSimplePurchaseResponse( SimplePurchaseResponse dto, - Product product, - ) : productName = product.name, + ) : productName = dto.productName, transactionType = TransactionType.purchase, timeUsed = dto.dateCreated, - price = product.price, - amountPurchased = product.amount, + price = dto.totalAmount, + amountPurchased = dto.numberOfTickets, id = dto.id; } diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index 0cb2539ac..d5f1a751d 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -1,24 +1,21 @@ -import 'package:coffeecard/data/repositories/utils/request_types.dart'; +import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/payment_handler.dart'; import 'package:dartz/dartz.dart'; -import 'package:flutter/widgets.dart'; class FreeProductService extends PaymentHandler { final PurchaseRepository _repository; - final BuildContext _context; const FreeProductService({ required super.repository, required super.context, - }) : _repository = repository, - _context = context; + }) : _repository = repository; @override - Future> initPurchase(int productId) async { + Future> initPurchase(int productId) async { final response = await _repository.initiatePurchase(productId, PaymentType.freepurchase); diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index 3f0029fc1..ba88b4aed 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:coffeecard/core/errors/failures.dart'; @@ -25,16 +24,12 @@ class MobilePayService extends PaymentHandler { _context = context; @override - Future> initPurchase(int productId) async { - final Either response; - try { - response = await _repository.initiatePurchase( - productId, - PaymentType.mobilepay, - ); - } catch (e) { - return Left(RequestFailure(e.toString())); - } + Future> initPurchase(int productId) async { + final Either response; + response = await _repository.initiatePurchase( + productId, + PaymentType.mobilepay, + ); return response.map( (response) { @@ -44,7 +39,6 @@ class MobilePayService extends PaymentHandler { return Payment( id: response.id, - paymentId: paymentDetails.paymentId, status: PaymentStatus.awaitingPayment, deeplink: paymentDetails.mobilePayAppRedirectUri, purchaseTime: response.dateCreated, diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index 07796ad83..115541812 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -16,6 +16,8 @@ enum InternalPaymentType { abstract class PaymentHandler { final PurchaseRepository _repository; + // Certain implementations of the payment handler require access to the build context, even if it does not do so itself. + // ignore: unused_field final BuildContext _context; const PaymentHandler({ @@ -42,15 +44,14 @@ abstract class PaymentHandler { Future> initPurchase(int productId); - Future> verifyPurchase( + Future> verifyPurchase( int purchaseId, ) async { // Call API endpoint, receive PaymentStatus final either = await _repository.getPurchase(purchaseId); - return either.fold( - (error) => Left(error), - (purchase) => Right(purchase.status), + return either.map( + (purchase) => purchase.status, ); } } diff --git a/lib/widgets/components/tickets/swipe_ticket_confirm.dart b/lib/widgets/components/tickets/swipe_ticket_confirm.dart index 7c6947332..0b65a0c61 100644 --- a/lib/widgets/components/tickets/swipe_ticket_confirm.dart +++ b/lib/widgets/components/tickets/swipe_ticket_confirm.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; import 'package:coffeecard/cubits/tickets/tickets_cubit.dart'; import 'package:coffeecard/widgets/components/card.dart'; import 'package:coffeecard/widgets/components/tickets/bottom_modal_sheet_helper.dart'; @@ -133,6 +134,7 @@ class _ModalContentState extends State<_ModalContent> widget.context .read() .useTicket(widget.productId); + widget.context.read().fetchReceipts(); }, ), ), diff --git a/openapi/coffeecard_api_v2.swagger.json b/openapi/coffeecard_api_v2.swagger.json index 419531b96..c678651c5 100644 --- a/openapi/coffeecard_api_v2.swagger.json +++ b/openapi/coffeecard_api_v2.swagger.json @@ -17,7 +17,7 @@ }, "servers": [ { - "url": "https://beta.analogio.dk/api/clippy" + "url": "https://localhost:5001" } ], "paths": { diff --git a/test/cubits/receipt/receipt_cubit_test.dart b/test/cubits/receipt/receipt_cubit_test.dart index a133eab8f..554bc9ce5 100644 --- a/test/cubits/receipt/receipt_cubit_test.dart +++ b/test/cubits/receipt/receipt_cubit_test.dart @@ -2,7 +2,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; -import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v2/receipt_repository.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; import 'package:dartz/dartz.dart'; From 63138c7809e715aa772f83d130f93d9ac333498f Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 11 Apr 2023 20:24:26 +0200 Subject: [PATCH 08/12] Refactor of payment service and purchase cubit --- lib/base/strings.dart | 3 +- lib/cubits/purchase/purchase_cubit.dart | 49 ++++-------------- .../repositories/v1/receipt_repository.dart | 1 - lib/payment/mobilepay_service.dart | 51 ++++++++++++------- lib/payment/payment_handler.dart | 1 - .../buy_ticket_bottom_modal_sheet.dart | 4 +- openapi/coffeecard_api_v2.swagger.json | 2 +- 7 files changed, 50 insertions(+), 61 deletions(-) delete mode 100644 lib/data/repositories/v1/receipt_repository.dart diff --git a/lib/base/strings.dart b/lib/base/strings.dart index de8dd2f06..a3f2951bb 100644 --- a/lib/base/strings.dart +++ b/lib/base/strings.dart @@ -138,6 +138,7 @@ abstract class Strings { static const confirmSwipe = 'Confirm use of ticket'; static const confirmPurchase = 'Confirm purchase'; static const tapHereToCancel = 'Tap here to cancel'; + static const String paymentConfirmationButtonRedeem = 'Get your free product'; static String paymentConfirmationTopTickets(int amount, String title) { return "You're buying $amount $title tickets"; @@ -147,7 +148,7 @@ abstract class Strings { return "You're buying and swiping $amount $title"; } - static String paymentConfirmationBottom(int price) { + static String paymentConfirmationBottomPurchase(int price) { return 'Pay $price,- with...'; } diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart index 5931fac79..c995b7c78 100644 --- a/lib/cubits/purchase/purchase_cubit.dart +++ b/lib/cubits/purchase/purchase_cubit.dart @@ -2,8 +2,6 @@ import 'package:bloc/bloc.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/payment/free_product_service.dart'; -import 'package:coffeecard/payment/mobilepay_service.dart'; import 'package:coffeecard/payment/payment_handler.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; @@ -18,48 +16,23 @@ class PurchaseCubit extends Cubit { PurchaseCubit({required this.paymentHandler, required this.product}) : super(const PurchaseInitial()); - Future _payWithMobilePay(MobilePayService service) async { - final either = await service.initPurchase(product.id); - - either.fold( - (error) => emit(PurchaseError(error.reason)), - (payment) async { - if (payment.status != PaymentStatus.error) { - emit(PurchaseProcessing(payment)); - await service.invokePaymentMethod(Uri.parse(payment.deeplink)); - } else { - emit(PurchasePaymentRejected(payment)); - } - }, - ); - } - - Future _payWithFreeProduct(FreeProductService service) async { - final either = await service.initPurchase(product.id); - either.fold((error) => emit(PurchaseError(error.reason)), (payment) { - if (payment.status != PaymentStatus.error) { - emit(PurchaseCompleted(payment)); - //verifyPurchase(); - } else { - emit(PurchasePaymentRejected(payment)); - } - }); - } - Future pay() async { sl().beginCheckoutEvent(product); if (state is PurchaseInitial) { emit(const PurchaseStarted()); - final service = paymentHandler; - if (service is MobilePayService) { - _payWithMobilePay(service); - } else if (service is FreeProductService) { - _payWithFreeProduct(service); - } else { - emit(const PurchaseError('Unknown payment handler')); - } + final either = await paymentHandler.initPurchase(product.id); + + either.fold((error) => emit(PurchaseError(error.reason)), (payment) { + if (payment.status == PaymentStatus.completed) { + emit(PurchaseCompleted(payment)); + } else if (payment.status == PaymentStatus.awaitingPayment) { + emit(PurchaseProcessing(payment)); + } else { + emit(PurchasePaymentRejected(payment)); + } + }); } } diff --git a/lib/data/repositories/v1/receipt_repository.dart b/lib/data/repositories/v1/receipt_repository.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/data/repositories/v1/receipt_repository.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index ba88b4aed..f84d7bb96 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -31,7 +31,7 @@ class MobilePayService extends PaymentHandler { PaymentType.mobilepay, ); - return response.map( + final either = response.map( (response) { final paymentDetails = MobilePayPaymentDetails.fromJsonFactory( response.paymentDetails, @@ -48,25 +48,42 @@ class MobilePayService extends PaymentHandler { ); }, ); + + await _invokeMobilePayApp(either); // Sideeffect + + return either; } - Future invokePaymentMethod(Uri uri) async { - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - final Uri url; + Future _invokeMobilePayApp( + Either paymentEither, + ) async { + paymentEither.map( + (payment) async { + final Uri mobilepayLink = Uri.parse(payment.deeplink); + + if (await canLaunchUrl(mobilepayLink)) { + await launchUrl(mobilepayLink, mode: LaunchMode.externalApplication); + + return; + } else { + final Uri url = _getAppStoreUri(); - // MobilePay not installed, send user to appstore - if (Platform.isAndroid) { - url = ApiUriConstants.mobilepayAndroid; - } else if (Platform.isIOS) { - url = ApiUriConstants.mobilepayIOS; - } else { - throw UnsupportedError('Unsupported platform'); - } - if (_context.mounted) { - launchUrlExternalApplication(url, _context); - } + // MobilePay not installed, send user to appstore + if (_context.mounted) { + await launchUrlExternalApplication(url, _context); + } + } + }, + ); + } + + Uri _getAppStoreUri() { + if (Platform.isAndroid) { + return ApiUriConstants.mobilepayAndroid; + } else if (Platform.isIOS) { + return ApiUriConstants.mobilepayIOS; + } else { + throw UnsupportedError('Unsupported platform'); } } } diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index 115541812..ad74cc8cb 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart'; enum InternalPaymentType { mobilePay, - applePay, free, } diff --git a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart index e99eafa2e..7a6d95507 100644 --- a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart +++ b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart @@ -66,8 +66,8 @@ class _ModalContent extends StatelessWidget { const Gap(4), Text( product.price != 0 - ? Strings.paymentConfirmationBottom(product.price) - : 'Get your free product', + ? Strings.paymentConfirmationBottomPurchase(product.price) + : Strings.paymentConfirmationButtonRedeem, style: AppTextStyle.price, ), const Gap(12), diff --git a/openapi/coffeecard_api_v2.swagger.json b/openapi/coffeecard_api_v2.swagger.json index c678651c5..419531b96 100644 --- a/openapi/coffeecard_api_v2.swagger.json +++ b/openapi/coffeecard_api_v2.swagger.json @@ -17,7 +17,7 @@ }, "servers": [ { - "url": "https://localhost:5001" + "url": "https://beta.analogio.dk/api/clippy" } ], "paths": { From c5c8b916e1ad4a7e5f51195934f6aaa4ccfaaf71 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Sat, 15 Apr 2023 00:24:53 +0200 Subject: [PATCH 09/12] Slight refactoring based on PR comments --- lib/cubits/purchase/purchase_cubit.dart | 21 ++++++++------- lib/payment/free_product_service.dart | 27 +++++++++---------- lib/payment/mobilepay_service.dart | 17 ++++-------- lib/payment/payment_handler.dart | 23 +++++++++------- .../buy_ticket_bottom_modal_sheet.dart | 3 ++- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart index c995b7c78..dc09bd6b1 100644 --- a/lib/cubits/purchase/purchase_cubit.dart +++ b/lib/cubits/purchase/purchase_cubit.dart @@ -24,15 +24,18 @@ class PurchaseCubit extends Cubit { final either = await paymentHandler.initPurchase(product.id); - either.fold((error) => emit(PurchaseError(error.reason)), (payment) { - if (payment.status == PaymentStatus.completed) { - emit(PurchaseCompleted(payment)); - } else if (payment.status == PaymentStatus.awaitingPayment) { - emit(PurchaseProcessing(payment)); - } else { - emit(PurchasePaymentRejected(payment)); - } - }); + either.fold( + (error) => emit(PurchaseError(error.reason)), + (payment) { + if (payment.status == PaymentStatus.completed) { + emit(PurchaseCompleted(payment)); + } else if (payment.status == PaymentStatus.awaitingPayment) { + emit(PurchaseProcessing(payment)); + } else { + emit(PurchasePaymentRejected(payment)); + } + }, + ); } } diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index d5f1a751d..a0f7715df 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -1,5 +1,4 @@ import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; @@ -7,29 +6,29 @@ import 'package:coffeecard/payment/payment_handler.dart'; import 'package:dartz/dartz.dart'; class FreeProductService extends PaymentHandler { - final PurchaseRepository _repository; - const FreeProductService({ - required super.repository, + required super.purchaseRepository, required super.context, - }) : _repository = repository; + }); @override Future> initPurchase(int productId) async { - final response = - await _repository.initiatePurchase(productId, PaymentType.freepurchase); + final response = await purchaseRepository.initiatePurchase( + productId, + PaymentType.freepurchase, + ); return response.fold( - (l) => Left(l), - (r) => Right( + (error) => Left(error), + (purchase) => Right( Payment( - id: r.id, + id: purchase.id, status: PaymentStatus.completed, deeplink: '', - purchaseTime: r.dateCreated, - price: r.totalAmount, - productId: r.productId, - productName: r.productName, + purchaseTime: purchase.dateCreated, + price: purchase.totalAmount, + productId: purchase.productId, + productName: purchase.productName, ), ), ); diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index f84d7bb96..fb73f2b4e 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/purchase/initiate_purchase.dart'; import 'package:coffeecard/models/purchase/payment.dart'; @@ -10,23 +9,17 @@ import 'package:coffeecard/payment/payment_handler.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; import 'package:coffeecard/utils/launch.dart'; import 'package:dartz/dartz.dart'; -import 'package:flutter/widgets.dart'; import 'package:url_launcher/url_launcher.dart'; class MobilePayService extends PaymentHandler { - final PurchaseRepository _repository; - final BuildContext _context; - MobilePayService({ - required super.repository, + required super.purchaseRepository, required super.context, - }) : _repository = repository, - _context = context; - + }); @override Future> initPurchase(int productId) async { final Either response; - response = await _repository.initiatePurchase( + response = await purchaseRepository.initiatePurchase( productId, PaymentType.mobilepay, ); @@ -69,8 +62,8 @@ class MobilePayService extends PaymentHandler { final Uri url = _getAppStoreUri(); // MobilePay not installed, send user to appstore - if (_context.mounted) { - await launchUrlExternalApplication(url, _context); + if (context.mounted) { + await launchUrlExternalApplication(url, context); } } }, diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index ad74cc8cb..427643f67 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -14,16 +14,15 @@ enum InternalPaymentType { } abstract class PaymentHandler { - final PurchaseRepository _repository; + final PurchaseRepository purchaseRepository; // Certain implementations of the payment handler require access to the build context, even if it does not do so itself. // ignore: unused_field - final BuildContext _context; + final BuildContext context; const PaymentHandler({ - required PurchaseRepository repository, - required BuildContext context, - }) : _repository = repository, - _context = context; + required this.purchaseRepository, + required this.context, + }); static PaymentHandler createPaymentHandler( InternalPaymentType paymentType, @@ -33,9 +32,15 @@ abstract class PaymentHandler { switch (paymentType) { case InternalPaymentType.mobilePay: - return MobilePayService(repository: repository, context: context); + return MobilePayService( + purchaseRepository: repository, + context: context, + ); case InternalPaymentType.free: - return FreeProductService(repository: repository, context: context); + return FreeProductService( + purchaseRepository: repository, + context: context, + ); default: throw UnimplementedError(); } @@ -47,7 +52,7 @@ abstract class PaymentHandler { int purchaseId, ) async { // Call API endpoint, receive PaymentStatus - final either = await _repository.getPurchase(purchaseId); + final either = await purchaseRepository.getPurchase(purchaseId); return either.map( (purchase) => purchase.status, diff --git a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart index 7a6d95507..413f7b015 100644 --- a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart +++ b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart @@ -94,7 +94,8 @@ class _BottomModalSheetButtonBarState extends State<_BottomModalSheetButtonBar> { @override Widget build(BuildContext context) { - if (widget.product.price == 0) { + final isFreeProduct = widget.product.price == 0; + if (isFreeProduct) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 4ef9ae66fa6adb1ed66977cd07c9ba6092970a26 Mon Sep 17 00:00:00 2001 From: TTA777 Date: Sat, 15 Apr 2023 16:55:56 +0200 Subject: [PATCH 10/12] Refactor receipt v2 repo --- lib/data/repositories/v2/receipt_repository.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/data/repositories/v2/receipt_repository.dart b/lib/data/repositories/v2/receipt_repository.dart index bd0fbc68b..54ccf0325 100644 --- a/lib/data/repositories/v2/receipt_repository.dart +++ b/lib/data/repositories/v2/receipt_repository.dart @@ -38,15 +38,13 @@ class ReceiptRepository { return usedTicketsEither.fold( (l) => Left(l), - (usedTickets) { - return purchasedTicketsEither.map( - (purchasedTickets) { - final allTickets = [...usedTickets, ...purchasedTickets]; - allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); - return allTickets; - }, - ); - }, + (usedTickets) => purchasedTicketsEither.map( + (purchasedTickets) { + final allTickets = [...usedTickets, ...purchasedTickets]; + allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); + return allTickets; + }, + ), ); } } From 137500ae91c606a843b4bca22f3b42f3bc521954 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Sat, 15 Apr 2023 20:38:00 +0200 Subject: [PATCH 11/12] Fixes after rebase --- lib/data/repositories/v1/ticket_repository.dart | 2 +- lib/models/receipts/receipt.dart | 11 +++++++++++ lib/service_locator.dart | 7 ------- lib/widgets/pages/home_page.dart | 3 --- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/data/repositories/v1/ticket_repository.dart b/lib/data/repositories/v1/ticket_repository.dart index 5479c18da..a0872c21f 100644 --- a/lib/data/repositories/v1/ticket_repository.dart +++ b/lib/data/repositories/v1/ticket_repository.dart @@ -47,6 +47,6 @@ class TicketRepository { ), ); - return result.map(Receipt.fromTicketDTO); + return result.map(Receipt.fromTicketDto); } } diff --git a/lib/models/receipts/receipt.dart b/lib/models/receipts/receipt.dart index ceab956d0..6c89e695c 100644 --- a/lib/models/receipts/receipt.dart +++ b/lib/models/receipts/receipt.dart @@ -1,3 +1,4 @@ +import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; enum TransactionType { purchase, ticketSwipe, placeholder } @@ -20,6 +21,16 @@ class Receipt { required this.id, }); + /// Creates a receipt from a used ticket DTO + Receipt.fromTicketDto(TicketDto dto) + : productName = dto.productName, + transactionType = TransactionType.ticketSwipe, + timeUsed = dto + .dateUsed!, // will not be null as the dto is a ticket that has been used at some point + price = 1, + amountPurchased = 1, + id = dto.id; + /// Creates a receipt from a used ticket DTO Receipt.fromTicketResponse(TicketResponse dto) : productName = dto.productName, diff --git a/lib/service_locator.dart b/lib/service_locator.dart index e03b49904..8b8157cd7 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -93,13 +93,6 @@ void configureServices() { ); // Repositories - sl.registerFactory( - () => OccupationRepository( - apiV1: sl(), - executor: sl(), - ), - ); - sl.registerFactory( () => ReceiptRepository( productRepository: sl(), diff --git a/lib/widgets/pages/home_page.dart b/lib/widgets/pages/home_page.dart index 4f040f3d3..e415c1df6 100644 --- a/lib/widgets/pages/home_page.dart +++ b/lib/widgets/pages/home_page.dart @@ -6,9 +6,6 @@ import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; import 'package:coffeecard/cubits/tickets/tickets_cubit.dart'; -import 'package:coffeecard/cubits/user/user_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:coffeecard/data/repositories/v1/occupation_repository.dart'; import 'package:coffeecard/data/repositories/v1/ticket_repository.dart'; import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; import 'package:coffeecard/data/repositories/v2/receipt_repository.dart'; From aade6a9fedef186cb5e8bd6c835e81c5220a1079 Mon Sep 17 00:00:00 2001 From: TTA777 Date: Sun, 16 Apr 2023 11:06:52 +0200 Subject: [PATCH 12/12] Fixing renamed idea run config --- .../{_template__of_Flutter.xml => development.xml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .idea/runConfigurations/{_template__of_Flutter.xml => development.xml} (64%) diff --git a/.idea/runConfigurations/_template__of_Flutter.xml b/.idea/runConfigurations/development.xml similarity index 64% rename from .idea/runConfigurations/_template__of_Flutter.xml rename to .idea/runConfigurations/development.xml index ffae9332d..c613891b1 100644 --- a/.idea/runConfigurations/_template__of_Flutter.xml +++ b/.idea/runConfigurations/development.xml @@ -1,7 +1,7 @@ - + - \ No newline at end of file +