Skip to content

Commit

Permalink
feat: TbdexHttpClient improvements (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethan-tbd authored May 10, 2024
1 parent 66cacf5 commit a5fa32f
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 99 deletions.
53 changes: 24 additions & 29 deletions lib/features/payment/payment_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import 'package:didpay/features/payment/payment_state.dart';
import 'package:didpay/features/payment/review_payment_page.dart';
import 'package:didpay/features/tbdex/rfq_state.dart';
import 'package:didpay/features/tbdex/tbdex.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/features/tbdex/tbdex_providers.dart';
import 'package:didpay/shared/json_schema_form.dart';
import 'package:didpay/shared/theme/grid.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';

Expand All @@ -27,44 +27,39 @@ class PaymentDetails {
final fee = paymentMethod.serviceFee;
final paymentName = paymentMethod.paymentName;

return isDisabled
? _buildDisabledButton(context)
: Expanded(
child: JsonSchemaForm(
schema: schema,
onSubmit: (formData) =>
// TODO(mistermoe): check requiredClaims and navigate to kcc flow if needed, https://github.com/TBD54566975/didpay/issues/122
Navigator.of(context).push(
final rfq = Tbdex.createRfq(ref, rfqState);
final isLoading = useState(false);

return Expanded(
child: JsonSchemaForm(
schema: schema,
isDisabled: isDisabled,
isLoading: isLoading.value,
onSubmit: (formData) {
// TODO(mistermoe): check requiredClaims and navigate to kcc flow if needed, https://github.com/TBD54566975/didpay/issues/122
isLoading.value = true;
ref.read(rfqProvider(rfq).future).then(
(_) async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ReviewPaymentPage(
rfq: Tbdex.createRfq(ref, rfqState),
exchangeId: rfq.metadata.id,
paymentState: paymentState.copyWith(
serviceFee: fee,
paymentName: paymentName,
formData: formData,
),
),
),
),
),
);
isLoading.value = false;
},
onError: (_) => isLoading.value = false,
);
},
),
);
}

static Widget _buildDisabledButton(BuildContext context) => Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: Container()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
child: FilledButton(
onPressed: null,
child: Text(Loc.of(context).next),
),
),
],
),
);
}

extension _PaymentMethodOperations on Object? {
Expand Down
64 changes: 43 additions & 21 deletions lib/features/payment/review_payment_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,26 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tbdex/tbdex.dart';

class ReviewPaymentPage extends HookConsumerWidget {
final Rfq rfq;
final String exchangeId;
final PaymentState paymentState;

const ReviewPaymentPage({
required this.rfq,
required this.exchangeId,
required this.paymentState,
super.key,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final quoteStatus = ref.watch(quoteProvider);
QuoteAsyncNotifier getQuoteNotifier() => ref.read(quoteProvider.notifier);
final quoteResult = ref.watch(quoteProvider);

useEffect(
() {
Future.delayed(Duration.zero, () {
ref.read(rfqProvider(rfq));
getQuoteNotifier().startPolling(rfq.metadata.id);
});
Future.delayed(
Duration.zero,
() => getQuoteNotifier().startPolling(exchangeId),
);
return getQuoteNotifier().stopPolling;
},
[],
Expand All @@ -44,12 +44,16 @@ class ReviewPaymentPage extends HookConsumerWidget {
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
child: quoteResult.when(
child: quoteStatus.when(
data: (quote) => quote != null
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeader(context),
_buildHeader(
context,
Loc.of(context).reviewYourPayment,
Loc.of(context).makeSureInfoIsCorrect,
),
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
Expand All @@ -64,17 +68,13 @@ class ReviewPaymentPage extends HookConsumerWidget {
),
),
),
_buildSubmitButton(context, quote.data),
_buildSubmitButton(context, ref, quote),
],
)
: _loading(),
loading: _loading,
error: (error, stackTrace) => Center(
child: Text(
'Failed to get quote: $error',
style: Theme.of(context).textTheme.displaySmall,
),
),
error: (error, stackTrace) =>
_buildErrorWidget(context, ref, quoteStatus.error.toString()),
),
),
),
Expand All @@ -83,14 +83,36 @@ class ReviewPaymentPage extends HookConsumerWidget {

Widget _loading() => const Center(child: CircularProgressIndicator());

Widget _buildHeader(BuildContext context) => Padding(
Widget _buildErrorWidget(
BuildContext context,
WidgetRef ref,
String errorMessage,
) {
return Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeader(context, Loc.of(context).errorFound, errorMessage),
Expanded(child: Container()),
FilledButton(
onPressed: () =>
ref.read(quoteProvider.notifier).startPolling(exchangeId),
child: Text(Loc.of(context).tapToRetry),
),
],
),
);
}

