From a026294e836d82a650825da9c3ec5eb2a18aa248 Mon Sep 17 00:00:00 2001 From: Ethan Lee <125412902+ethan-tbd@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:12:40 -0700 Subject: [PATCH] feat: add `DapState` (#276) --- lib/features/dap/dap_state.dart | 56 +++++++++++++++++++ lib/features/payment/payment_amount_page.dart | 51 ++++++++++------- .../payment/payment_details_page.dart | 6 +- .../payment/payment_details_state.dart | 16 ------ lib/features/payment/payment_method.dart | 5 -- lib/features/payment/payment_state.dart | 4 +- lib/features/send/send_page.dart | 5 +- lib/features/tbdex/tbdex_service.dart | 30 +++++++--- lib/shared/json_schema_form.dart | 11 +++- test/features/home/home_page_test.dart | 2 +- .../payment/payment_amount_page_test.dart | 1 - 11 files changed, 128 insertions(+), 59 deletions(-) create mode 100644 lib/features/dap/dap_state.dart diff --git a/lib/features/dap/dap_state.dart b/lib/features/dap/dap_state.dart new file mode 100644 index 00000000..b9fc3720 --- /dev/null +++ b/lib/features/dap/dap_state.dart @@ -0,0 +1,56 @@ +import 'package:dap/dap.dart'; +import 'package:didpay/features/payment/payment_method.dart'; + +class DapState { + final MoneyAddress? selectedAddress; + final List? moneyAddresses; + + DapState({ + this.selectedAddress, + this.moneyAddresses, + }); + + static const Map protocolToKindMap = { + 'addr': 'BTC_ONCHAIN_PAYOUT', + 'lnaddr': 'BTC_LN_PAYOUT', + 'sol': 'USDC_ONCHAIN', + }; + + String? get protocol => selectedAddress?.css.split(':').firstOrNull; + + String? get paymentAddress => selectedAddress?.css.split(':').lastOrNull; + + List? get currencies => + moneyAddresses?.map((address) => address.currency).toList(); + + MoneyAddress? getSelectedMoneyAddress(String? paymentCurrency) => + moneyAddresses?.firstWhere( + (address) => address.currency.toUpperCase() == paymentCurrency, + ); + + List? filterPaymentMethods( + List? paymentMethods, + ) { + final protocolKinds = moneyAddresses + ?.map( + (address) => + protocolToKindMap[address.css.split(':').firstOrNull ?? ''], + ) + .toSet(); + + return paymentMethods + ?.where( + (method) => protocolKinds?.contains(method.kind) ?? false, + ) + .toList(); + } + + DapState copyWith({ + MoneyAddress? selectedAddress, + List? moneyAddresses, + }) => + DapState( + selectedAddress: selectedAddress ?? this.selectedAddress, + moneyAddresses: moneyAddresses ?? this.moneyAddresses, + ); +} diff --git a/lib/features/payment/payment_amount_page.dart b/lib/features/payment/payment_amount_page.dart index 423cb530..c9f76de8 100644 --- a/lib/features/payment/payment_amount_page.dart +++ b/lib/features/payment/payment_amount_page.dart @@ -1,3 +1,4 @@ +import 'package:didpay/features/dap/dap_state.dart'; import 'package:didpay/features/payin/payin.dart'; import 'package:didpay/features/payment/payment_amount_state.dart'; import 'package:didpay/features/payment/payment_details_page.dart'; @@ -24,9 +25,11 @@ import 'package:tbdex/tbdex.dart'; class PaymentAmountPage extends HookConsumerWidget { final PaymentState paymentState; + final DapState? dapState; const PaymentAmountPage({ required this.paymentState, + this.dapState, super.key, }); @@ -43,7 +46,7 @@ class PaymentAmountPage extends HookConsumerWidget { () { Future.delayed( Duration.zero, - () => _getOfferings(context, ref, offerings), + () => _getOfferings(context, ref, dapState?.currencies, offerings), ); return null; }, @@ -72,6 +75,7 @@ class PaymentAmountPage extends HookConsumerWidget { onRetry: () => _getOfferings( context, ref, + dapState?.currencies, offerings, ), ), @@ -134,33 +138,38 @@ class PaymentAmountPage extends HookConsumerWidget { : () => Navigator.of(context).push( MaterialPageRoute( builder: (context) { + final paymentCurrency = paymentState.transactionType == + TransactionType.deposit + ? state.value?.payinCurrency + : state.value?.payoutCurrency; + + final paymentMethods = paymentState.transactionType == + TransactionType.deposit + ? state.value?.selectedOffering?.data.payin.methods + .map(PaymentMethod.fromPayinMethod) + .toList() + : state.value?.selectedOffering?.data.payout.methods + .map(PaymentMethod.fromPayoutMethod) + .toList(); + final paymentDetailsState = (paymentState.paymentDetailsState ?? PaymentDetailsState()) .copyWith( - paymentCurrency: paymentState.transactionType == - TransactionType.deposit - ? state.value?.payinCurrency - : state.value?.payoutCurrency, - paymentMethods: paymentState.transactionType == - TransactionType.deposit - ? state - .value?.selectedOffering?.data.payin.methods - .map(PaymentMethod.fromPayinMethod) - .toList() - : state - .value?.selectedOffering?.data.payout.methods - .map(PaymentMethod.fromPayoutMethod) - .toList(), + paymentCurrency: paymentCurrency, + paymentMethods: + dapState?.filterPaymentMethods(paymentMethods) ?? + paymentMethods, ); return PaymentDetailsPage( paymentState: paymentState.copyWith( paymentAmountState: state.value, - paymentDetailsState: paymentDetailsState.copyWith( - paymentMethods: - paymentDetailsState.filterDapProtocol(), - ), + paymentDetailsState: paymentDetailsState, + ), + dapState: dapState?.copyWith( + selectedAddress: dapState + ?.getSelectedMoneyAddress(paymentCurrency), ), ); }, @@ -174,14 +183,14 @@ class PaymentAmountPage extends HookConsumerWidget { Future _getOfferings( BuildContext context, WidgetRef ref, + List? payoutCurrencies, ValueNotifier>>> state, ) async { state.value = const AsyncLoading(); try { final offerings = await ref.read(tbdexServiceProvider).getOfferings( ref.read(pfisProvider), - payinCurrency: paymentState.filterPayinCurrency, - payoutCurrency: paymentState.filterPayoutCurrency, + payoutCurrencies: payoutCurrencies, ); if (context.mounted) { diff --git a/lib/features/payment/payment_details_page.dart b/lib/features/payment/payment_details_page.dart index 8148a61e..47efdca0 100644 --- a/lib/features/payment/payment_details_page.dart +++ b/lib/features/payment/payment_details_page.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:didpay/features/dap/dap_state.dart'; 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'; @@ -27,9 +28,11 @@ import 'package:tbdex/tbdex.dart'; class PaymentDetailsPage extends HookConsumerWidget { final PaymentState paymentState; + final DapState? dapState; const PaymentDetailsPage({ required this.paymentState, + this.dapState, super.key, }); @@ -104,7 +107,7 @@ class PaymentDetailsPage extends HookConsumerWidget { Header( title: paymentState.transactionType == TransactionType.send - ? state.value.moneyAddresses != null + ? dapState?.moneyAddresses != null ? Loc.of(context).checkTheirPaymentDetails : Loc.of(context).enterTheirPaymentDetails : Loc.of(context).enterYourPaymentDetails, @@ -144,6 +147,7 @@ class PaymentDetailsPage extends HookConsumerWidget { Expanded( child: JsonSchemaForm( state: state.value, + dapState: dapState, onSubmit: (formData) async { state.value = state.value.copyWith(formData: formData); diff --git a/lib/features/payment/payment_details_state.dart b/lib/features/payment/payment_details_state.dart index e8b1d6f1..8be7335d 100644 --- a/lib/features/payment/payment_details_state.dart +++ b/lib/features/payment/payment_details_state.dart @@ -1,4 +1,3 @@ -import 'package:dap/dap.dart'; import 'package:didpay/features/payment/payment_method.dart'; class PaymentDetailsState { @@ -8,7 +7,6 @@ class PaymentDetailsState { final String? selectedPaymentType; final PaymentMethod? selectedPaymentMethod; final List? paymentMethods; - final List? moneyAddresses; final List? credentialsJwt; final Map? formData; @@ -19,7 +17,6 @@ class PaymentDetailsState { this.selectedPaymentType, this.selectedPaymentMethod, this.paymentMethods, - this.moneyAddresses, this.credentialsJwt, this.formData, }); @@ -37,17 +34,6 @@ class PaymentDetailsState { ) .toList(); - List? filterDapProtocol() => paymentMethods - ?.where( - (method) => method.kind.contains( - PaymentMethod.protocolPaymentMap[ - moneyAddresses?.firstOrNull?.css.split(':').firstOrNull ?? - ''] ?? - '', - ), - ) - .toList(); - PaymentDetailsState copyWith({ String? paymentCurrency, String? paymentName, @@ -55,7 +41,6 @@ class PaymentDetailsState { String? selectedPaymentType, PaymentMethod? selectedPaymentMethod, List? paymentMethods, - List? moneyAddresses, List? credentialsJwt, Map? formData, }) { @@ -67,7 +52,6 @@ class PaymentDetailsState { selectedPaymentMethod: selectedPaymentMethod ?? this.selectedPaymentMethod, paymentMethods: paymentMethods ?? this.paymentMethods, - moneyAddresses: moneyAddresses ?? this.moneyAddresses, credentialsJwt: credentialsJwt ?? this.credentialsJwt, formData: formData ?? this.formData, ); diff --git a/lib/features/payment/payment_method.dart b/lib/features/payment/payment_method.dart index c2cb548b..169067cc 100644 --- a/lib/features/payment/payment_method.dart +++ b/lib/features/payment/payment_method.dart @@ -48,9 +48,4 @@ class PaymentMethod { fee: fee ?? this.fee, ); } - - static const Map protocolPaymentMap = { - 'addr': 'BTC_ONCHAIN_PAYOUT', - 'lnaddr': 'BTC_LN_PAYOUT', - }; } diff --git a/lib/features/payment/payment_state.dart b/lib/features/payment/payment_state.dart index 9b64cb04..77de6a27 100644 --- a/lib/features/payment/payment_state.dart +++ b/lib/features/payment/payment_state.dart @@ -30,10 +30,8 @@ class PaymentState { switch (transactionType) { case TransactionType.deposit: return 'USDC'; + // TODO(ethan-tbd): use dap currencies here when tbdex-dart supports list of currencies case TransactionType.send: - return paymentDetailsState?.paymentCurrency ?? - paymentDetailsState?.moneyAddresses?.firstOrNull?.currency - .toUpperCase(); case TransactionType.withdraw: return null; } diff --git a/lib/features/send/send_page.dart b/lib/features/send/send_page.dart index fedf0e2c..f9f8154b 100644 --- a/lib/features/send/send_page.dart +++ b/lib/features/send/send_page.dart @@ -1,5 +1,6 @@ import 'package:dap/dap.dart'; import 'package:didpay/features/dap/dap_form.dart'; +import 'package:didpay/features/dap/dap_state.dart'; import 'package:didpay/features/feature_flags/feature_flag.dart'; import 'package:didpay/features/feature_flags/feature_flags_notifier.dart'; import 'package:didpay/features/payment/payment_amount_page.dart'; @@ -58,9 +59,11 @@ class SendPage extends HookConsumerWidget { transactionType: TransactionType.send, paymentDetailsState: PaymentDetailsState( paymentName: recipientDap.dap, - moneyAddresses: moneyAddresses, ), ), + dapState: DapState( + moneyAddresses: moneyAddresses, + ), ), fullscreenDialog: true, ), diff --git a/lib/features/tbdex/tbdex_service.dart b/lib/features/tbdex/tbdex_service.dart index 80986c21..3832f9a3 100644 --- a/lib/features/tbdex/tbdex_service.dart +++ b/lib/features/tbdex/tbdex_service.dart @@ -11,8 +11,8 @@ final tbdexServiceProvider = Provider((_) => TbdexService()); class TbdexService { Future>> getOfferings( List pfis, { - String? payinCurrency, - String? payoutCurrency, + List? payinCurrencies, + List? payoutCurrencies, }) async { final offeringsMap = >{}; @@ -20,13 +20,27 @@ class TbdexService { try { final offerings = await TbdexHttpClient.listOfferings( pfi.did, - filter: GetOfferingsFilter( - payinCurrency: payinCurrency, - payoutCurrency: payoutCurrency, - ), + // TODO(ethan-tbd): update tbdex-dart to support list of payin and payout currencies + // filter: GetOfferingsFilter( + // payinCurrency: payinCurrency, + // payoutCurrency: payoutCurrency, + // ), ); - offeringsMap[pfi] = offerings; + // TODO(ethan-tbd): remove when tbdex-dart supports filtering by payin and payout currencies + final filteredOfferings = (payoutCurrencies == null) + ? offerings + : offerings + .where( + (offering) => payoutCurrencies.contains( + offering.data.payout.currencyCode.toLowerCase(), + ), + ) + .toList(); + + if (filteredOfferings.isNotEmpty) { + offeringsMap[pfi] = filteredOfferings; + } } on Exception catch (e) { if (e is ValidationError) continue; rethrow; @@ -39,7 +53,7 @@ class TbdexService { ); } - // temporarily filter out stored balance payin offerings + // TODO(ethan-tbd): remove later, temporarily filter out stored balance payin offerings final filteredOfferingsMap = offeringsMap.map( (key, value) => MapEntry( key, diff --git a/lib/shared/json_schema_form.dart b/lib/shared/json_schema_form.dart index aa05b30e..e84530ad 100644 --- a/lib/shared/json_schema_form.dart +++ b/lib/shared/json_schema_form.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:didpay/features/dap/dap_state.dart'; import 'package:didpay/features/payment/payment_details_state.dart'; import 'package:didpay/shared/next_button.dart'; import 'package:didpay/shared/theme/grid.dart'; @@ -11,12 +12,14 @@ import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; class JsonSchemaForm extends HookWidget { final PaymentDetailsState state; final Future Function(Map) onSubmit; + final DapState? dapState; final _formKey = GlobalKey(); JsonSchemaForm({ required this.state, required this.onSubmit, + this.dapState, super.key, }); @@ -58,9 +61,13 @@ class JsonSchemaForm extends HookWidget { final pattern = valueMap['pattern'] as String?; final formatter = TextInputUtil.getMaskFormatter(pattern); - final initialText = state.formData?[key] ?? - state.moneyAddresses?.firstOrNull?.css.split(':').lastOrNull ?? + + final dapPrefillText = (key == 'chain' + ? dapState?.protocol + : dapState?.paymentAddress) ?? ''; + + final initialText = state.formData?[key] ?? dapPrefillText; final controller = TextEditingController(text: initialText); final focusNode = FocusNode(); diff --git a/test/features/home/home_page_test.dart b/test/features/home/home_page_test.dart index 6e2fe4a6..f6421f04 100644 --- a/test/features/home/home_page_test.dart +++ b/test/features/home/home_page_test.dart @@ -47,7 +47,7 @@ void main() async { when( () => mockTbdexService.getOfferings( pfis, - payinCurrency: 'USDC', + payinCurrencies: ['USDC'], ), ).thenAnswer((_) async => offerings); diff --git a/test/features/payment/payment_amount_page_test.dart b/test/features/payment/payment_amount_page_test.dart index 25863c0e..3c82303a 100644 --- a/test/features/payment/payment_amount_page_test.dart +++ b/test/features/payment/payment_amount_page_test.dart @@ -32,7 +32,6 @@ void main() async { when( () => mockTbdexService.getOfferings( pfis, - payoutCurrency: 'USDC', ), ).thenAnswer((_) async => offerings); });