From 810c1034619ab3a1485104dca8b7d2dfd6279c01 Mon Sep 17 00:00:00 2001 From: kirahsapong <102400653+kirahsapong@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:55:27 -0800 Subject: [PATCH] Review Request Page (#65) * Add translations * Add optional total to fee table * Add review request page and test * pass data from withdraw or deposit page to payment details page * update translations * change to shorthand syntax and add test helper widgets * upadte page heading to match payment details --- lib/features/deposit/deposit_page.dart | 19 +- .../payments/payment_details_page.dart | 45 ++++- .../payments/review_request_page.dart | 173 ++++++++++++++++++ lib/features/withdraw/withdraw_page.dart | 20 +- lib/l10n/app_en.arb | 8 +- lib/l10n/app_localizations.dart | 36 ++++ lib/l10n/app_localizations_en.dart | 18 ++ lib/shared/fee_details.dart | 31 ++++ .../payments/payment_details_page_test.dart | 46 +++-- .../payments/review_request_page_test.dart | 75 ++++++++ 10 files changed, 439 insertions(+), 32 deletions(-) create mode 100644 lib/features/payments/review_request_page.dart create mode 100644 test/features/payments/review_request_page_test.dart diff --git a/lib/features/deposit/deposit_page.dart b/lib/features/deposit/deposit_page.dart index 16f34cf3..dac955d5 100644 --- a/lib/features/deposit/deposit_page.dart +++ b/lib/features/deposit/deposit_page.dart @@ -1,3 +1,4 @@ +import 'package:didpay/features/home/transaction.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:didpay/features/currency/currency_converter.dart'; @@ -25,6 +26,8 @@ class DepositPage extends HookWidget { final isValidKeyPress = useState(true); final selectedCurrencyItem = useState>(supportedCurrencyList[1]); + final outputAmount = double.parse('0${depositAmount.value}') / + double.parse(selectedCurrencyItem.value['exchangeRate'].toString()); return Scaffold( appBar: AppBar(), @@ -45,10 +48,7 @@ class DepositPage extends HookWidget { inputSelectedCurrency: selectedCurrencyItem.value['label'].toString(), inputLabel: Loc.of(context).youDeposit, - outputAmount: (double.parse('0${depositAmount.value}') / - double.parse(selectedCurrencyItem - .value['exchangeRate'] - .toString())), + outputAmount: outputAmount, isValidKeyPress: isValidKeyPress.value, onDropdownTap: () { CurrencyModal.show( @@ -83,7 +83,16 @@ class DepositPage extends HookWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const PaymentDetailsPage(), + builder: (context) => PaymentDetailsPage( + inputAmount: depositAmount.value, + inputCurrency: + selectedCurrencyItem.value['label'].toString(), + exchangeRate: selectedCurrencyItem.value['exchangeRate'] + .toString(), + outputAmount: outputAmount.toString(), + outputCurrency: Loc.of(context).usd, + transactionType: Type.deposit, + ), ), ); }, diff --git a/lib/features/payments/payment_details_page.dart b/lib/features/payments/payment_details_page.dart index ec6ed6a3..0f94a00e 100644 --- a/lib/features/payments/payment_details_page.dart +++ b/lib/features/payments/payment_details_page.dart @@ -1,3 +1,5 @@ +import 'package:collection/collection.dart'; +import 'package:didpay/features/payments/review_request_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:didpay/features/payments/payment_method.dart'; @@ -8,7 +10,21 @@ import 'package:didpay/shared/json_schema_form.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class PaymentDetailsPage extends HookConsumerWidget { - const PaymentDetailsPage({super.key}); + final String inputCurrency; + final String inputAmount; + final String outputCurrency; + final String outputAmount; + final String exchangeRate; + final String transactionType; + + const PaymentDetailsPage( + {required this.inputCurrency, + required this.inputAmount, + required this.outputCurrency, + required this.outputAmount, + required this.exchangeRate, + required this.transactionType, + super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -174,12 +190,37 @@ class PaymentDetailsPage extends HookConsumerWidget { child: JsonSchemaForm( schema: selectedPaymentMethod.value!.requiredPaymentDetails, onSubmit: (formData) { - // TODO: save payment details here + if (isValidOnSubmit(formData, selectedPaymentMethod)) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ReviewRequestPage( + formData: formData, + bankName: selectedPaymentMethod.value!.kind + .split('_') + .lastOrNull ?? + '', + inputAmount: inputAmount, + outputAmount: outputAmount, + inputCurrency: inputCurrency, + outputCurrency: outputCurrency, + exchangeRate: exchangeRate, + serviceFee: selectedPaymentMethod.value!.fee ?? '0.00', + transactionType: transactionType, + ), + ), + ); + } }, ), ); } + bool isValidOnSubmit(Map formData, + ValueNotifier selectedPaymentMethod) { + return formData['accountNumber'] != null && + selectedPaymentMethod.value!.kind.split('_').lastOrNull != null; + } + Widget _buildDisabledButton(BuildContext context) { return Expanded( child: Column( diff --git a/lib/features/payments/review_request_page.dart b/lib/features/payments/review_request_page.dart new file mode 100644 index 00000000..b342468e --- /dev/null +++ b/lib/features/payments/review_request_page.dart @@ -0,0 +1,173 @@ +import 'package:didpay/features/home/transaction.dart'; +import 'package:didpay/l10n/app_localizations.dart'; +import 'package:didpay/shared/fee_details.dart'; +import 'package:didpay/shared/success_page.dart'; +import 'package:didpay/shared/theme/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:intl/intl.dart'; + +class ReviewRequestPage extends HookWidget { + final String inputAmount; + final String outputAmount; + final String inputCurrency; + final String outputCurrency; + final String exchangeRate; + final String serviceFee; + final String bankName; + final Map formData; + final String transactionType; + + const ReviewRequestPage({ + required this.inputAmount, + required this.outputAmount, + required this.inputCurrency, + required this.outputCurrency, + required this.exchangeRate, + required this.serviceFee, + required this.bankName, + required this.formData, + required this.transactionType, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Grid.side), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: Grid.md), + _buildAmounts(context), + const SizedBox(height: Grid.md), + _buildFeeDetails(context), + const SizedBox(height: Grid.md), + _buildBankDetails(context), + ])), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FilledButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => SuccessPage( + text: Loc.of(context).yourRequestWasSubmitted, + ), + ), + ); + }, + child: Text(Loc.of(context).submit), + ), + ], + ), + ], + )))); + } + + Widget _buildHeader(BuildContext context) => Column(children: [ + Align( + alignment: Alignment.topLeft, + child: Text( + Loc.of(context).reviewYourRequest, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: Grid.xs), + Align( + alignment: Alignment.topLeft, + child: Text( + Loc.of(context).makeSureInfoIsCorrect, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ]); + + Widget _buildAmounts(BuildContext context) => + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + NumberFormat.simpleCurrency().format(double.parse(inputAmount)), + style: Theme.of(context).textTheme.displaySmall, + ), + const SizedBox(width: Grid.xs), + Text( + inputCurrency, + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + const SizedBox(height: Grid.xxs), + Text( + transactionType == Type.deposit + ? Loc.of(context).youPay + : Loc.of(context).withdrawAmount, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: Grid.xs), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + NumberFormat.simpleCurrency().format(double.parse(outputAmount)), + style: Theme.of(context).textTheme.displaySmall, + ), + const SizedBox(width: Grid.xs), + Text( + outputCurrency, + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + const SizedBox(height: Grid.xxs), + Text( + transactionType == Type.deposit + ? Loc.of(context).depositAmount + : Loc.of(context).youGet, + style: Theme.of(context).textTheme.bodyLarge, + ) + ]); + + Widget _buildFeeDetails(BuildContext context) => FeeDetails( + originCurrency: Loc.of(context).usd, + destinationCurrency: + inputCurrency != Loc.of(context).usd ? inputCurrency : outputCurrency, + exchangeRate: exchangeRate, + serviceFee: double.parse(serviceFee).toStringAsFixed(2), + total: inputCurrency != Loc.of(context).usd + ? (double.parse(inputAmount) + double.parse(serviceFee)) + .toStringAsFixed(2) + : (double.parse(outputAmount) + double.parse(serviceFee)) + .toStringAsFixed(2)); + + Widget _buildBankDetails(BuildContext context) => Column(children: [ + Text(bankName), + const SizedBox(height: Grid.xxs), + Text(obscureAccountNumber(formData['accountNumber']!)), + ]); + + String obscureAccountNumber(String input) { + if (input.length <= 4) { + return input; + } + return '${'•' * (input.length - 4)} ${input.substring(input.length - 4)}'; + } +} diff --git a/lib/features/withdraw/withdraw_page.dart b/lib/features/withdraw/withdraw_page.dart index 4a70de01..7e820ddc 100644 --- a/lib/features/withdraw/withdraw_page.dart +++ b/lib/features/withdraw/withdraw_page.dart @@ -1,3 +1,4 @@ +import 'package:didpay/features/home/transaction.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:didpay/features/currency/currency_converter.dart'; @@ -25,6 +26,8 @@ class WithdrawPage extends HookWidget { final isValidKeyPress = useState(true); final selectedCurrencyItem = useState>(supportedCurrencyList[1]); + final outputAmount = double.parse('0${withdrawAmount.value}') * + double.parse(selectedCurrencyItem.value['exchangeRate'].toString()); return Scaffold( appBar: AppBar(), @@ -45,11 +48,7 @@ class WithdrawPage extends HookWidget { inputLabel: Loc.of(context).youWithdraw, outputSelectedCurrency: selectedCurrencyItem.value['label'].toString(), - outputAmount: - (double.parse('0${withdrawAmount.value}') * - double.parse(selectedCurrencyItem - .value['exchangeRate'] - .toString())), + outputAmount: outputAmount, isValidKeyPress: isValidKeyPress.value, onDropdownTap: () { CurrencyModal.show( @@ -84,7 +83,16 @@ class WithdrawPage extends HookWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const PaymentDetailsPage(), + builder: (context) => PaymentDetailsPage( + inputAmount: withdrawAmount.value, + inputCurrency: Loc.of(context).usd, + exchangeRate: selectedCurrencyItem.value['exchangeRate'] + .toString(), + outputAmount: outputAmount.toString(), + outputCurrency: + selectedCurrencyItem.value['label'].toString(), + transactionType: Type.withdrawal, + ), ), ); }, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2c987c84..3c549cd8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -72,5 +72,11 @@ "copiedDid": "Copied DID!", "simulatedQrCodeScan": "Simulated QR code scan!", "sendingPayment": "Sending payment...", - "verifyingYourIdentity": "Verifying your identity..." + "verifyingYourIdentity": "Verifying your identity...", + "reviewYourRequest": "Review your request", + "depositAmount": "Deposit amount", + "withdrawAmount": "Withdraw amount", + "total": "Total", + "submit": "Submit", + "yourRequestWasSubmitted": "Your request was submitted!" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d987cf6b..f10491ed 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -384,6 +384,42 @@ abstract class Loc { /// In en, this message translates to: /// **'Verifying your identity...'** String get verifyingYourIdentity; + + /// No description provided for @reviewYourRequest. + /// + /// In en, this message translates to: + /// **'Review your request'** + String get reviewYourRequest; + + /// No description provided for @depositAmount. + /// + /// In en, this message translates to: + /// **'Deposit amount'** + String get depositAmount; + + /// No description provided for @withdrawAmount. + /// + /// In en, this message translates to: + /// **'Withdraw amount'** + String get withdrawAmount; + + /// No description provided for @total. + /// + /// In en, this message translates to: + /// **'Total'** + String get total; + + /// No description provided for @submit. + /// + /// In en, this message translates to: + /// **'Submit'** + String get submit; + + /// No description provided for @yourRequestWasSubmitted. + /// + /// In en, this message translates to: + /// **'Your request was submitted!'** + String get yourRequestWasSubmitted; } class _LocDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b45d807f..7756a30d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -156,4 +156,22 @@ class LocEn extends Loc { @override String get verifyingYourIdentity => 'Verifying your identity...'; + + @override + String get reviewYourRequest => 'Review your request'; + + @override + String get depositAmount => 'Deposit amount'; + + @override + String get withdrawAmount => 'Withdraw amount'; + + @override + String get total => 'Total'; + + @override + String get submit => 'Submit'; + + @override + String get yourRequestWasSubmitted => 'Your request was submitted!'; } diff --git a/lib/shared/fee_details.dart b/lib/shared/fee_details.dart index c71fa51a..1fe1f73a 100644 --- a/lib/shared/fee_details.dart +++ b/lib/shared/fee_details.dart @@ -9,12 +9,14 @@ class FeeDetails extends HookWidget { final String exchangeRate; // from origin -> destination (ie. destination / origin = exchangeRate) final String serviceFee; + final String total; const FeeDetails({ required this.originCurrency, required this.destinationCurrency, required this.exchangeRate, required this.serviceFee, + this.total = '', super.key, }); @@ -75,6 +77,35 @@ class FeeDetails extends HookWidget { ), ], ), + if (total.isNotEmpty) + Column( + children: [ + const SizedBox(height: Grid.sm), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + Loc.of(context).total, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + '$total $destinationCurrency', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + ], + ) ], ), ); diff --git a/test/features/payments/payment_details_page_test.dart b/test/features/payments/payment_details_page_test.dart index 9447fd98..436eb409 100644 --- a/test/features/payments/payment_details_page_test.dart +++ b/test/features/payments/payment_details_page_test.dart @@ -1,19 +1,31 @@ -import 'package:flutter/material.dart'; import 'package:didpay/features/payments/payment_details_page.dart'; +import 'package:flutter/material.dart'; import 'package:didpay/features/payments/payment_method.dart'; import 'package:didpay/features/payments/search_payment_methods_page.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../helpers/widget_helpers.dart'; void main() { group('PaymentDetailsPage', () { + Widget paymentDetailsPageTestWidget( + {List overrides = const []}) => + WidgetHelpers.testableWidget( + child: const PaymentDetailsPage( + inputAmount: '1.00', + inputCurrency: 'USD', + exchangeRate: '17', + outputAmount: '17.00', + outputCurrency: 'MXN', + transactionType: 'Deposit', + ), + overrides: overrides, + ); testWidgets('should show make sure this information is correct', (tester) async { await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), - ), + WidgetHelpers.testableWidget(child: paymentDetailsPageTestWidget()), ); expect( @@ -23,9 +35,7 @@ void main() { testWidgets('should show payment method selection zero state', (tester) async { await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), - ), + WidgetHelpers.testableWidget(child: paymentDetailsPageTestWidget()), ); expect(find.text('Select a payment method'), findsOneWidget); @@ -35,7 +45,7 @@ void main() { testWidgets('should show enter your momo details', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -55,7 +65,7 @@ void main() { testWidgets('should show enter your bank details', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -75,7 +85,7 @@ void main() { testWidgets('should show enter your wallet details', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -95,7 +105,7 @@ void main() { testWidgets('should show no payment type segments', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -116,7 +126,7 @@ void main() { (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -145,7 +155,7 @@ void main() { (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -181,7 +191,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -205,7 +215,7 @@ void main() { (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -233,7 +243,7 @@ void main() { testWidgets('should show momo schema form', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -261,7 +271,7 @@ void main() { testWidgets('should show bank schema form', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ @@ -289,7 +299,7 @@ void main() { testWidgets('should show wallet schema form', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( - child: const PaymentDetailsPage(), + child: paymentDetailsPageTestWidget(), overrides: [ paymentMethodProvider.overrideWith( (ref) => [ diff --git a/test/features/payments/review_request_page_test.dart b/test/features/payments/review_request_page_test.dart new file mode 100644 index 00000000..6d52e845 --- /dev/null +++ b/test/features/payments/review_request_page_test.dart @@ -0,0 +1,75 @@ +import 'package:didpay/features/payments/review_request_page.dart'; +import 'package:didpay/shared/fee_details.dart'; +import 'package:didpay/shared/success_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../helpers/widget_helpers.dart'; + +void main() { + Widget requestReviewPageTestWidget({List overrides = const []}) => + WidgetHelpers.testableWidget( + child: const ReviewRequestPage( + inputAmount: '1.00', + inputCurrency: 'USD', + exchangeRate: '17', + outputAmount: '17.00', + outputCurrency: 'MXN', + transactionType: 'Deposit', + serviceFee: '9.0', + bankName: 'ABC Bank', + formData: {'accountNumber': '1234567890'}, + ), + overrides: overrides, + ); + group('ReviewRequestPage', () { + testWidgets('should show input and output amounts', (tester) async { + await tester.pumpWidget( + WidgetHelpers.testableWidget(child: requestReviewPageTestWidget())); + + expect(find.text('\$1.00'), findsOneWidget); + expect(find.text('USD'), findsOneWidget); + expect(find.text('\$17.00'), findsOneWidget); + expect(find.text('MXN'), findsOneWidget); + }); + + testWidgets('should show fee table with service fee and total', + (tester) async { + await tester.pumpWidget( + WidgetHelpers.testableWidget(child: requestReviewPageTestWidget())); + + expect(find.byType(FeeDetails), findsOneWidget); + expect(find.text('9.00 MXN'), findsOneWidget); + expect(find.text('26.00 MXN'), findsOneWidget); + }); + + testWidgets('should show bank name', (tester) async { + await tester.pumpWidget( + WidgetHelpers.testableWidget(child: requestReviewPageTestWidget())); + + expect(find.text('ABC Bank'), findsOneWidget); + }); + + testWidgets('should show obscured account number', (tester) async { + await tester.pumpWidget( + WidgetHelpers.testableWidget(child: requestReviewPageTestWidget())); + + expect(find.textContaining('•'), findsOneWidget); + expect(find.textContaining('7890'), findsOneWidget); + expect(find.textContaining('1234567890'), findsNothing); + }); + + testWidgets('should show success page on tap of submit button', + (tester) async { + await tester.pumpWidget( + WidgetHelpers.testableWidget(child: requestReviewPageTestWidget())); + + await tester.tap(find.text('Submit')); + await tester.pumpAndSettle(); + + expect(find.byType(SuccessPage), findsOneWidget); + expect(find.text('Your request was submitted!'), findsOneWidget); + }); + }); +}