Widget _buildHeader(BuildContext context, String title, String subtitle) =>
Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.xs),
child: Column(
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
Loc.of(context).reviewYourPayment,
title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
Expand All @@ -100,7 +122,7 @@ class ReviewPaymentPage extends HookConsumerWidget {
Align(
alignment: Alignment.topLeft,
child: Text(
Loc.of(context).makeSureInfoIsCorrect,
subtitle,
style: Theme.of(context).textTheme.bodyMedium,
),
),
Expand Down Expand Up @@ -211,7 +233,7 @@ class ReviewPaymentPage extends HookConsumerWidget {
),
);

Widget _buildSubmitButton(BuildContext context, QuoteData quote) =>
Widget _buildSubmitButton(BuildContext context, WidgetRef ref, Quote quote) =>
FilledButton(
onPressed: () {
Navigator.of(context).push(
Expand All @@ -221,7 +243,7 @@ class ReviewPaymentPage extends HookConsumerWidget {
);
},
child: Text(
'${Loc.of(context).pay} ${FeeDetails.calculateTotalAmount(quote)} ${quote.payin.currencyCode}',
'${Loc.of(context).pay} ${FeeDetails.calculateTotalAmount(quote.data)} ${quote.data.payin.currencyCode}',
),
);
}
12 changes: 9 additions & 3 deletions lib/features/tbdex/quote_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'dart:math';

import 'package:didpay/config/config.dart';
import 'package:didpay/features/account/account_providers.dart';
import 'package:didpay/features/tbdex/tbdex_exceptions.dart';
import 'package:didpay/shared/http_status.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tbdex/tbdex.dart';

Expand Down Expand Up @@ -50,7 +52,7 @@ class QuoteAsyncNotifier extends AutoDisposeAsyncNotifier<Quote?> {
}
} on Exception catch (e) {
state = AsyncValue.error(
Exception('Failed to fetch exchange: $e'),
e,
StackTrace.current,
);
stopPolling();
Expand Down Expand Up @@ -79,13 +81,17 @@ class QuoteAsyncNotifier extends AutoDisposeAsyncNotifier<Quote?> {
final country = ref.read(countryProvider);
final pfi = Config.getPfi(country);

final exchange = await TbdexHttpClient.getExchange(
final response = await TbdexHttpClient.getExchange(
did,
pfi?.didUri ?? '',
exchangeId,
);

return exchange;
if (response.statusCode.category != HttpStatus.success) {
throw QuoteException('failed to retrieve quote', response.statusCode);
}

return response.data ?? [];
}

bool _containsQuote(Exchange exchange) =>
Expand Down
26 changes: 26 additions & 0 deletions lib/features/tbdex/tbdex_exceptions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:didpay/shared/http_status.dart';

// TODO(ethan-tbd): finish working on custom exceptions
class TbdexException implements Exception {
final String message;
final int errorCode;

TbdexException(this.message, this.errorCode);

@override
String toString() {
return '${errorCode.category}: $errorCode, $message';
}
}

class RfqException extends TbdexException {
RfqException(super.message, super.errorCode);
}

class OfferingException extends TbdexException {
OfferingException(super.message, super.errorCode);
}

class QuoteException extends TbdexException {
QuoteException(super.message, super.errorCode);
}
34 changes: 20 additions & 14 deletions lib/features/tbdex/tbdex_providers.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
import 'package:didpay/config/config.dart';
import 'package:didpay/features/account/account_providers.dart';
import 'package:didpay/features/tbdex/quote_notifier.dart';
import 'package:didpay/features/tbdex/tbdex_exceptions.dart';
import 'package:didpay/shared/http_status.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tbdex/tbdex.dart';

final offeringsProvider =
FutureProvider.autoDispose<List<Offering>>((ref) async {
try {
final country = ref.read(countryProvider);
final pfi = Config.getPfi(country);
final offerings = await TbdexHttpClient.listOfferings(pfi?.didUri ?? '');
return offerings;
} on Exception catch (e) {
throw Exception('Failed to load offerings: $e');
}
final country = ref.read(countryProvider);
final pfi = Config.getPfi(country);

final response = await TbdexHttpClient.listOfferings(pfi?.didUri ?? '');

return response.statusCode.category == HttpStatus.success
? response.data!
: throw OfferingException(
'failed to fetch offerings',
response.statusCode,
);
});

final rfqProvider =
FutureProvider.family.autoDispose<void, Rfq>((ref, rfq) async {
try {
final did = ref.read(didProvider);
await rfq.sign(did);
final did = ref.read(didProvider);
await rfq.sign(did);

final response =
await TbdexHttpClient.createExchange(rfq, replyTo: rfq.metadata.from);

await TbdexHttpClient.createExchange(rfq, replyTo: rfq.metadata.from);
} on Exception catch (e) {
throw Exception('Failed to send rfq: $e');
if (response.statusCode.category != HttpStatus.success) {
throw RfqException('failed to send rfq', response.statusCode);
}
});

Expand Down
4 changes: 3 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,7 @@
"totalToAccount": "Total to account",
"totalToYou": "Total to you",
"totalToRecipient": "Total to recipient",
"pay": "Pay"
"pay": "Pay",
"errorFound": "Error found",
"tapToRetry": "Tap to retry"
}
12 changes: 12 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,18 @@ abstract class Loc {
/// In en, this message translates to:
/// **'Pay'**
String get pay;

/// No description provided for @errorFound.
///
/// In en, this message translates to:
/// **'Error found'**
String get errorFound;

/// No description provided for @tapToRetry.
///
/// In en, this message translates to:
/// **'Tap to retry'**
String get tapToRetry;
}

class _LocDelegate extends LocalizationsDelegate<Loc> {
Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,10 @@ class LocEn extends Loc {

@override
String get pay => 'Pay';

@override
String get errorFound => 'Error found';

@override
String get tapToRetry => 'Tap to retry';
}
Loading

0 comments on commit a5fa32f

Please sign in to comment.