Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vc creation and verification flow #8

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions frontend/lib/features/account/account_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/account/account_did_page.dart';
import 'package:flutter_starter/features/account/account_vc_page.dart';

class AccountPage extends StatelessWidget {
const AccountPage({super.key});
Expand All @@ -11,15 +12,27 @@ class AccountPage extends StatelessWidget {
body: ListView(
children: [
ListTile(
title: const Text('My DID'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AccountDidPage(),
),
);
}),
title: const Text('My DID'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AccountDidPage(),
),
);
},
),
ListTile(
title: const Text('My verifiable credential'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AccountVCPage(),
),
);
},
),
],
),
);
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/features/account/account_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:web5_flutter/web5_flutter.dart';

final didProvider = Provider<Did>((ref) => throw UnimplementedError());
final vcProvider = StateProvider<String?>((ref) => null);
20 changes: 20 additions & 0 deletions frontend/lib/features/account/account_vc_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/account/account_providers.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

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

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

return Scaffold(
appBar: AppBar(title: const Text('My VC')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Center(child: SelectableText(vc ?? '')),
),
);
}
}
6 changes: 4 additions & 2 deletions frontend/lib/features/app/app.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/app/app_tabs.dart';
import 'package:flutter_starter/features/onboarding/onboarding_welcome_page.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:flutter_starter/shared/theme/theme.dart';

class App extends StatelessWidget {
const App({super.key});
final bool onboarding;
const App({required this.onboarding, super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'DIDPay',
theme: lightTheme(context),
darkTheme: darkTheme(context),
home: const AppTabs(),
home: onboarding ? const OnboardingWelcomePage() : const AppTabs(),
localizationsDelegates: Loc.localizationsDelegates,
supportedLocales: const [
Locale('en', ''),
Expand Down
50 changes: 22 additions & 28 deletions frontend/lib/features/home/home_page.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/pfis/pfi_providers.dart';
import 'package:flutter_starter/features/pfis/pfi_verification_page.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:web5_flutter/web5_flutter.dart';

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

@override
Widget build(BuildContext context, WidgetRef ref) {
final pfis = ref.watch(pfisProvider);
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: ListView(
appBar: AppBar(title: Text(Loc.of(context).home)),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...pfis.map(
(pfi) => ListTile(
title: Text(pfi.name),
subtitle: Text(pfi.didUri),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final result = await DidDht.resolve(pfi.didUri);
final widgetService = result.didDocument?.service
?.firstWhere((e) => e.type == 'kyc-widget');
if (widgetService?.serviceEndpoint != null) {
// ignore: use_build_context_synchronously
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => PfiVerificationPage(
widgetUri: widgetService!.serviceEndpoint,
),
),
);
}
},
),
)
const SizedBox(height: 40),
Text(
'Balance: \$0.00 USD',
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(
onPressed: () {}, child: Text(Loc.of(context).deposit)),
FilledButton(onPressed: () {}, child: Text(Loc.of(context).send)),
FilledButton(
onPressed: () {}, child: Text(Loc.of(context).withdraw)),
],
),
const SizedBox(height: 40),
],
),
);
Expand Down
50 changes: 50 additions & 0 deletions frontend/lib/features/onboarding/onboarding_welcome_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/features/pfis/pfis_page.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';

class OnboardingWelcomePage extends StatelessWidget {
const OnboardingWelcomePage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 40),
Text(
Loc.of(context).welcomeToDIDPay,
style: Theme.of(context).textTheme.displayMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 80),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Text(
Loc.of(context).toSendMoney,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FilledButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const PfisPage(),
fullscreenDialog: true,
),
);
},
child: Text(Loc.of(context).getStarted),
),
),
],
),
),
);
}
}
120 changes: 120 additions & 0 deletions frontend/lib/features/pfis/pfi_confirmation_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:flutter_starter/features/account/account_providers.dart';
import 'package:flutter_starter/services/service_providers.dart';
import 'package:flutter_starter/shared/constants.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_starter/features/app/app_tabs.dart';
import 'package:flutter_starter/features/pfis/pfi.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:web5_flutter/web5_flutter.dart';

class PfiConfirmationPage extends HookConsumerWidget {
final Pfi pfi;
final String transactionId;

const PfiConfirmationPage({
required this.pfi,
required this.transactionId,
super.key,
});

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

useEffect(() {
verifyCredential(ref);
return null;
}, []);

return Scaffold(
body: SafeArea(
child: vcJwt == null ? verifying(context) : verified(context, vcJwt),
),
);
}

Widget verifying(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 40),
Text(
'Verifying your credentials...',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
const Center(child: CircularProgressIndicator())
],
);
}

Widget verified(BuildContext context, String vcJwt) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 40),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Your credentials have been verified!',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
Icon(Icons.check_circle,
size: 80, color: Theme.of(context).colorScheme.primary),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FilledButton(
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const AppTabs()),
(Route<dynamic> route) => false,
);
},
child: Text(Loc.of(context).done),
),
),
],
);
}

Future<void> verifyCredential(WidgetRef ref) async {
final result = await DidDht.resolve(pfi.didUri);
final pfiService =
result.didDocument?.service?.firstWhereOrNull((e) => e.type == 'PFI');

if (pfiService == null) {
// Add real error handling here...
throw Exception('PFI service endpoint not found');
}

var uri = Uri.parse(
'${pfiService.serviceEndpoint}/credential?transaction_id=$transactionId');
final response = await http.get(uri);
if (response.statusCode != 200) {
// Add real error handling here...
throw Exception('Failed to get credential');
}

final jsonResponse = json.decode(response.body);
ref.read(secureStorageProvider).write(
key: Constants.verifiableCredentialKey, value: jsonResponse['jwt']);
ref.read(vcProvider.notifier).state = jsonResponse['jwt'];
}
}
2 changes: 1 addition & 1 deletion frontend/lib/features/pfis/pfi_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ final pfisProvider = Provider<List<Pfi>>(
(ref) => [
Pfi(
id: 'prototype',
name: 'Prototype PFI',
name: 'Prototype',
didUri: 'did:dht:3x1hbjobt577amnoeoxcenqrbjicym5mgsx6c6zszisf1igfj51y',
),
Pfi(
Expand Down
Loading