From 0d1bb59e9b1070fad25ab68e6ec64b8323fea8cf Mon Sep 17 00:00:00 2001 From: Ethan Lee <125412902+ethan-tbd@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:31:48 -0700 Subject: [PATCH] chore: remove polling widgets (#281) --- lib/features/dap/dap_form.dart | 2 - .../payment/payment_details_page.dart | 115 +++++++----- .../payment/payment_details_state.dart | 5 + .../payment_fetch_instructions_widget.dart | 87 ---------- .../payment/payment_fetch_quote_widget.dart | 91 ---------- lib/features/payment/payment_review_page.dart | 163 +++++++++++------- lib/features/payment/payment_state.dart | 5 - lib/features/tbdex/tbdex_service.dart | 6 - .../payment/payment_review_page_test.dart | 2 +- 9 files changed, 182 insertions(+), 294 deletions(-) delete mode 100644 lib/features/payment/payment_fetch_instructions_widget.dart delete mode 100644 lib/features/payment/payment_fetch_quote_widget.dart diff --git a/lib/features/dap/dap_form.dart b/lib/features/dap/dap_form.dart index d7f517e..72af98a 100644 --- a/lib/features/dap/dap_form.dart +++ b/lib/features/dap/dap_form.dart @@ -130,8 +130,6 @@ class DapForm extends HookConsumerWidget { dap.value = const AsyncValue.loading(); final result = await DapResolver().resolve(parsedDap); - await Future.delayed(const Duration(milliseconds: 500)); - dap.value = AsyncValue.data(result.dap); await onSubmit(result.dap, result.moneyAddresses); } on Exception catch (e) { diff --git a/lib/features/payment/payment_details_page.dart b/lib/features/payment/payment_details_page.dart index 47efdca..807dc83 100644 --- a/lib/features/payment/payment_details_page.dart +++ b/lib/features/payment/payment_details_page.dart @@ -3,9 +3,9 @@ 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'; -import 'package:didpay/features/payment/payment_fetch_quote_widget.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_state.dart'; import 'package:didpay/features/payment/payment_types_page.dart'; import 'package:didpay/features/pfis/pfi.dart'; @@ -38,8 +38,7 @@ class PaymentDetailsPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final quote = useState>(ref.watch(quoteProvider)); - final rfq = useState?>(null); + final quote = useState?>(null); final state = useState( paymentState.paymentDetailsState ?? PaymentDetailsState(), ); @@ -63,41 +62,32 @@ class PaymentDetailsPage extends HookConsumerWidget { [state.value.selectedPaymentType], ); - final isAwaiting = - (rfq.value?.isLoading ?? false) || (quote.value.isLoading); + final isAwaiting = quote.value?.isLoading ?? false; return PopScope( canPop: !isAwaiting, onPopInvoked: (_) { if (isAwaiting) { - rfq.value = null; - quote.value = const AsyncData(null); + ref.read(quoteProvider.notifier).stopPolling(); + quote.value = null; } }, child: Scaffold( appBar: AppBar(), body: SafeArea( - child: rfq.value != null - ? rfq.value!.when( - data: (sentRfq) => PaymentFetchQuoteWidget( - paymentState: paymentState.copyWith( - paymentDetailsState: state.value - .copyWith(exchangeId: sentRfq.metadata.exchangeId), - ), - quote: quote, - rfq: rfq, - ref: ref, - ), + child: quote.value != null + ? quote.value!.when( + data: (_) => Container(), loading: () => LoadingMessage( - message: Loc.of(context).sendingYourRequest, + message: Loc.of(context).fetchingYourQuote, ), error: (error, _) => ErrorMessage( message: error.toString(), onRetry: () => _sendRfq( context, ref, - state.value, - rfq, + state, + quote, ), ), ) @@ -125,12 +115,7 @@ class PaymentDetailsPage extends HookConsumerWidget { availableMethods, state, ), - _buildPaymentForm( - context, - ref, - state, - rfq, - ), + _buildPaymentForm(context, ref, quote, state), ], ), ), @@ -141,14 +126,15 @@ class PaymentDetailsPage extends HookConsumerWidget { Widget _buildPaymentForm( BuildContext context, WidgetRef ref, + ValueNotifier?> quote, ValueNotifier state, - ValueNotifier?> rfq, ) => Expanded( child: JsonSchemaForm( state: state.value, dapState: dapState, onSubmit: (formData) async { + quote.value = const AsyncLoading(); state.value = state.value.copyWith(formData: formData); final presentationDefinition = paymentState @@ -186,10 +172,19 @@ class PaymentDetailsPage extends HookConsumerWidget { if (context.mounted) { await _sendRfq( + context, + ref, + state, + quote, + ); + } + + if (context.mounted && quote.value != null) { + await _pollForQuote( context, ref, state.value, - rfq, + quote, ); } }, @@ -282,32 +277,70 @@ class PaymentDetailsPage extends HookConsumerWidget { Future _sendRfq( BuildContext context, WidgetRef ref, - PaymentDetailsState state, - ValueNotifier?> rfq, + ValueNotifier state, + ValueNotifier?> quote, ) async { - rfq.value = const AsyncLoading(); - try { final updatedPaymentState = - paymentState.copyWith(paymentDetailsState: state); + paymentState.copyWith(paymentDetailsState: state.value); final sentRfq = await ref.read(tbdexServiceProvider).sendRfq( ref.read(didProvider), - updatedPaymentState.paymentAmountState?.pfiDid ?? '', - updatedPaymentState.paymentAmountState?.offeringId ?? '', - updatedPaymentState.paymentAmountState?.payinAmount ?? '', + paymentState.paymentAmountState?.pfiDid ?? '', + paymentState.paymentAmountState?.offeringId ?? '', + paymentState.paymentAmountState?.payinAmount ?? '', updatedPaymentState.selectedPayinKind ?? '', updatedPaymentState.selectedPayoutKind ?? '', updatedPaymentState.payinDetails, updatedPaymentState.payoutDetails, - claims: updatedPaymentState.paymentDetailsState?.credentialsJwt, + claims: state.value.credentialsJwt, ); - if (context.mounted && rfq.value != null) { - rfq.value = AsyncData(sentRfq); + state.value = state.value.copyWith(exchangeId: sentRfq.metadata.id); + } on Exception catch (e) { + quote.value = AsyncError(e, StackTrace.current); + } + } + + Future _pollForQuote( + BuildContext context, + WidgetRef ref, + PaymentDetailsState state, + ValueNotifier?> quote, + ) async { + quote.value = const AsyncLoading(); + final quoteNotifier = ref.read(quoteProvider.notifier); + + try { + final fetchedQuote = await quoteNotifier.startPolling( + paymentState.paymentAmountState?.pfiDid ?? '', + state.exchangeId ?? '', + ); + + if (context.mounted) { + quoteNotifier.stopPolling(); + + if (fetchedQuote != null && quote.value != null) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PaymentReviewPage( + paymentState: paymentState.copyWith( + paymentDetailsState: state.copyWith(quote: fetchedQuote), + ), + ), + ), + ); + } + + if (context.mounted) { + quote.value = null; + } } } on Exception catch (e) { - rfq.value = AsyncError(e, StackTrace.current); + if (context.mounted) { + quoteNotifier.stopPolling(); + quote.value = AsyncError(e, StackTrace.current); + } } } } diff --git a/lib/features/payment/payment_details_state.dart b/lib/features/payment/payment_details_state.dart index 8be7335..48d6e54 100644 --- a/lib/features/payment/payment_details_state.dart +++ b/lib/features/payment/payment_details_state.dart @@ -1,4 +1,5 @@ import 'package:didpay/features/payment/payment_method.dart'; +import 'package:tbdex/tbdex.dart'; class PaymentDetailsState { final String? paymentCurrency; @@ -9,6 +10,7 @@ class PaymentDetailsState { final List? paymentMethods; final List? credentialsJwt; final Map? formData; + final Quote? quote; PaymentDetailsState({ this.paymentCurrency, @@ -19,6 +21,7 @@ class PaymentDetailsState { this.paymentMethods, this.credentialsJwt, this.formData, + this.quote, }); Set? get paymentTypes => @@ -43,6 +46,7 @@ class PaymentDetailsState { List? paymentMethods, List? credentialsJwt, Map? formData, + Quote? quote, }) { return PaymentDetailsState( paymentCurrency: paymentCurrency ?? this.paymentCurrency, @@ -54,6 +58,7 @@ class PaymentDetailsState { paymentMethods: paymentMethods ?? this.paymentMethods, credentialsJwt: credentialsJwt ?? this.credentialsJwt, formData: formData ?? this.formData, + quote: quote ?? this.quote, ); } } diff --git a/lib/features/payment/payment_fetch_instructions_widget.dart b/lib/features/payment/payment_fetch_instructions_widget.dart deleted file mode 100644 index 11d025c..0000000 --- a/lib/features/payment/payment_fetch_instructions_widget.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:didpay/features/payment/payment_state.dart'; -import 'package:didpay/features/tbdex/tbdex_order_instructions_notifier.dart'; -import 'package:didpay/l10n/app_localizations.dart'; -import 'package:didpay/shared/error_message.dart'; -import 'package:didpay/shared/loading_message.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:tbdex/tbdex.dart'; - -class PaymentFetchInstructionsWidget extends HookWidget { - final PaymentState paymentState; - final ValueNotifier> instructions; - final ValueNotifier?> order; - final Future Function(BuildContext, OrderInstructions) - onInstructionsFetched; - final WidgetRef ref; - - const PaymentFetchInstructionsWidget({ - required this.paymentState, - required this.instructions, - required this.order, - required this.onInstructionsFetched, - required this.ref, - super.key, - }); - - @override - Widget build(BuildContext context) { - TbdexOrderInstructionsNotifier getInstructionsNotifier() => - ref.read(orderInstructionsProvider.notifier); - - useEffect( - () { - Future.delayed(Duration.zero, () async { - if (context.mounted) { - await _pollForInstructions(context, ref, getInstructionsNotifier()); - } - }); - return getInstructionsNotifier().stopPolling; - }, - [], - ); - - return instructions.value.when( - data: (q) => Container(), - loading: () => LoadingMessage( - message: Loc.of(context).fetchingPaymentInstructions, - ), - error: (error, _) => ErrorMessage( - message: error.toString(), - onRetry: () => - _pollForInstructions(context, ref, getInstructionsNotifier()), - ), - ); - } - - Future _pollForInstructions( - BuildContext context, - WidgetRef ref, - TbdexOrderInstructionsNotifier instructionsNotifier, - ) async { - instructions.value = const AsyncLoading(); - - try { - final fetchedInstructions = await instructionsNotifier.startPolling( - paymentState.paymentAmountState?.pfiDid ?? '', - paymentState.paymentDetailsState?.exchangeId ?? '', - ); - - if (context.mounted && fetchedInstructions != null) { - instructionsNotifier.stopPolling(); - - await onInstructionsFetched(context, fetchedInstructions); - - if (context.mounted) { - instructions.value = AsyncData(fetchedInstructions); - order.value = null; - } - } - } on Exception catch (e) { - if (context.mounted) { - instructions.value = AsyncError(e, StackTrace.current); - } - } - } -} diff --git a/lib/features/payment/payment_fetch_quote_widget.dart b/lib/features/payment/payment_fetch_quote_widget.dart deleted file mode 100644 index def6c1f..0000000 --- a/lib/features/payment/payment_fetch_quote_widget.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:didpay/features/payment/payment_review_page.dart'; -import 'package:didpay/features/payment/payment_state.dart'; -import 'package:didpay/features/tbdex/tbdex_quote_notifier.dart'; -import 'package:didpay/l10n/app_localizations.dart'; -import 'package:didpay/shared/error_message.dart'; -import 'package:didpay/shared/loading_message.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:tbdex/tbdex.dart'; - -class PaymentFetchQuoteWidget extends HookWidget { - final PaymentState paymentState; - final ValueNotifier> quote; - final ValueNotifier?> rfq; - final WidgetRef ref; - - const PaymentFetchQuoteWidget({ - required this.paymentState, - required this.quote, - required this.rfq, - required this.ref, - super.key, - }); - - @override - Widget build(BuildContext context) { - TbdexQuoteNotifier getQuoteNotifier() => ref.read(quoteProvider.notifier); - - useEffect( - () { - Future.delayed(Duration.zero, () async { - if (context.mounted) { - await _pollForQuote(context, ref, getQuoteNotifier()); - } - }); - return getQuoteNotifier().stopPolling; - }, - [], - ); - - return quote.value.when( - data: (q) => Container(), - loading: () => LoadingMessage( - message: Loc.of(context).fetchingYourQuote, - ), - error: (error, _) => ErrorMessage( - message: error.toString(), - onRetry: () => _pollForQuote(context, ref, getQuoteNotifier()), - ), - ); - } - - Future _pollForQuote( - BuildContext context, - WidgetRef ref, - TbdexQuoteNotifier quoteNotifier, - ) async { - quote.value = const AsyncLoading(); - - try { - final fetchedQuote = await quoteNotifier.startPolling( - paymentState.paymentAmountState?.pfiDid ?? '', - paymentState.paymentDetailsState?.exchangeId ?? '', - ); - - if (context.mounted && fetchedQuote != null) { - quoteNotifier.stopPolling(); - - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => PaymentReviewPage( - paymentState: paymentState.copyWith( - quote: fetchedQuote, - ), - ), - ), - ); - - if (context.mounted) { - quote.value = const AsyncData(null); - rfq.value = null; - } - } - } on Exception catch (e) { - if (context.mounted) { - quote.value = AsyncError(e, StackTrace.current); - } - } - } -} diff --git a/lib/features/payment/payment_review_page.dart b/lib/features/payment/payment_review_page.dart index 3394ae4..de9a592 100644 --- a/lib/features/payment/payment_review_page.dart +++ b/lib/features/payment/payment_review_page.dart @@ -3,9 +3,9 @@ import 'package:decimal/decimal.dart'; import 'package:didpay/features/did/did_provider.dart'; import 'package:didpay/features/payment/payment_confirmation_page.dart'; import 'package:didpay/features/payment/payment_fee_details.dart'; -import 'package:didpay/features/payment/payment_fetch_instructions_widget.dart'; import 'package:didpay/features/payment/payment_link_webview_page.dart'; import 'package:didpay/features/payment/payment_state.dart'; +import 'package:didpay/features/tbdex/tbdex_order_instructions_notifier.dart'; import 'package:didpay/features/tbdex/tbdex_service.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/l10n/app_localizations.dart'; @@ -31,9 +31,10 @@ class PaymentReviewPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final order = useState?>(null); - final orderInstructions = - useState>(const AsyncLoading()); + final orderInstructions = useState?>(null); + + final quote = paymentState.paymentDetailsState?.quote?.data; + final isAwaiting = orderInstructions.value?.isLoading ?? false; return PopScope( canPop: false, @@ -53,17 +54,12 @@ class PaymentReviewPage extends HookConsumerWidget { ); }, child: Scaffold( - appBar: AppBar(), + appBar: isAwaiting ? null : AppBar(), body: SafeArea( - child: order.value != null - ? order.value!.when( - data: (_) => PaymentFetchInstructionsWidget( - paymentState: paymentState, - instructions: orderInstructions, - order: order, - onInstructionsFetched: _handleFetchedInstructions, - ref: ref, - ), + child: orderInstructions.value != null + ? orderInstructions.value!.when( + data: (_) => + _buildPage(context, ref, quote, orderInstructions), loading: () => LoadingMessage( message: Loc.of(context).sendingYourOrder, ), @@ -73,55 +69,71 @@ class PaymentReviewPage extends HookConsumerWidget { context, ref, paymentState, - order, + orderInstructions, ), ), ) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + : _buildPage(context, ref, quote, orderInstructions), + ), + ), + ); + } + + Widget _buildPage( + BuildContext context, + WidgetRef ref, + QuoteData? quote, + ValueNotifier?> state, + ) => + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Header( + title: Loc.of(context).reviewYourPayment, + subtitle: Loc.of(context).makeSureInfoIsCorrect, + ), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Grid.side, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Header( - title: Loc.of(context).reviewYourPayment, - subtitle: Loc.of(context).makeSureInfoIsCorrect, - ), - Expanded( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Grid.side, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: Grid.sm), - _buildAmounts(context, paymentState.quote?.data), - _buildFeeDetails( - context, - paymentState.quote?.data, - ), - _buildPaymentDetails(context), - ], - ), - ), - ), + const SizedBox(height: Grid.sm), + _buildAmounts( + context, + quote, ), - NextButton( - onPressed: () => orderInstructions.value.hasValue - ? _handleFetchedInstructions( - context, - orderInstructions.value.asData!.value, - ) - : _submitOrder(context, ref, paymentState, order), - title: - '${Loc.of(context).pay} ${PaymentFeeDetails.calculateTotalAmount(paymentState.quote?.data)} ${paymentState.quote?.data.payin.currencyCode}', + _buildFeeDetails( + context, + quote, ), + _buildPaymentDetails(context), ], ), - ), - ), - ); - } + ), + ), + ), + NextButton( + onPressed: () => (state.value?.hasValue ?? false) + ? _onInstructionsFetched( + context, + state.value!.asData!.value, + ) + : _submitOrder( + context, + ref, + paymentState, + state, + ), + title: + '${Loc.of(context).pay} ${PaymentFeeDetails.calculateTotalAmount(quote)} ${quote?.payin.currencyCode}', + ), + ], + ); Widget _buildAmounts(BuildContext context, QuoteData? quote) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -244,30 +256,59 @@ class PaymentReviewPage extends HookConsumerWidget { BuildContext context, WidgetRef ref, PaymentState paymentState, - ValueNotifier?> state, + ValueNotifier?> state, ) async { state.value = const AsyncLoading(); try { - final order = await ref.read(tbdexServiceProvider).sendOrder( + await ref.read(tbdexServiceProvider).sendOrder( ref.read(didProvider), paymentState.paymentAmountState?.pfiDid ?? '', paymentState.paymentDetailsState?.exchangeId ?? '', ); if (context.mounted) { - state.value = AsyncData(order); + await _pollForInstructions(context, ref, state); } } on Exception catch (e) { state.value = AsyncError(e, StackTrace.current); } } - Future _handleFetchedInstructions( + Future _pollForInstructions( + BuildContext context, + WidgetRef ref, + ValueNotifier?> state, + ) async { + final instructionsNotifier = ref.read(orderInstructionsProvider.notifier); + + try { + final fetchedInstructions = await instructionsNotifier.startPolling( + paymentState.paymentAmountState?.pfiDid ?? '', + paymentState.paymentDetailsState?.exchangeId ?? '', + ); + + if (context.mounted) { + state.value = AsyncData(fetchedInstructions); + instructionsNotifier.stopPolling(); + + if (fetchedInstructions != null && state.value != null) { + await _onInstructionsFetched(context, fetchedInstructions); + } + } + } on Exception catch (e) { + if (context.mounted) { + instructionsNotifier.stopPolling(); + state.value = AsyncError(e, StackTrace.current); + } + } + } + + Future _onInstructionsFetched( BuildContext context, - OrderInstructions? fetchedInstructions, + OrderInstructions? instructions, ) async { - final paymentLink = fetchedInstructions?.data.payin.link; + final paymentLink = instructions?.data.payin.link; if (paymentLink == null) { await Navigator.of(context).pushAndRemoveUntil( diff --git a/lib/features/payment/payment_state.dart b/lib/features/payment/payment_state.dart index 77de6a2..970a62c 100644 --- a/lib/features/payment/payment_state.dart +++ b/lib/features/payment/payment_state.dart @@ -1,19 +1,16 @@ import 'package:didpay/features/payment/payment_amount_state.dart'; import 'package:didpay/features/payment/payment_details_state.dart'; import 'package:didpay/features/transaction/transaction.dart'; -import 'package:tbdex/tbdex.dart'; class PaymentState { final TransactionType transactionType; final PaymentAmountState? paymentAmountState; final PaymentDetailsState? paymentDetailsState; - final Quote? quote; const PaymentState({ required this.transactionType, this.paymentAmountState, this.paymentDetailsState, - this.quote, }); String? get filterPayinCurrency { @@ -92,13 +89,11 @@ class PaymentState { PaymentState copyWith({ PaymentAmountState? paymentAmountState, PaymentDetailsState? paymentDetailsState, - Quote? quote, }) { return PaymentState( transactionType: transactionType, paymentAmountState: paymentAmountState ?? this.paymentAmountState, paymentDetailsState: paymentDetailsState ?? this.paymentDetailsState, - quote: quote ?? this.quote, ); } } diff --git a/lib/features/tbdex/tbdex_service.dart b/lib/features/tbdex/tbdex_service.dart index 4d836ac..d401d51 100644 --- a/lib/features/tbdex/tbdex_service.dart +++ b/lib/features/tbdex/tbdex_service.dart @@ -67,8 +67,6 @@ class TbdexService { ), ); - await Future.delayed(const Duration(milliseconds: 300)); - return filteredOfferingsMap; } @@ -203,8 +201,6 @@ class TbdexService { rethrow; } - await Future.delayed(const Duration(milliseconds: 300)); - return rfq; } @@ -222,8 +218,6 @@ class TbdexService { rethrow; } - await Future.delayed(const Duration(milliseconds: 300)); - return order; } diff --git a/test/features/payment/payment_review_page_test.dart b/test/features/payment/payment_review_page_test.dart index 4a5cc37..23427cd 100644 --- a/test/features/payment/payment_review_page_test.dart +++ b/test/features/payment/payment_review_page_test.dart @@ -61,8 +61,8 @@ void main() async { selectedPaymentMethod: PaymentMethod.fromPayinMethod( offering.data.payin.methods.first, ), + quote: quote, ), - quote: quote, ), ), overrides: [