diff --git a/lib/features/payment/payment_details_page.dart b/lib/features/payment/payment_details_page.dart index 807dc835..f6fb1dc0 100644 --- a/lib/features/payment/payment_details_page.dart +++ b/lib/features/payment/payment_details_page.dart @@ -4,10 +4,9 @@ import 'package:didpay/features/did/did_provider.dart'; import 'package:didpay/features/kcc/kcc_consent_page.dart'; import 'package:didpay/features/payment/payment_details_state.dart'; import 'package:didpay/features/payment/payment_method.dart'; -import 'package:didpay/features/payment/payment_methods_page.dart'; import 'package:didpay/features/payment/payment_review_page.dart'; +import 'package:didpay/features/payment/payment_selection_page.dart'; import 'package:didpay/features/payment/payment_state.dart'; -import 'package:didpay/features/payment/payment_types_page.dart'; import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/tbdex/tbdex_quote_notifier.dart'; import 'package:didpay/features/tbdex/tbdex_service.dart'; @@ -55,6 +54,7 @@ class PaymentDetailsPage extends HookConsumerWidget { state.value = state.value.copyWith( selectedPaymentMethod: selectedMethod, paymentName: selectedMethod?.title, + resetSelectedPaymentMethod: selectedMethod == null, ); return; @@ -108,7 +108,7 @@ class PaymentDetailsPage extends HookConsumerWidget { context, state, ), - if (state.value.hasNoPaymentTypes || + if (!state.value.hasMultiplePaymentTypes || state.value.selectedPaymentType != null) _buildPaymentMethodSelector( context, @@ -211,7 +211,10 @@ class PaymentDetailsPage extends HookConsumerWidget { onTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => PaymentTypesPage(state: state), + builder: (context) => PaymentSelectionPage( + state: state, + isSelectingMethod: false, + ), ), ); }, @@ -262,7 +265,7 @@ class PaymentDetailsPage extends HookConsumerWidget { : () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => PaymentMethodsPage( + builder: (context) => PaymentSelectionPage( state: state, availableMethods: availableMethods, ), diff --git a/lib/features/payment/payment_details_state.dart b/lib/features/payment/payment_details_state.dart index 48d6e54f..1f3b7c7c 100644 --- a/lib/features/payment/payment_details_state.dart +++ b/lib/features/payment/payment_details_state.dart @@ -27,7 +27,6 @@ class PaymentDetailsState { Set? get paymentTypes => paymentMethods?.map((method) => method.type).whereType().toSet(); - bool get hasNoPaymentTypes => paymentTypes?.isEmpty ?? true; bool get hasMultiplePaymentTypes => (paymentTypes?.length ?? 0) > 1; List? filterPaymentMethods(String? paymentType) => @@ -47,14 +46,16 @@ class PaymentDetailsState { List? credentialsJwt, Map? formData, Quote? quote, + bool resetSelectedPaymentMethod = false, }) { return PaymentDetailsState( paymentCurrency: paymentCurrency ?? this.paymentCurrency, paymentName: paymentName ?? this.paymentName, exchangeId: exchangeId ?? this.exchangeId, selectedPaymentType: selectedPaymentType ?? this.selectedPaymentType, - selectedPaymentMethod: - selectedPaymentMethod ?? this.selectedPaymentMethod, + selectedPaymentMethod: resetSelectedPaymentMethod + ? null + : selectedPaymentMethod ?? this.selectedPaymentMethod, paymentMethods: paymentMethods ?? this.paymentMethods, credentialsJwt: credentialsJwt ?? this.credentialsJwt, formData: formData ?? this.formData, diff --git a/lib/features/payment/payment_methods_page.dart b/lib/features/payment/payment_selection_page.dart similarity index 61% rename from lib/features/payment/payment_methods_page.dart rename to lib/features/payment/payment_selection_page.dart index e3a9a76f..ad621593 100644 --- a/lib/features/payment/payment_methods_page.dart +++ b/lib/features/payment/payment_selection_page.dart @@ -5,15 +5,17 @@ import 'package:didpay/shared/search_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -class PaymentMethodsPage extends HookWidget { +class PaymentSelectionPage extends HookWidget { final _formKey = GlobalKey(); - final List? availableMethods; final ValueNotifier state; + final List? availableMethods; + final bool isSelectingMethod; - PaymentMethodsPage({ - required this.availableMethods, + PaymentSelectionPage({ required this.state, + this.availableMethods, + this.isSelectingMethod = true, super.key, }); @@ -23,7 +25,7 @@ class PaymentMethodsPage extends HookWidget { final focusNode = useFocusNode(); return Scaffold( - appBar: AppBar(scrolledUnderElevation: 0), + appBar: AppBar(), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -34,12 +36,18 @@ class PaymentMethodsPage extends HookWidget { searchText: searchText, ), Expanded( - child: _buildMethodsList( - context, - searchText, - availableMethods, - state, - ), + child: isSelectingMethod + ? _buildMethodsList( + context, + searchText, + availableMethods, + state, + ) + : _buildTypesList( + context, + searchText, + state, + ), ), ], ), @@ -97,4 +105,40 @@ class PaymentMethodsPage extends HookWidget { itemCount: filteredPaymentMethods?.length, ); } + + Widget _buildTypesList( + BuildContext context, + ValueNotifier searchText, + ValueNotifier state, + ) { + final filteredPaymentTypes = state.value.paymentTypes + ?.where( + (entry) => + entry.toLowerCase().contains(searchText.value.toLowerCase()), + ) + .toList(); + + return ListView.builder( + itemBuilder: (context, index) { + final currentPaymentType = filteredPaymentTypes?.elementAtOrNull(index); + final selected = state.value.selectedPaymentType == currentPaymentType; + + return ListTile( + visualDensity: VisualDensity.compact, + selected: selected, + title: Text(currentPaymentType ?? ''), + trailing: selected ? const Icon(Icons.check) : null, + onTap: () { + if (currentPaymentType != null) { + state.value = + state.value.copyWith(selectedPaymentType: currentPaymentType); + } + + Navigator.of(context).pop(); + }, + ); + }, + itemCount: filteredPaymentTypes?.length, + ); + } } diff --git a/lib/features/payment/payment_types_page.dart b/lib/features/payment/payment_types_page.dart deleted file mode 100644 index e6913b39..00000000 --- a/lib/features/payment/payment_types_page.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:didpay/features/payment/payment_details_state.dart'; -import 'package:didpay/shared/search_field.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -class PaymentTypesPage extends HookWidget { - final _formKey = GlobalKey(); - final ValueNotifier state; - - PaymentTypesPage({ - required this.state, - super.key, - }); - - @override - Widget build(BuildContext context) { - final searchText = useState(''); - final focusNode = useFocusNode(); - - return Scaffold( - appBar: AppBar(scrolledUnderElevation: 0), - body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SearchField( - focusNode: focusNode, - formKey: _formKey, - searchText: searchText, - ), - Expanded( - child: _buildTypesList( - context, - searchText, - state, - ), - ), - ], - ), - ), - ); - } - - Widget _buildTypesList( - BuildContext context, - ValueNotifier searchText, - ValueNotifier state, - ) { - final filteredPaymentTypes = state.value.paymentTypes - ?.where( - (entry) => - entry.toLowerCase().contains(searchText.value.toLowerCase()), - ) - .toList(); - - return ListView.builder( - itemBuilder: (context, index) { - final currentPaymentType = filteredPaymentTypes?.elementAtOrNull(index); - final selected = state.value.selectedPaymentType == currentPaymentType; - - return ListTile( - visualDensity: VisualDensity.compact, - selected: selected, - title: Text(currentPaymentType ?? ''), - trailing: selected ? const Icon(Icons.check) : null, - onTap: () { - if (currentPaymentType != null) { - state.value = - state.value.copyWith(selectedPaymentType: currentPaymentType); - } - - Navigator.of(context).pop(); - }, - ); - }, - itemCount: filteredPaymentTypes?.length, - ); - } -} diff --git a/test/features/payment/payment_details_page_test.dart b/test/features/payment/payment_details_page_test.dart index 6059307d..1dbb07c3 100644 --- a/test/features/payment/payment_details_page_test.dart +++ b/test/features/payment/payment_details_page_test.dart @@ -3,9 +3,8 @@ import 'package:didpay/features/payment/payment_amount_state.dart'; import 'package:didpay/features/payment/payment_details_page.dart'; import 'package:didpay/features/payment/payment_details_state.dart'; import 'package:didpay/features/payment/payment_method.dart'; -import 'package:didpay/features/payment/payment_methods_page.dart'; +import 'package:didpay/features/payment/payment_selection_page.dart'; import 'package:didpay/features/payment/payment_state.dart'; -import 'package:didpay/features/payment/payment_types_page.dart'; import 'package:didpay/features/pfis/pfis_notifier.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:flutter/material.dart'; @@ -162,7 +161,8 @@ void main() async { expect(find.widgetWithIcon(Icon, Icons.chevron_right), findsNothing); }); - testWidgets('should show PaymentTypesPage on tap of select a payment type', + testWidgets( + 'should show PaymentSelectionPage on tap of select a payment type', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( @@ -187,11 +187,11 @@ void main() async { await tester.tap(find.text('Select a payment type')); await tester.pumpAndSettle(); - expect(find.byType(PaymentTypesPage), findsOneWidget); + expect(find.byType(PaymentSelectionPage), findsOneWidget); }); testWidgets( - 'should show PaymentMethodsPage on tap of select a payment method', + 'should show PaymentSelectionPage on tap of select a payment method', (tester) async { await tester.pumpWidget( WidgetHelpers.testableWidget( @@ -214,7 +214,7 @@ void main() async { await tester.tap(find.text('Select a payment method')); await tester.pumpAndSettle(); - expect(find.byType(PaymentMethodsPage), findsOneWidget); + expect(find.byType(PaymentSelectionPage), findsOneWidget); }); testWidgets('should show payment type after PaymentTypesPage selection', diff --git a/test/features/payment/payment_methods_page_test.dart b/test/features/payment/payment_methods_page_test.dart deleted file mode 100644 index b56936f3..00000000 --- a/test/features/payment/payment_methods_page_test.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:didpay/features/payment/payment_details_state.dart'; -import 'package:didpay/features/payment/payment_method.dart'; -import 'package:didpay/features/payment/payment_methods_page.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../helpers/test_data.dart'; -import '../../helpers/widget_helpers.dart'; - -void main() { - final schema = TestData.paymentDetailsSchema(); - final paymentMethods = [ - PaymentMethod( - kind: 'BANK_ACCESS BANK', - name: 'Access Bank', - schema: schema.toJson(), - fee: '9.0', - ), - PaymentMethod( - kind: 'MOMO_MTN', - name: 'MTN', - schema: schema.toJson(), - ), - ]; - - group('PaymentMethodsPage', () { - Widget paymentMethodsPageTestWidget() => WidgetHelpers.testableWidget( - child: PaymentMethodsPage( - availableMethods: paymentMethods, - state: ValueNotifier( - PaymentDetailsState( - paymentCurrency: '', - selectedPaymentMethod: paymentMethods.first, - ), - ), - ), - ); - testWidgets('should show search field', (tester) async { - await tester.pumpWidget( - WidgetHelpers.testableWidget(child: paymentMethodsPageTestWidget()), - ); - - expect(find.byType(TextFormField), findsOneWidget); - expect(find.byIcon(Icons.search), findsOneWidget); - expect(find.text('Search'), findsOneWidget); - }); - - testWidgets('should show payment method list', (tester) async { - await tester.pumpWidget( - WidgetHelpers.testableWidget(child: paymentMethodsPageTestWidget()), - ); - - expect(find.byType(ListTile), findsExactly(2)); - expect(find.widgetWithText(ListTile, 'Access Bank'), findsOneWidget); - expect(find.widgetWithText(ListTile, 'MTN'), findsOneWidget); - }); - - testWidgets('should show a payment method after valid search', - (tester) async { - await tester.pumpWidget( - WidgetHelpers.testableWidget(child: paymentMethodsPageTestWidget()), - ); - - await tester.enterText(find.byType(TextFormField), 'MTN'); - await tester.pump(); - - expect(find.byType(ListTile), findsExactly(1)); - expect(find.widgetWithText(ListTile, 'MTN'), findsOneWidget); - }); - - testWidgets('should show no payment methods after invalid search', - (tester) async { - await tester.pumpWidget( - WidgetHelpers.testableWidget(child: paymentMethodsPageTestWidget()), - ); - - await tester.enterText(find.byType(TextFormField), 'invalid'); - await tester.pump(); - - expect(find.byType(ListTile), findsNothing); - }); - }); -} diff --git a/test/features/payment/payment_selection_page_test.dart b/test/features/payment/payment_selection_page_test.dart new file mode 100644 index 00000000..2e0c5f9f --- /dev/null +++ b/test/features/payment/payment_selection_page_test.dart @@ -0,0 +1,140 @@ +import 'package:didpay/features/payment/payment_details_state.dart'; +import 'package:didpay/features/payment/payment_method.dart'; +import 'package:didpay/features/payment/payment_selection_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/test_data.dart'; +import '../../helpers/widget_helpers.dart'; + +void main() { + final schema = TestData.paymentDetailsSchema(); + final paymentMethods = [ + PaymentMethod( + kind: 'BANK_ACCESS BANK', + name: 'Access Bank', + schema: schema.toJson(), + fee: '9.0', + ), + PaymentMethod( + kind: 'MOMO_MTN', + name: 'MTN', + schema: schema.toJson(), + ), + ]; + + final paymentTypes = [ + PaymentMethod( + type: 'Bank', + kind: 'BANK_ACCESS BANK', + ), + PaymentMethod( + type: 'Mobile money', + kind: 'MOMO_MTN', + ), + PaymentMethod( + type: 'Wallet', + kind: 'BTC_WALLET', + ), + ]; + + group('PaymentSelectionPage - Payment Methods', () { + Widget paymentSelectionPageTestWidget() => WidgetHelpers.testableWidget( + child: PaymentSelectionPage( + availableMethods: paymentMethods, + state: ValueNotifier( + PaymentDetailsState( + paymentCurrency: '', + selectedPaymentMethod: paymentMethods.first, + ), + ), + ), + ); + + testWidgets('should show search field for methods', (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + expect(find.byType(TextFormField), findsOneWidget); + expect(find.byIcon(Icons.search), findsOneWidget); + expect(find.text('Search'), findsOneWidget); + }); + + testWidgets('should show payment method list', (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + expect(find.byType(ListTile), findsExactly(2)); + expect(find.widgetWithText(ListTile, 'Access Bank'), findsOneWidget); + expect(find.widgetWithText(ListTile, 'MTN'), findsOneWidget); + }); + + testWidgets('should show a payment method after valid search', + (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + await tester.enterText(find.byType(TextFormField), 'MTN'); + await tester.pump(); + + expect(find.byType(ListTile), findsExactly(1)); + expect(find.widgetWithText(ListTile, 'MTN'), findsOneWidget); + }); + + testWidgets('should show no payment methods after invalid search', + (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + await tester.enterText(find.byType(TextFormField), 'invalid'); + await tester.pump(); + + expect(find.byType(ListTile), findsNothing); + }); + }); + + group('PaymentSelectionPage - Payment Types', () { + Widget paymentSelectionPageTestWidget() => WidgetHelpers.testableWidget( + child: PaymentSelectionPage( + state: ValueNotifier( + PaymentDetailsState(paymentMethods: paymentTypes), + ), + isSelectingMethod: false, + ), + ); + + testWidgets('should show search field for types', (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + expect(find.byType(TextFormField), findsOneWidget); + expect(find.byIcon(Icons.search), findsOneWidget); + expect(find.text('Search'), findsOneWidget); + }); + + testWidgets('should show payment type list', (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + expect(find.byType(ListTile), findsExactly(3)); + expect(find.widgetWithText(ListTile, 'Bank'), findsOneWidget); + expect(find.widgetWithText(ListTile, 'Mobile money'), findsOneWidget); + expect(find.widgetWithText(ListTile, 'Wallet'), findsOneWidget); + }); + + testWidgets('should show a payment type after valid search', + (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + await tester.enterText(find.byType(TextFormField), 'Bank'); + await tester.pump(); + + expect(find.byType(ListTile), findsExactly(1)); + expect(find.widgetWithText(ListTile, 'Bank'), findsOneWidget); + }); + + testWidgets('should show no payment types after invalid search', + (tester) async { + await tester.pumpWidget(paymentSelectionPageTestWidget()); + + await tester.enterText(find.byType(TextFormField), 'invalid'); + await tester.pump(); + + expect(find.byType(ListTile), findsNothing); + }); + }); +} diff --git a/test/features/payment/payment_types_page_test.dart b/test/features/payment/payment_types_page_test.dart deleted file mode 100644 index adfee76c..00000000 --- a/test/features/payment/payment_types_page_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:didpay/features/payment/payment_details_state.dart'; -import 'package:didpay/features/payment/payment_method.dart'; -import 'package:didpay/features/payment/payment_types_page.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../helpers/widget_helpers.dart'; - -void main() { - final paymentMethods = [ - PaymentMethod( - type: 'Bank', - kind: 'BANK_ACCESS BANK', - ), - PaymentMethod( - type: 'Mobile money', - kind: 'MOMO_MTN', - ), - PaymentMethod( - type: 'Wallet', - kind: 'BTC_WALLET', - ), - ]; - - group('PaymentTypesPage', () { - Widget paymentTypesPageTestWidget() => WidgetHelpers.testableWidget( - child: PaymentTypesPage( - state: ValueNotifier( - PaymentDetailsState(paymentMethods: paymentMethods), - ), - ), - ); - - testWidgets('should show search field', (tester) async { - await tester.pumpWidget(paymentTypesPageTestWidget()); - - expect(find.byType(TextFormField), findsOneWidget); - expect(find.byIcon(Icons.search), findsOneWidget); - expect(find.text('Search'), findsOneWidget); - }); - - testWidgets('should show payment type list', (tester) async { - await tester.pumpWidget(paymentTypesPageTestWidget()); - - expect(find.byType(ListTile), findsExactly(3)); - expect(find.widgetWithText(ListTile, 'Bank'), findsOneWidget); - expect(find.widgetWithText(ListTile, 'Mobile money'), findsOneWidget); - expect(find.widgetWithText(ListTile, 'Wallet'), findsOneWidget); - }); - - testWidgets('should show a payment type after valid search', - (tester) async { - await tester.pumpWidget(paymentTypesPageTestWidget()); - - await tester.enterText(find.byType(TextFormField), 'Bank'); - await tester.pump(); - - expect(find.byType(ListTile), findsExactly(1)); - expect(find.widgetWithText(ListTile, 'Bank'), findsOneWidget); - }); - - testWidgets('should show no payment types after invalid search', - (tester) async { - await tester.pumpWidget(paymentTypesPageTestWidget()); - - await tester.enterText(find.byType(TextFormField), 'invalid'); - await tester.pump(); - - expect(find.byType(ListTile), findsNothing); - }); - }); -}