From 87ac9c17929c257cb5bdb75eb3a1c5e0a5c5cf0f Mon Sep 17 00:00:00 2001 From: Mati Date: Wed, 29 Nov 2023 17:36:23 -0300 Subject: [PATCH 1/8] feat(topup): makes the discount message be dynamic depending on the max discount PE-5138 --- lib/l10n/app_en.arb | 18 ++++++++++ lib/turbo/services/payment_service.dart | 19 +++++++++++ .../payment_form/payment_form_state.dart | 2 ++ lib/turbo/topup/models/payment_model.dart | 5 ++- lib/turbo/topup/models/price_estimate.dart | 2 ++ lib/turbo/topup/views/topup_payment_form.dart | 33 +++++++++++++------ 6 files changed, 68 insertions(+), 11 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3295726fde..49226c24bd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1822,6 +1822,15 @@ "@turboErrorMessageSessionExpired": {}, "turboErrorMessageUnknown": "The payment was not successful. Please check your card information and try again.", "@turboErrorMessageUnknown": {}, + "turboPercentageDiscountApplied": "({percentage}% discount applied)", + "@turboPercentageDiscountApplied": { + "description": "E.g. \"50% discount applied\"", + "placeholders": { + "percentage": { + "type": "String" + } + } + }, "turboPleaseEnterAmountBetween": "Please enter an amount between {min} - {max}", "@turboPleaseEnterAmountBetween": { "description": "Error message for when the given amount is not in range", @@ -1836,6 +1845,15 @@ } } }, + "turboUsdDiscountApplied": "(${amount} discount applied)", + "@turboUsdDiscountApplied": { + "description": "E.g. \"10$ discount applied\"", + "placeholders": { + "amount": { + "type": "String" + } + } + }, "unableToFetchEstimateAtThisTime": "Unable to fetch the estimate at this time.", "@unableToFetchEstimateAtThisTime": {}, "unableToUpdateQuote": "Unable to update quote. Please try again.", diff --git a/lib/turbo/services/payment_service.dart b/lib/turbo/services/payment_service.dart index 6246e76352..1b336d6e4d 100644 --- a/lib/turbo/services/payment_service.dart +++ b/lib/turbo/services/payment_service.dart @@ -288,6 +288,25 @@ class PriceForFiat extends Equatable { final int? quotedPaymentAmount; final List adjustments; + bool get hasReachedMaximumDiscount { + final maxDiscount = adjustments.first.maxDiscount; + if (maxDiscount == null) { + return false; + } + + final adjustmentAmount = adjustments.first.adjustmentAmount; + return maxDiscount + adjustmentAmount == 0; + } + + String? get adjustmentAmount { + if (adjustments.isEmpty) { + return null; + } + + final adjustmentAmount = -adjustments.first.adjustmentAmount / 100; + return adjustmentAmount.toStringAsFixed(2); + } + String? get humanReadableDiscountPercentage { if (adjustments.isEmpty) { return null; diff --git a/lib/turbo/topup/blocs/payment_form/payment_form_state.dart b/lib/turbo/topup/blocs/payment_form/payment_form_state.dart index 1847d3cc47..593bfc5612 100644 --- a/lib/turbo/topup/blocs/payment_form/payment_form_state.dart +++ b/lib/turbo/topup/blocs/payment_form/payment_form_state.dart @@ -14,6 +14,8 @@ abstract class PaymentFormState extends Equatable { priceEstimate.humanReadableDiscountPercentage; String get paymentAmount => priceEstimate.paymentAmount.toStringAsFixed(2); BigInt get winstonCredits => priceEstimate.winstonCredits; + bool get hasReachedMaximumDiscount => priceEstimate.hasReachedMaximumDiscount; + String? get adjustmentAmount => priceEstimate.adjustmentAmount; @override List get props => [ diff --git a/lib/turbo/topup/models/payment_model.dart b/lib/turbo/topup/models/payment_model.dart index 2c93d68091..19c05c54eb 100644 --- a/lib/turbo/topup/models/payment_model.dart +++ b/lib/turbo/topup/models/payment_model.dart @@ -76,6 +76,7 @@ class Adjustment extends Equatable { final double operatorMagnitude; final String operator; final int adjustmentAmount; + final int? maxDiscount; const Adjustment({ required this.name, @@ -83,6 +84,7 @@ class Adjustment extends Equatable { required this.operatorMagnitude, required this.operator, required this.adjustmentAmount, + required this.maxDiscount, }); String get humanReadableDiscountPercentage { @@ -105,7 +107,7 @@ class Adjustment extends Equatable { @override String toString() { - return 'Adjustment{name: $name, description: $description, operatorMagnitude: $operatorMagnitude, operator: $operator, adjustmentAmount: $adjustmentAmount}'; + return 'Adjustment{name: $name, description: $description, operatorMagnitude: $operatorMagnitude, operator: $operator, adjustmentAmount: $adjustmentAmount, maxDiscount: $maxDiscount}'; } @override @@ -115,5 +117,6 @@ class Adjustment extends Equatable { operatorMagnitude, operator, adjustmentAmount, + maxDiscount ?? '', ]; } diff --git a/lib/turbo/topup/models/price_estimate.dart b/lib/turbo/topup/models/price_estimate.dart index 6a50f5e3a9..e8119dfb7b 100644 --- a/lib/turbo/topup/models/price_estimate.dart +++ b/lib/turbo/topup/models/price_estimate.dart @@ -19,6 +19,8 @@ class PriceEstimate extends Equatable { ? estimate.actualPaymentAmount! / 100 : priceInCurrency; BigInt get winstonCredits => estimate.winstonCredits; + bool get hasReachedMaximumDiscount => estimate.hasReachedMaximumDiscount; + String? get adjustmentAmount => estimate.adjustmentAmount; factory PriceEstimate.zero() => PriceEstimate( estimate: PriceForFiat.zero(), diff --git a/lib/turbo/topup/views/topup_payment_form.dart b/lib/turbo/topup/views/topup_payment_form.dart index 9286d71496..196401c0a6 100644 --- a/lib/turbo/topup/views/topup_payment_form.dart +++ b/lib/turbo/topup/views/topup_payment_form.dart @@ -258,16 +258,7 @@ class TurboPaymentFormViewState extends State { ), ), if (state.hasPromoCodeApplied) - TextSpan( - text: - ' (${state.humanReadableDiscountPercentage}% discount applied)', // TODO: localize - style: ArDriveTypography.body.buttonNormalRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDisabled, - ), - ), + _buildDiscountText(context, state), ], ), ); @@ -281,6 +272,28 @@ class TurboPaymentFormViewState extends State { ); } + TextSpan _buildDiscountText(BuildContext context, PaymentFormState state) { + late String text; + final hasReachedMaximumDiscount = state.hasReachedMaximumDiscount; + + if (hasReachedMaximumDiscount) { + text = appLocalizationsOf(context).turboUsdDiscountApplied( + state.adjustmentAmount!, + ); + } else { + text = appLocalizationsOf(context).turboPercentageDiscountApplied( + state.humanReadableDiscountPercentage!, + ); + } + + return TextSpan( + text: ' $text', + style: ArDriveTypography.body.buttonNormalRegular( + color: ArDriveTheme.of(context).themeData.colors.themeFgDisabled, + ), + ); + } + Widget _header(BuildContext context) { return SizedBox( width: double.maxFinite, From 215c313e6db7497a1f2de0e7039e2162486e5d30 Mon Sep 17 00:00:00 2001 From: Mati Date: Wed, 29 Nov 2023 17:37:08 -0300 Subject: [PATCH 2/8] test(payment form bloc): updates unit tests; adds one for the new case PE-5138 --- .../payment_form/payment_form_bloc_test.dart | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/test/turbo/blocs/payment_form/payment_form_bloc_test.dart b/test/turbo/blocs/payment_form/payment_form_bloc_test.dart index df9f904afe..4e6f6406f4 100644 --- a/test/turbo/blocs/payment_form/payment_form_bloc_test.dart +++ b/test/turbo/blocs/payment_form/payment_form_bloc_test.dart @@ -151,6 +151,7 @@ void main() { final validPromoCodes = { 'BANANA': 0.1, 'MANZANA': 0.2, + 'FRUTILLA': 1.0, }; late PriceEstimate initialPriceEstimate; // PriceEstimate? estimateInState; @@ -205,6 +206,7 @@ void main() { operatorMagnitude: magnitude, operator: 'multiply', adjustmentAmount: adjustmentAmount, + maxDiscount: null, ) ], actualPaymentAmount: actualAmount, @@ -275,6 +277,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -302,6 +305,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -328,6 +332,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -355,6 +360,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -381,6 +387,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -408,6 +415,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -434,6 +442,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -461,6 +470,7 @@ void main() { operatorMagnitude: 0.9, operator: 'multiply', adjustmentAmount: 1, + maxDiscount: null, ) ], actualPaymentAmount: 9, @@ -487,6 +497,7 @@ void main() { operatorMagnitude: 0.8, operator: 'multiply', adjustmentAmount: 2, + maxDiscount: null, ) ], actualPaymentAmount: 8, @@ -503,5 +514,123 @@ void main() { ), ], ); + + // has reached max discount + blocTest( + 'hasReachedMaximumDiscount is true when the discount reaches the maximum', + build: () { + when(() => mockTurbo.refreshPriceEstimate( + promoCode: any(named: 'promoCode'))).thenAnswer((_) async { + final promoCode = _.namedArguments[#promoCode] as String?; + final isValidPromoCode = validPromoCodes.containsKey(promoCode); + final discountFactor = validPromoCodes[promoCode]; + const maxDiscount = 5; + + if (promoCode == 'NETWORK ERROR') { + throw Exception(); + } + + if (isValidPromoCode) { + final quotedAmount = (initialPriceEstimate.priceInCurrency).floor(); + final magnitude = (100 - (100 * discountFactor!)) / 100; + int adjustmentAmount = (quotedAmount * discountFactor).floor(); + if (adjustmentAmount > maxDiscount) { + adjustmentAmount = maxDiscount; + } + final actualAmount = quotedAmount - adjustmentAmount; + + final estimate = PriceForFiat( + winc: BigInt.from(10), + adjustments: [ + Adjustment( + name: 'Promo code', + description: 'Promo code', + operatorMagnitude: magnitude, + operator: 'multiply', + adjustmentAmount: adjustmentAmount, + maxDiscount: maxDiscount, + ) + ], + actualPaymentAmount: actualAmount, + quotedPaymentAmount: quotedAmount, + ); + + final priceEstimate = PriceEstimate( + estimate: estimate, + priceInCurrency: 10, + estimatedStorage: 1, + ); + + // estimateInState = priceEstimate; + + return priceEstimate; + } else { + throw PaymentServiceInvalidPromoCode(promoCode: promoCode); + } + }); + + return paymentFormBloc; + }, + seed: () { + return PaymentFormLoaded( + initialPriceEstimate, + DateTime.now() + .add(const Duration(days: 1)) + .difference(DateTime.now()) + .inSeconds, + const [], + ); + }, + act: (bloc) async { + bloc.add(const PaymentFormUpdatePromoCode('FRUTILLA')); + }, + expect: () => [ + // Fetching for FRUTILLA + PaymentFormLoaded( + PriceEstimate( + estimate: PriceForFiat( + winc: BigInt.from(10), + adjustments: const [], + actualPaymentAmount: null, + quotedPaymentAmount: null, + ), + priceInCurrency: 10, + estimatedStorage: 1, + ), + 1234, + const [], + isPromoCodeInvalid: false, + isFetchingPromoCode: true, + errorFetchingPromoCode: false, + ), + // Valid result for FRUTILLA + PaymentFormLoaded( + PriceEstimate( + estimate: PriceForFiat( + winc: BigInt.from(10), + adjustments: const [ + Adjustment( + name: 'Promo code', + description: 'Promo code', + operatorMagnitude: 0, + operator: 'multiply', + adjustmentAmount: 5, + maxDiscount: 5, + ) + ], + actualPaymentAmount: 5, + quotedPaymentAmount: 10, + ), + priceInCurrency: 10, + estimatedStorage: 1, + ), + 1234, + const [], + isPromoCodeInvalid: false, + isFetchingPromoCode: false, + errorFetchingPromoCode: false, + ), + ], + ); }); } From 8df90273ed9be65a5b26819ef4c55754ef2ab2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Batista?= Date: Thu, 30 Nov 2023 14:10:45 -0300 Subject: [PATCH 3/8] chore(payment form): makes a late variable be final instead PE-5138 --- lib/turbo/topup/views/topup_payment_form.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/turbo/topup/views/topup_payment_form.dart b/lib/turbo/topup/views/topup_payment_form.dart index 196401c0a6..6fa62fa10b 100644 --- a/lib/turbo/topup/views/topup_payment_form.dart +++ b/lib/turbo/topup/views/topup_payment_form.dart @@ -273,7 +273,7 @@ class TurboPaymentFormViewState extends State { } TextSpan _buildDiscountText(BuildContext context, PaymentFormState state) { - late String text; + final String text; final hasReachedMaximumDiscount = state.hasReachedMaximumDiscount; if (hasReachedMaximumDiscount) { From 12abc0dfff2db4da081581eece05c933b60c86fa Mon Sep 17 00:00:00 2001 From: Mati Date: Thu, 30 Nov 2023 14:19:44 -0300 Subject: [PATCH 4/8] feat(payment form bloc test): clean up PE-5138 --- test/turbo/blocs/payment_form/payment_form_bloc_test.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/turbo/blocs/payment_form/payment_form_bloc_test.dart b/test/turbo/blocs/payment_form/payment_form_bloc_test.dart index 4e6f6406f4..21ee6e6640 100644 --- a/test/turbo/blocs/payment_form/payment_form_bloc_test.dart +++ b/test/turbo/blocs/payment_form/payment_form_bloc_test.dart @@ -526,14 +526,12 @@ void main() { final discountFactor = validPromoCodes[promoCode]; const maxDiscount = 5; - if (promoCode == 'NETWORK ERROR') { - throw Exception(); - } - if (isValidPromoCode) { final quotedAmount = (initialPriceEstimate.priceInCurrency).floor(); final magnitude = (100 - (100 * discountFactor!)) / 100; int adjustmentAmount = (quotedAmount * discountFactor).floor(); + + // Here we simulate that the discount has reached the maximum if (adjustmentAmount > maxDiscount) { adjustmentAmount = maxDiscount; } @@ -561,8 +559,6 @@ void main() { estimatedStorage: 1, ); - // estimateInState = priceEstimate; - return priceEstimate; } else { throw PaymentServiceInvalidPromoCode(promoCode: promoCode); From f2ac2a832edb03ce486b5cc12b4955d5624e669b Mon Sep 17 00:00:00 2001 From: Mati Date: Mon, 4 Dec 2023 09:39:03 -0300 Subject: [PATCH 5/8] test(payment service): implements unit tests for the Payment Service class PE-5146 --- test/services/payment_service_test.dart | 280 ++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 test/services/payment_service_test.dart diff --git a/test/services/payment_service_test.dart b/test/services/payment_service_test.dart new file mode 100644 index 0000000000..efc688f732 --- /dev/null +++ b/test/services/payment_service_test.dart @@ -0,0 +1,280 @@ +// unit tests for PaymentService + +import 'dart:io'; + +import 'package:ardrive/turbo/services/payment_service.dart'; +import 'package:ardrive/turbo/topup/models/payment_model.dart'; +import 'package:ardrive_http/ardrive_http.dart'; +import 'package:arweave/arweave.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../test_utils/utils.dart'; + +const String fakeUrl = 'https://fakeurl.com'; +const String fakePromoCode = 'TOTEM'; +const String currency = 'USD'; +const double amount = 1; +const int byteSize = 1000; + +class ArDriveHTTPMock extends Mock implements ArDriveHTTP {} + +void main() { + late PaymentService paymentService; + late ArDriveHTTPMock httpClient; + late Wallet wallet; + late String walletAddress; + + group('PaymentService class', () { + setUp(() async { + httpClient = ArDriveHTTPMock(); + paymentService = PaymentService( + turboPaymentUri: Uri.parse(fakeUrl), + httpClient: httpClient, + ); + wallet = getTestWallet(); + walletAddress = await wallet.getAddress(); + + when(() => httpClient.get(url: '$fakeUrl/v1/price/bytes/$byteSize')) + .thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: '{"winc": "1000000000000"}', + retryAttempts: 0, + ), + ); + + when( + () => httpClient.get( + url: '$fakeUrl/v1/price/$currency/$amount', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: '{"winc": "1000000000000"}', + retryAttempts: 0, + ), + ); + + when( + () => httpClient.get( + url: + '$fakeUrl/v1/price/fiat/$currency/$amount?promoCode=$fakePromoCode', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: '{"winc": "1000000000000"}', + retryAttempts: 0, + ), + ); + + when( + () => httpClient.get( + url: '$fakeUrl/v1/balance', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: '{"winc": "1000000000000"}', + retryAttempts: 0, + ), + ); + + when( + () => httpClient.get( + url: + '$fakeUrl/v1/top-up/payment-intent/$walletAddress/$currency/$amount', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: + '{"paymentSession": {"id": "session1", "client_secret": "secret1"}, "topUpQuote": {"topUpQuoteId": "quote1", "destinationAddress": "address1", "destinationAddressType": "type1", "paymentAmount": 100, "quotedPaymentAmount": 90, "currencyType": "USD", "winstonCreditAmount": "1000", "quoteExpirationDate": "2022-12-31", "paymentProvider": "provider1"}, "adjustments": [{"name": "adjustment1", "description": "description1", "operatorMagnitude": 1.5, "operator": "+", "adjustmentAmount": 10, "maxDiscount": 5}]}', + retryAttempts: 0, + ), + ); + + when( + () => httpClient.get( + url: + '$fakeUrl/v1/top-up/payment-intent/$walletAddress/$currency/$amount?promoCode=$fakePromoCode', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: + '{"paymentSession": {"id": "session1", "client_secret": "secret1"}, "topUpQuote": {"topUpQuoteId": "quote1", "destinationAddress": "address1", "destinationAddressType": "type1", "paymentAmount": 100, "quotedPaymentAmount": 90, "currencyType": "USD", "winstonCreditAmount": "1000", "quoteExpirationDate": "2022-12-31", "paymentProvider": "provider1"}, "adjustments": [{"name": "adjustment1", "description": "description1", "operatorMagnitude": 1.5, "operator": "+", "adjustmentAmount": 10, "maxDiscount": 5}]}', + retryAttempts: 0, + ), + ); + + when( + () => httpClient.get( + url: '$fakeUrl/v1/countries', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.ok, + data: '["US", "CA"]', + retryAttempts: 0, + ), + ); + }); + + group('getPriceForBytes method', () { + test('should return a BigInt', () async { + final result = + await paymentService.getPriceForBytes(byteSize: byteSize); + expect(result, isA()); + }); + + test('should throw an exception if the status code is not 200, 202, 204', + () async { + when(() => httpClient.get(url: '$fakeUrl/v1/price/bytes/$byteSize')) + .thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.badRequest, + data: null, + retryAttempts: 0, + ), + ); + expect( + () async => await paymentService.getPriceForBytes(byteSize: byteSize), + throwsException, + ); + }); + }); + + group('getPriceForFiat method', () { + test('should return a PriceForFiat object', () async { + final result = await paymentService.getPriceForFiat( + amount: amount, + currency: currency, + wallet: wallet, + ); + expect(result, isA()); + }); + + test('should throw an exception if the status code is not 200, 202, 204', + () async { + when( + () => httpClient.get( + url: '$fakeUrl/v1/price/$currency/$amount?promoCode=$fakePromoCode', + headers: any(named: 'headers'), + ), + ).thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: HttpStatus.badRequest, + data: null, + retryAttempts: 0, + ), + ); + expect( + () async => await paymentService.getPriceForFiat( + amount: amount, + currency: currency, + wallet: wallet, + promoCode: fakePromoCode, + ), + throwsException, + ); + }); + }); + + group('getBalance method', () { + test('should return a BigInt', () async { + final result = await paymentService.getBalance(wallet: wallet); + expect(result, isA()); + }); + + test('should throw an exception if the status code is not 200, 202, 204', + () async { + when( + () => httpClient.get( + url: '$fakeUrl/v1/balance', + headers: any(named: 'headers'), + ), + ).thenThrow(ArDriveHTTPException( + statusCode: 404, + retryAttempts: 0, + exception: Exception('404'), + )); + expect( + () async => await paymentService.getBalance(wallet: wallet), + throwsException, + ); + }); + }); + }); + + group('getPaymentIntent method', () { + test('should return a PaymentModel object', () async { + final result = await paymentService.getPaymentIntent( + wallet: wallet, + amount: amount, + currency: currency, + ); + expect(result, isA()); + }); + + test('should throw an exception if the status code is not 200, 202, 204', + () async { + when( + () => httpClient.get( + url: + '$fakeUrl/v1/top-up/payment-intent/$walletAddress/$currency/$amount?promoCode=$fakePromoCode', + headers: any(named: 'headers'), + ), + ).thenThrow( + ArDriveHTTPException( + statusCode: 404, + retryAttempts: 0, + exception: Exception('404'), + ), + ); + expect( + () async => await paymentService.getPaymentIntent( + wallet: wallet, + amount: amount, + currency: currency, + promoCode: fakePromoCode, + ), + throwsException, + ); + }); + }); + + group('getSupportedCountries method', () { + test('should return a list of countries', () async { + final result = await paymentService.getSupportedCountries(); + expect(result, isA>()); + }); + + test('should throw an exception if the status code is not 200, 202, 204', + () async { + when( + () => httpClient.get( + url: '$fakeUrl/v1/countries', + headers: any(named: 'headers'), + ), + ).thenThrow( + ArDriveHTTPException( + statusCode: 404, + retryAttempts: 0, + exception: Exception('404'), + ), + ); + expect( + () async => await paymentService.getSupportedCountries(), + throwsException, + ); + }); + }); +} From d71057ef73fe0c9161c630a367ce5a6bf9a76d9a Mon Sep 17 00:00:00 2001 From: Mati Date: Mon, 4 Dec 2023 11:46:05 -0300 Subject: [PATCH 6/8] test(payment service): corrects the unit tests to have explicit expectations PE-5146 --- lib/turbo/services/payment_service.dart | 95 +++++++++++++++---------- test/services/payment_service_test.dart | 91 +++++++++++++++++++---- 2 files changed, 136 insertions(+), 50 deletions(-) diff --git a/lib/turbo/services/payment_service.dart b/lib/turbo/services/payment_service.dart index 1b336d6e4d..2d9667cfe7 100644 --- a/lib/turbo/services/payment_service.dart +++ b/lib/turbo/services/payment_service.dart @@ -65,27 +65,29 @@ class PaymentService { nonce: nonce, wallet: wallet, ); - final result = await httpClient.get( - url: '$turboPaymentUri/v1/balance', - headers: { - 'x-nonce': nonce, - 'x-signature': signature, - 'x-public-key': publicKey, - }, - ).onError((ArDriveHTTPException error, stackTrace) { - if (error.statusCode == 404) { - logger.w('user not found'); - throw TurboUserNotFound(); - } - - logger.e('error getting balance', error, stackTrace); - - throw error; - }); + try { + final result = await httpClient.get( + url: '$turboPaymentUri/v1/balance', + headers: { + 'x-nonce': nonce, + 'x-signature': signature, + 'x-public-key': publicKey, + }, + ); - final price = BigInt.parse((json.decode(result.data)['winc'])); + final price = BigInt.parse((json.decode(result.data)['winc'])); - return price; + return price; + } catch (error, stackTrace) { + if (error is ArDriveHTTPException) { + if (error.statusCode == 404) { + logger.w('user not found'); + throw TurboUserNotFound(); + } + } + logger.e('error getting balance', error, stackTrace); + rethrow; + } } Future getPaymentIntent({ @@ -159,30 +161,30 @@ Future _requestPriceForFiat( final acceptedStatusCodes = [200, 202, 204]; final String urlParams = _urlParamsForGetPriceForFiat(promoCode: promoCode); - final result = await httpClient - .get( - url: '$turboPaymentUri/v1/price/$currency/$amount$urlParams', - headers: signatureHeaders, - ) - .onError( - (ArDriveHTTPException error, stackTrace) { + try { + final result = await httpClient.get( + url: '$turboPaymentUri/v1/price/$currency/$amount$urlParams', + headers: signatureHeaders, + ); + + if (!acceptedStatusCodes.contains(result.statusCode)) { + throw PaymentServiceException( + 'Turbo price fetch failed with status code ${result.statusCode}', + ); + } + + return result; + } catch (error) { + if (error is ArDriveHTTPException) { if (error.statusCode == 400) { logger.e('Invalid promo code: $promoCode'); throw PaymentServiceInvalidPromoCode(promoCode: promoCode); } - throw PaymentServiceException( - 'Turbo price fetch failed with exception: $error', - ); - }, - ); - - if (!acceptedStatusCodes.contains(result.statusCode)) { + } throw PaymentServiceException( - 'Turbo price fetch failed with status code ${result.statusCode}', + 'Turbo price fetch failed with exception: $error', ); } - - return result; } Future> _signatureHeadersForGetPriceForFiat({ @@ -262,7 +264,7 @@ class TurboUserNotFound implements Exception { TurboUserNotFound(); } -class PaymentServiceException implements Exception { +class PaymentServiceException implements Exception, Equatable { final String message; PaymentServiceException([this.message = '']); @@ -271,15 +273,32 @@ class PaymentServiceException implements Exception { String toString() { return 'PaymentServiceException{message: $message}'; } + + @override + List get props => [message]; + + @override + bool? get stringify => true; } class PaymentServiceInvalidPromoCode implements PaymentServiceException { final String? promoCode; - PaymentServiceInvalidPromoCode({required this.promoCode}); + const PaymentServiceInvalidPromoCode({required this.promoCode}); @override String get message => 'Invalid promo code: "$promoCode"'; + + @override + String toString() { + return 'PaymentServiceInvalidPromoCode{promoCode: $promoCode}'; + } + + @override + List get props => [promoCode ?? '']; + + @override + bool? get stringify => true; } class PriceForFiat extends Equatable { diff --git a/test/services/payment_service_test.dart b/test/services/payment_service_test.dart index efc688f732..ba0c2d8738 100644 --- a/test/services/payment_service_test.dart +++ b/test/services/payment_service_test.dart @@ -133,6 +133,30 @@ void main() { final result = await paymentService.getPriceForBytes(byteSize: byteSize); expect(result, isA()); + + when(() => httpClient.get(url: '$fakeUrl/v1/price/bytes/$byteSize')) + .thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: 202, + data: '{"winc": "1000000000000"}', + retryAttempts: 0, + ), + ); + final result2 = + await paymentService.getPriceForBytes(byteSize: byteSize); + expect(result2, isA()); + + when(() => httpClient.get(url: '$fakeUrl/v1/price/bytes/$byteSize')) + .thenAnswer( + (_) async => ArDriveHTTPResponse( + statusCode: 204, + data: '{"winc": "1000000000000"}', + retryAttempts: 0, + ), + ); + final result3 = + await paymentService.getPriceForBytes(byteSize: byteSize); + expect(result3, isA()); }); test('should throw an exception if the status code is not 200, 202, 204', @@ -162,18 +186,19 @@ void main() { expect(result, isA()); }); - test('should throw an exception if the status code is not 200, 202, 204', + test( + 'should throw a PaymentServiceInvalidPromoCode exception if the status code is bad request', () async { when( () => httpClient.get( url: '$fakeUrl/v1/price/$currency/$amount?promoCode=$fakePromoCode', headers: any(named: 'headers'), ), - ).thenAnswer( - (_) async => ArDriveHTTPResponse( - statusCode: HttpStatus.badRequest, - data: null, + ).thenThrow( + ArDriveHTTPException( + statusCode: 400, retryAttempts: 0, + exception: Exception('400'), ), ); expect( @@ -183,7 +208,33 @@ void main() { wallet: wallet, promoCode: fakePromoCode, ), - throwsException, + throwsA(isA()), + ); + }); + + test( + 'should throw a PaymentServiceException exception if the status code is not 400', + () async { + when( + () => httpClient.get( + url: '$fakeUrl/v1/price/$currency/$amount?promoCode=$fakePromoCode', + headers: any(named: 'headers'), + ), + ).thenThrow( + ArDriveHTTPException( + statusCode: 500, + retryAttempts: 0, + exception: Exception('500'), + ), + ); + expect( + () async => await paymentService.getPriceForFiat( + amount: amount, + currency: currency, + wallet: wallet, + promoCode: fakePromoCode, + ), + throwsA(isA()), ); }); }); @@ -194,7 +245,8 @@ void main() { expect(result, isA()); }); - test('should throw an exception if the status code is not 200, 202, 204', + test( + 'should throw a TurboUserNotFound exception if the status code is 404', () async { when( () => httpClient.get( @@ -208,7 +260,24 @@ void main() { )); expect( () async => await paymentService.getBalance(wallet: wallet), - throwsException, + throwsA(isA()), + ); + }); + + test('should throw an exception if the status code is not 404', () async { + when( + () => httpClient.get( + url: '$fakeUrl/v1/balance', + headers: any(named: 'headers'), + ), + ).thenThrow(ArDriveHTTPException( + statusCode: 500, + retryAttempts: 0, + exception: Exception('500'), + )); + expect( + () async => await paymentService.getBalance(wallet: wallet), + throwsA(isA()), ); }); }); @@ -224,8 +293,7 @@ void main() { expect(result, isA()); }); - test('should throw an exception if the status code is not 200, 202, 204', - () async { + test('should throw an exception if the request fails', () async { when( () => httpClient.get( url: @@ -257,8 +325,7 @@ void main() { expect(result, isA>()); }); - test('should throw an exception if the status code is not 200, 202, 204', - () async { + test('should throw an exception if the request fails', () async { when( () => httpClient.get( url: '$fakeUrl/v1/countries', From 272fae98b66eb6ea3d02acb0dd99f9e155284417 Mon Sep 17 00:00:00 2001 From: Mati Date: Tue, 5 Dec 2023 11:39:37 -0300 Subject: [PATCH 7/8] feat(pubspec): before-release version bump PE-5174 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4988af6c78..d989181614 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Secure, permanent storage publish_to: 'none' -version: 2.26.0 +version: 2.26.1 environment: sdk: '>=3.0.2 <4.0.0' From 212d43963d79675c671e409994261aa1b8f11134 Mon Sep 17 00:00:00 2001 From: Mati Date: Tue, 5 Dec 2023 12:48:56 -0300 Subject: [PATCH 8/8] feat(android release notes): before-release android release notes PE-5174 --- android/fastlane/metadata/android/en-US/changelogs/82.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 android/fastlane/metadata/android/en-US/changelogs/82.txt diff --git a/android/fastlane/metadata/android/en-US/changelogs/82.txt b/android/fastlane/metadata/android/en-US/changelogs/82.txt new file mode 100644 index 0000000000..d8c9901162 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/82.txt @@ -0,0 +1 @@ +- Corrects the top-up modal able to display proper promo code discounts details.