From 7ddc1794112a5b14a3469bf85934b00e34a22787 Mon Sep 17 00:00:00 2001 From: Ethan Lee <125412902+ethan-tbd@users.noreply.github.com> Date: Tue, 28 May 2024 15:47:25 -0700 Subject: [PATCH] feat: support multiple pfis (#173) --- lib/features/app/app.dart | 1 - lib/features/currency/currency_dropdown.dart | 10 +- lib/features/currency/currency_modal.dart | 103 +++++++++++------- lib/features/kcc/kcc_retrieval_page.dart | 1 - lib/features/kcc/kcc_webview_page.dart | 1 - lib/features/payin/payin.dart | 10 +- lib/features/payment/payment_amount_page.dart | 25 +++-- lib/features/payout/payout.dart | 10 +- lib/features/tbdex/tbdex_service.dart | 27 +++-- lib/shared/theme/grid.dart | 1 + test/features/home/home_page_test.dart | 4 +- test/features/payin/payin_test.dart | 23 ++-- .../payment/payment_amount_page_test.dart | 4 +- test/features/payout/payout_test.dart | 25 +++-- 14 files changed, 152 insertions(+), 93 deletions(-) diff --git a/lib/features/app/app.dart b/lib/features/app/app.dart index 04032567..c3e551f9 100644 --- a/lib/features/app/app.dart +++ b/lib/features/app/app.dart @@ -2,7 +2,6 @@ import 'package:didpay/features/app/app_tabs.dart'; import 'package:didpay/features/pfis/add_pfi_page.dart'; import 'package:didpay/features/pfis/pfis_notifier.dart'; import 'package:didpay/l10n/app_localizations.dart'; -import 'package:didpay/shared/modal_flow.dart'; import 'package:didpay/shared/theme/theme.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/currency/currency_dropdown.dart b/lib/features/currency/currency_dropdown.dart index 02d5b117..b95ca906 100644 --- a/lib/features/currency/currency_dropdown.dart +++ b/lib/features/currency/currency_dropdown.dart @@ -1,4 +1,5 @@ import 'package:didpay/features/currency/currency_modal.dart'; +import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/shared/theme/grid.dart'; import 'package:flutter/material.dart'; @@ -7,13 +8,15 @@ import 'package:tbdex/tbdex.dart'; class CurrencyDropdown extends HookConsumerWidget { final TransactionType transactionType; + final ValueNotifier selectedPfi; final ValueNotifier selectedOffering; - final List offerings; + final Map> offeringsMap; const CurrencyDropdown({ required this.transactionType, + required this.selectedPfi, required this.selectedOffering, - required this.offerings, + required this.offeringsMap, super.key, }); @@ -33,8 +36,9 @@ class CurrencyDropdown extends HookConsumerWidget { CurrencyModal.show( context, transactionType, + selectedPfi, selectedOffering, - offerings, + offeringsMap, ); }, ), diff --git a/lib/features/currency/currency_modal.dart b/lib/features/currency/currency_modal.dart index f3304470..f5983d9e 100644 --- a/lib/features/currency/currency_modal.dart +++ b/lib/features/currency/currency_modal.dart @@ -1,3 +1,4 @@ +import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/shared/theme/grid.dart'; import 'package:flutter/material.dart'; @@ -7,53 +8,77 @@ class CurrencyModal { static Future show( BuildContext context, TransactionType transactionType, + ValueNotifier selectedPfi, ValueNotifier selectedOffering, - List offerings, + Map> offeringsMap, ) => showModalBottomSheet( useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => SafeArea( - child: SizedBox( - height: 100 + (offerings.length * 30), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: Grid.xs), - child: Text( - 'Select currency', - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), + child: LayoutBuilder( + builder: (context, constraints) { + final totalOfferings = offeringsMap.values + .fold(0, (total, list) => total + list.length); + final height = totalOfferings * Grid.tileHeight; + final maxHeight = MediaQuery.of(context).size.height * 0.4; + + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxHeight, + minHeight: height < maxHeight ? height : maxHeight, ), - Expanded( - child: ListView( - children: offerings.map((offering) { - return ListTile( - onTap: () { - selectedOffering.value = offering; - Navigator.pop(context); - }, - title: _buildCurrencyTitle( - context, - offering, - transactionType, - ), - subtitle: _buildCurrencySubtitle( - context, - offering, - transactionType, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: Grid.xs), + child: Text( + 'Select currency', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + Flexible( + child: Scrollbar( + thumbVisibility: true, + child: ListView( + shrinkWrap: true, + children: offeringsMap.entries + .expand( + (entry) => entry.value.map( + (offering) => ListTile( + onTap: () { + selectedPfi.value = entry.key; + selectedOffering.value = offering; + Navigator.pop(context); + }, + title: _buildCurrencyTitle( + context, + offering, + transactionType, + ), + subtitle: _buildCurrencySubtitle( + context, + offering, + transactionType, + ), + trailing: + (selectedOffering.value == offering) + ? const Icon(Icons.check) + : null, + ), + ), + ) + .toList(), ), - trailing: (selectedOffering.value == offering) - ? const Icon(Icons.check) - : null, - ); - }).toList(), - ), + ), + ), + ], ), - ], - ), + ); + }, ), ), ); @@ -64,9 +89,7 @@ class CurrencyModal { TransactionType transactionType, ) => Text( - transactionType == TransactionType.deposit - ? offering.data.payin.currencyCode - : offering.data.payout.currencyCode, + '${offering.data.payin.currencyCode} → ${offering.data.payout.currencyCode}', style: Theme.of(context).textTheme.titleMedium, ); diff --git a/lib/features/kcc/kcc_retrieval_page.dart b/lib/features/kcc/kcc_retrieval_page.dart index 3ee93d2a..ea679471 100644 --- a/lib/features/kcc/kcc_retrieval_page.dart +++ b/lib/features/kcc/kcc_retrieval_page.dart @@ -20,7 +20,6 @@ class KccRetrievalPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - print("HI I AM A RETRIEVER"); final bearerDid = ref.watch(didProvider); final kccIssuanceService = ref.watch(kccIssuanceProvider); final credentialResponse = diff --git a/lib/features/kcc/kcc_webview_page.dart b/lib/features/kcc/kcc_webview_page.dart index a847a1eb..e21452c9 100644 --- a/lib/features/kcc/kcc_webview_page.dart +++ b/lib/features/kcc/kcc_webview_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:didpay/features/account/account_providers.dart'; import 'package:didpay/features/kcc/kcc_issuance_service.dart'; diff --git a/lib/features/payin/payin.dart b/lib/features/payin/payin.dart index 02384821..3ab9f107 100644 --- a/lib/features/payin/payin.dart +++ b/lib/features/payin/payin.dart @@ -1,5 +1,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:didpay/features/currency/currency_dropdown.dart'; +import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/l10n/app_localizations.dart'; import 'package:didpay/shared/shake_animated_text.dart'; @@ -14,15 +15,17 @@ class Payin extends HookWidget { final TransactionType transactionType; final ValueNotifier amount; final ValueNotifier keyPress; + final ValueNotifier selectedPfi; final ValueNotifier selectedOffering; - final List offerings; + final Map> offeringsMap; const Payin({ required this.transactionType, required this.amount, required this.keyPress, + required this.selectedPfi, required this.selectedOffering, - required this.offerings, + required this.offeringsMap, super.key, }); @@ -143,8 +146,9 @@ class Payin extends HookWidget { case TransactionType.deposit: return CurrencyDropdown( transactionType: transactionType, + selectedPfi: selectedPfi, selectedOffering: selectedOffering, - offerings: offerings, + offeringsMap: offeringsMap, ); case TransactionType.withdraw: case TransactionType.send: diff --git a/lib/features/payment/payment_amount_page.dart b/lib/features/payment/payment_amount_page.dart index 1072225d..47ff3da6 100644 --- a/lib/features/payment/payment_amount_page.dart +++ b/lib/features/payment/payment_amount_page.dart @@ -35,9 +35,10 @@ class PaymentAmountPage extends HookConsumerWidget { final payinAmount = useState('0'); final payoutAmount = useState(0); final keyPress = useState(PayinKeyPress(0, '')); + final selectedPfi = useState(null); final selectedOffering = useState(null); final getOfferingsState = - useState>>(const AsyncLoading()); + useState>>>(const AsyncLoading()); useEffect( () { @@ -51,8 +52,9 @@ class PaymentAmountPage extends HookConsumerWidget { appBar: AppBar(), body: SafeArea( child: getOfferingsState.value.when( - data: (offerings) { - selectedOffering.value ??= offerings.first; + data: (offeringsMap) { + selectedPfi.value ??= offeringsMap.keys.first; + selectedOffering.value ??= offeringsMap[selectedPfi.value]!.first; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -72,8 +74,9 @@ class PaymentAmountPage extends HookConsumerWidget { transactionType: transactionType, amount: payinAmount, keyPress: keyPress, + selectedPfi: selectedPfi, selectedOffering: selectedOffering, - offerings: offerings, + offeringsMap: offeringsMap, ), const SizedBox(height: Grid.sm), Payout( @@ -81,8 +84,9 @@ class PaymentAmountPage extends HookConsumerWidget { double.tryParse(payinAmount.value) ?? 0.0, transactionType: transactionType, payoutAmount: payoutAmount, + selectedPfi: selectedPfi, selectedOffering: selectedOffering, - offerings: offerings, + offeringsMap: offeringsMap, ), const SizedBox(height: Grid.xl), FeeDetails( @@ -109,9 +113,8 @@ class PaymentAmountPage extends HookConsumerWidget { currency: selectedOffering.value?.data.payout.currencyCode ?? '', ), + selectedPfi.value, selectedOffering.value, - // TODO(ethan-tbd): get pfi from selectedOffering - ref.read(pfisProvider)[0], ), ], ); @@ -131,8 +134,8 @@ class PaymentAmountPage extends HookConsumerWidget { BuildContext context, String payinAmount, String payoutAmount, + Pfi? selectedPfi, Offering? selectedOffering, - Pfi pfi, ) => Padding( padding: const EdgeInsets.symmetric(horizontal: Grid.side), @@ -151,7 +154,7 @@ class PaymentAmountPage extends HookConsumerWidget { selectedOffering?.data.payout.methods.firstOrNull, ), paymentState: PaymentState( - pfi: pfi, + pfi: selectedPfi ?? const Pfi(did: ''), payoutAmount: payoutAmount, payinCurrency: selectedOffering?.data.payin.currencyCode ?? '', @@ -175,13 +178,13 @@ class PaymentAmountPage extends HookConsumerWidget { void _getOfferings( WidgetRef ref, - ValueNotifier>> state, + ValueNotifier>>> state, ) { state.value = const AsyncLoading(); ref .read(tbdexServiceProvider) .getOfferings(ref.read(pfisProvider)) - .then((offerings) => state.value = AsyncData(offerings)) + .then((offeringsMap) => state.value = AsyncData(offeringsMap)) .catchError((error, stackTrace) { state.value = AsyncError(error, stackTrace); throw error; diff --git a/lib/features/payout/payout.dart b/lib/features/payout/payout.dart index f68684a4..e14e9926 100644 --- a/lib/features/payout/payout.dart +++ b/lib/features/payout/payout.dart @@ -1,5 +1,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:didpay/features/currency/currency_dropdown.dart'; +import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/l10n/app_localizations.dart'; import 'package:didpay/shared/theme/grid.dart'; @@ -12,15 +13,17 @@ class Payout extends HookWidget { final double payinAmount; final TransactionType transactionType; final ValueNotifier payoutAmount; + final ValueNotifier selectedPfi; final ValueNotifier selectedOffering; - final List offerings; + final Map> offeringsMap; const Payout({ required this.payinAmount, required this.transactionType, required this.payoutAmount, + required this.selectedPfi, required this.selectedOffering, - required this.offerings, + required this.offeringsMap, super.key, }); @@ -80,8 +83,9 @@ class Payout extends HookWidget { case TransactionType.withdraw: return CurrencyDropdown( transactionType: transactionType, + selectedPfi: selectedPfi, selectedOffering: selectedOffering, - offerings: offerings, + offeringsMap: offeringsMap, ); case TransactionType.deposit: case TransactionType.send: diff --git a/lib/features/tbdex/tbdex_service.dart b/lib/features/tbdex/tbdex_service.dart index 8e197860..10646d35 100644 --- a/lib/features/tbdex/tbdex_service.dart +++ b/lib/features/tbdex/tbdex_service.dart @@ -8,46 +8,49 @@ import 'package:web5/web5.dart'; final tbdexServiceProvider = Provider((_) => TbdexService()); class TbdexService { - // TODO(ethan-tbd): return Map> - Future> getOfferings(List pfis) async { - final offerings = []; + Future>> getOfferings(List pfis) async { + final offeringsMap = >{}; for (final pfi in pfis) { - final response = await TbdexHttpClient.listOfferings(pfi.did); - if (response.statusCode.category == HttpStatus.success) { - offerings.addAll(response.data!); + try { + final response = await TbdexHttpClient.listOfferings(pfi.did); + if (response.statusCode.category == HttpStatus.success) { + offeringsMap[pfi] = response.data!; + } + } on Exception catch (_) { + rethrow; } } - if (offerings.isEmpty) { + if (offeringsMap.isEmpty) { throw Exception('no offerings found'); } - return offerings; + return offeringsMap; } Future>> getExchanges( BearerDid did, List pfis, ) async { - final exchangeMap = >{}; + final exchangesMap = >{}; for (final pfi in pfis) { try { final response = await TbdexHttpClient.listExchanges(did, pfi.did); if (response.statusCode.category == HttpStatus.success) { - exchangeMap[pfi] = response.data!; + exchangesMap[pfi] = response.data!; } else { throw Exception( 'failed to fetch exchanges with status code ${response.statusCode}', ); } } on Exception { - return exchangeMap; + return exchangesMap; } } - return exchangeMap; + return exchangesMap; } Future> getExchange( diff --git a/lib/shared/theme/grid.dart b/lib/shared/theme/grid.dart index 559133e8..47af8897 100644 --- a/lib/shared/theme/grid.dart +++ b/lib/shared/theme/grid.dart @@ -14,4 +14,5 @@ class Grid { static const double side = 20; static const double radius = 25; + static const double tileHeight = 80; } diff --git a/test/features/home/home_page_test.dart b/test/features/home/home_page_test.dart index cc6c1550..c46ef77d 100644 --- a/test/features/home/home_page_test.dart +++ b/test/features/home/home_page_test.dart @@ -24,7 +24,9 @@ void main() async { const pfi = Pfi(did: 'did:web:x%3A8892:ingress'); final jsonList = jsonDecode(jsonString) as List; - final offerings = [Offering.fromJson(jsonList[0])]; + final offerings = { + pfi: [Offering.fromJson(jsonList[0])], + }; late MockTbdexService mockTbdexService; group('HomePage', () { diff --git a/test/features/payin/payin_test.dart b/test/features/payin/payin_test.dart index 11ad24bf..cd4740bb 100644 --- a/test/features/payin/payin_test.dart +++ b/test/features/payin/payin_test.dart @@ -1,4 +1,5 @@ import 'package:didpay/features/payin/payin.dart'; +import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/shared/shake_animated_text.dart'; import 'package:flutter/material.dart'; @@ -10,6 +11,7 @@ import '../../helpers/widget_helpers.dart'; void main() { group('Payin', () { final amount = ValueNotifier('70'); + final pfi = ValueNotifier(null); final offering = ValueNotifier( Offering.create( 'pfiDid', @@ -45,9 +47,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.deposit, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), @@ -61,9 +64,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.deposit, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), @@ -77,9 +81,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.deposit, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), @@ -93,9 +98,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.withdraw, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), @@ -109,9 +115,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.deposit, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), @@ -126,9 +133,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.deposit, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), @@ -143,9 +151,10 @@ void main() { WidgetHelpers.testableWidget( child: Payin( transactionType: TransactionType.withdraw, - offerings: const [], + offeringsMap: const {}, amount: amount, keyPress: keyPress, + selectedPfi: pfi, selectedOffering: offering, ), ), diff --git a/test/features/payment/payment_amount_page_test.dart b/test/features/payment/payment_amount_page_test.dart index 63fb6275..b1f04ead 100644 --- a/test/features/payment/payment_amount_page_test.dart +++ b/test/features/payment/payment_amount_page_test.dart @@ -22,7 +22,9 @@ void main() { const pfi = Pfi(did: 'did:web:x%3A8892:ingress'); final jsonList = jsonDecode(jsonString) as List; - final offerings = [Offering.fromJson(jsonList[0])]; + final offerings = { + pfi: [Offering.fromJson(jsonList[0])], + }; late MockTbdexService mockTbdexService; group('PaymentAmountPage', () { diff --git a/test/features/payout/payout_test.dart b/test/features/payout/payout_test.dart index d70b3be3..84a76492 100644 --- a/test/features/payout/payout_test.dart +++ b/test/features/payout/payout_test.dart @@ -1,4 +1,5 @@ import 'package:didpay/features/payout/payout.dart'; +import 'package:didpay/features/pfis/pfi.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -9,6 +10,7 @@ import '../../helpers/widget_helpers.dart'; void main() { group('Payout', () { final amount = ValueNotifier(2); + final pfi = ValueNotifier(null); final offering = ValueNotifier( Offering.create( 'pfiDid', @@ -43,10 +45,11 @@ void main() { WidgetHelpers.testableWidget( child: Payout( payoutAmount: amount, + selectedPfi: pfi, selectedOffering: offering, transactionType: TransactionType.deposit, payinAmount: 34, - offerings: const [], + offeringsMap: const {}, ), ), ); @@ -59,10 +62,11 @@ void main() { WidgetHelpers.testableWidget( child: Payout( payoutAmount: amount, + selectedPfi: pfi, selectedOffering: offering, transactionType: TransactionType.deposit, - payinAmount: 0, - offerings: const [], + payinAmount: 34, + offeringsMap: const {}, ), ), ); @@ -75,10 +79,11 @@ void main() { WidgetHelpers.testableWidget( child: Payout( payoutAmount: amount, + selectedPfi: pfi, selectedOffering: offering, transactionType: TransactionType.withdraw, - payinAmount: 0, - offerings: const [], + payinAmount: 34, + offeringsMap: const {}, ), ), ); @@ -92,10 +97,11 @@ void main() { WidgetHelpers.testableWidget( child: Payout( payoutAmount: amount, + selectedPfi: pfi, selectedOffering: offering, transactionType: TransactionType.withdraw, - payinAmount: 0, - offerings: const [], + payinAmount: 34, + offeringsMap: const {}, ), ), ); @@ -109,10 +115,11 @@ void main() { WidgetHelpers.testableWidget( child: Payout( payoutAmount: amount, + selectedPfi: pfi, selectedOffering: offering, transactionType: TransactionType.deposit, - payinAmount: 0, - offerings: const [], + payinAmount: 34, + offeringsMap: const {}, ), ), );