Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
feat: introduce kcc into rfq flow (#180)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanwlee authored May 31, 2024
1 parent f8e4cb0 commit e7468b5
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 76 deletions.
2 changes: 1 addition & 1 deletion lib/features/account/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class AccountPage extends HookWidget {
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AccountVCPage(),
builder: (context) => const AccountVcPage(),
),
);
},
Expand Down
1 change: 0 additions & 1 deletion lib/features/account/account_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:web5/web5.dart';

final didProvider = Provider<BearerDid>((ref) => throw UnimplementedError());
final vcProvider = StateProvider<String?>((ref) => null);
58 changes: 49 additions & 9 deletions lib/features/account/account_vc_page.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
import 'package:didpay/features/account/account_providers.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/features/vcs/vcs_notifier.dart';
import 'package:didpay/shared/theme/grid.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class AccountVCPage extends HookConsumerWidget {
const AccountVCPage({super.key});
class AccountVcPage extends HookConsumerWidget {
const AccountVcPage({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final vc = ref.watch(vcProvider);
final vcs = ref.watch(vcsProvider);

return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
child: Center(
child: SelectableText(vc ?? Loc.of(context).vcNotFound),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Grid.side,
vertical: Grid.xs,
),
child: Text(
'My VCs',
style: Theme.of(context).textTheme.bodyLarge,
),
),
Expanded(
child: ListView.builder(
itemCount: vcs.length,
itemBuilder: (context, index) =>
_buildVcTile(context, ref, vcs[index]),
),
),
],
),
),
);
}

Widget _buildVcTile(BuildContext context, WidgetRef ref, String vc) {
return ListTile(
title: Text(
vc,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
leading: Container(
width: Grid.md,
height: Grid.md,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(Grid.xxs),
),
child: const Center(
// TODO(ethan-tbd): replace with PFI icon
child: Icon(Icons.abc),
),
),
);
Expand Down
5 changes: 1 addition & 4 deletions lib/features/app/app.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
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/theme/theme.dart';
import 'package:flutter/material.dart';
Expand All @@ -11,12 +9,11 @@ class App extends HookConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final pfis = ref.watch(pfisProvider);
return MaterialApp(
title: 'DIDPay',
theme: lightTheme(context),
darkTheme: darkTheme(context),
home: pfis.isEmpty ? AddPfiPage() : const AppTabs(),
home: const AppTabs(),
localizationsDelegates: Loc.localizationsDelegates,
supportedLocales: const [
Locale('en', ''),
Expand Down
89 changes: 49 additions & 40 deletions lib/features/kcc/kcc_retrieval_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import 'package:didpay/features/account/account_providers.dart';
import 'package:didpay/features/kcc/kcc_issuance_service.dart';
import 'package:didpay/features/kcc/lib.dart';
import 'package:didpay/features/pfis/pfi.dart';
import 'package:didpay/features/vcs/vcs_notifier.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/async/async_error_widget.dart';
import 'package:didpay/shared/async/async_loading_widget.dart';
import 'package:didpay/shared/theme/grid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Expand All @@ -23,70 +26,76 @@ class KccRetrievalPage extends HookConsumerWidget {
final bearerDid = ref.watch(didProvider);
final kccIssuanceService = ref.watch(kccIssuanceProvider);
final credentialResponse =
useState<AsyncValue<CredentialResponse>>(const AsyncLoading());
useState<AsyncValue<String>>(const AsyncLoading());

useEffect(
() {
Future.microtask(() async {
try {
final value = await kccIssuanceService.pollForCredential(
final response = await kccIssuanceService.pollForCredential(
pfi,
idvRequest,
bearerDid,
);

// TODO(mistermoe): save credential to shared preferences
credentialResponse.value = AsyncData(value);
await ref.read(vcsProvider.notifier).add(response).then(
(credential) =>
credentialResponse.value = AsyncData(credential),
);
} on Exception catch (e, stackTrace) {
credentialResponse.value = AsyncError(e, stackTrace);
}
});

return;
return null;
},
[],
);

return Scaffold(
body: SafeArea(
child: credentialResponse.value.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text('Error - $error')),
data: (data) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: Grid.xl),
Text(
'Your KCC has been issued and stored on your device!',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: Grid.xs),
Icon(
Icons.check_circle,
size: Grid.xl,
color: Theme.of(context).colorScheme.primary,
),
],
),
loading: () => const AsyncLoadingWidget(text: 'Loading...'),
error: (error, stackTrace) => AsyncErrorWidget(
text: error.toString(),
onRetry: () => Navigator.of(context).pop(),
),
data: (data) => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: Grid.xl),
Text(
'Your VC has been created',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: Grid.xs),
Icon(
Icons.check_circle,
size: Grid.xl,
color: Theme.of(context).colorScheme.primary,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
child: ElevatedButton(
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
child: Text(Loc.of(context).done),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
child: FilledButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context, rootNavigator: true)
.pop(credentialResponse.value.asData?.value);
}
},
child: Text(Loc.of(context).next),
),
],
);
},
),
],
),
),
),
);
Expand Down
9 changes: 7 additions & 2 deletions lib/features/kcc/kcc_webview_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:didpay/features/kcc/kcc_issuance_service.dart';
import 'package:didpay/features/kcc/kcc_retrieval_page.dart';
import 'package:didpay/features/kcc/lib/idv_request.dart';
import 'package:didpay/features/pfis/pfi.dart';
import 'package:didpay/shared/async/async_error_widget.dart';
import 'package:didpay/shared/async/async_loading_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Expand Down Expand Up @@ -55,8 +57,11 @@ class KccWebviewPage extends HookConsumerWidget {
appBar: AppBar(title: const Text('PFI Verification')),
body: SafeArea(
child: idvRequest.value.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text('Error - $error')),
loading: () => const AsyncLoadingWidget(text: 'Loading...'),
error: (error, stackTrace) => AsyncErrorWidget(
text: error.toString(),
onRetry: () => Navigator.of(context).pop(),
),
data: (data) {
return InAppWebView(
initialSettings: settings,
Expand Down
82 changes: 73 additions & 9 deletions lib/features/payment/payment_details_page.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import 'package:collection/collection.dart';
import 'package:didpay/features/account/account_providers.dart';
import 'package:didpay/features/kcc/kcc_consent_page.dart';
import 'package:didpay/features/payment/payment_method_operations.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/tbdex/tbdex_service.dart';
import 'package:didpay/features/transaction/transaction.dart';
import 'package:didpay/features/vcs/vcs_notifier.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/async/async_error_widget.dart';
import 'package:didpay/shared/async/async_loading_widget.dart';
import 'package:didpay/shared/json_schema_form.dart';
import 'package:didpay/shared/modal_flow.dart';
import 'package:didpay/shared/theme/grid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Expand All @@ -29,6 +32,7 @@ class PaymentDetailsPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final selectedPaymentMethod = useState<Object?>(null);
final selectedPaymentType = useState<String?>(null);
final offeringCredentials = useState<List<String>?>(null);
final sendRfqState = useState<AsyncValue<Rfq>?>(null);

final paymentMethods = _getPaymentMethods(paymentState);
Expand Down Expand Up @@ -63,8 +67,13 @@ class PaymentDetailsPage extends HookConsumerWidget {
),
error: (error, _) => AsyncErrorWidget(
text: error.toString(),
onRetry: () =>
_sendRfq(context, ref, paymentState, sendRfqState),
onRetry: () => _sendRfq(
context,
ref,
paymentState,
sendRfqState,
claims: offeringCredentials.value,
),
),
)
: Column(
Expand Down Expand Up @@ -95,8 +104,25 @@ class PaymentDetailsPage extends HookConsumerWidget {
? selectedPaymentMethod.value as PayoutMethod?
: paymentState.payoutMethods?.firstOrNull,
),
onPaymentFormSubmit: (paymentState) =>
_sendRfq(context, ref, paymentState, sendRfqState),
onPaymentFormSubmit: (paymentState) async {
await _getVc(
context,
ref,
paymentState,
offeringCredentials,
).then((value) {
if (offeringCredentials.value != null &&
offeringCredentials.value!.isNotEmpty) {
_sendRfq(
context,
ref,
paymentState,
sendRfqState,
claims: offeringCredentials.value,
);
}
});
},
),
],
),
Expand Down Expand Up @@ -284,20 +310,26 @@ class PaymentDetailsPage extends HookConsumerWidget {
BuildContext context,
WidgetRef ref,
PaymentState paymentState,
ValueNotifier<AsyncValue<Rfq>?> state,
) {
ValueNotifier<AsyncValue<Rfq>?> state, {
List<String>? claims,
}) {
state.value = const AsyncLoading();
ref
.read(tbdexServiceProvider)
.sendRfq(ref.read(didProvider), paymentState)
.sendRfq(
ref.read(didProvider),
paymentState.copyWith(claims: claims),
)
.then((rfq) async {
state.value = AsyncData(rfq);
await Navigator.of(context)
.push(
MaterialPageRoute(
builder: (context) => PaymentReviewPage(
paymentState:
paymentState.copyWith(exchangeId: rfq.metadata.id),
paymentState: paymentState.copyWith(
exchangeId: rfq.metadata.id,
claims: claims,
),
),
),
)
Expand All @@ -307,4 +339,36 @@ class PaymentDetailsPage extends HookConsumerWidget {
throw error;
});
}

Future<void> _getVc(
BuildContext context,
WidgetRef ref,
PaymentState paymentState,
ValueNotifier<List<String>?> offeringCredentials,
) async {
final presentationDefinition =
paymentState.selectedOffering?.data.requiredClaims;
final credentials =
presentationDefinition?.selectCredentials(ref.read(vcsProvider));

if (credentials != null && credentials.isNotEmpty) {
offeringCredentials.value = credentials;
} else if (presentationDefinition != null) {
final issuedCredential = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ModalFlow(
initialWidget: KccConsentPage(
pfi: paymentState.selectedPfi!,
),
),
),
);

issuedCredential == null
? offeringCredentials.value = null
: offeringCredentials.value = [issuedCredential as String];
} else {
offeringCredentials.value = null;
}
}
}
Loading

0 comments on commit e7468b5

Please sign in to comment.