Skip to content

Commit

Permalink
feat: add VcsAddPage (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethan-tbd authored Oct 25, 2024
1 parent 459c1f1 commit 09f3b3e
Show file tree
Hide file tree
Showing 16 changed files with 349 additions and 32 deletions.
34 changes: 20 additions & 14 deletions lib/features/account/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:didpay/features/pfis/pfi.dart';
import 'package:didpay/features/pfis/pfis_add_page.dart';
import 'package:didpay/features/pfis/pfis_notifier.dart';
import 'package:didpay/features/qr/qr_tabs.dart';
import 'package:didpay/features/vcs/vcs_add_page.dart';
import 'package:didpay/features/vcs/vcs_notifier.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/modal/modal_manage_item.dart';
Expand Down Expand Up @@ -200,17 +201,17 @@ class AccountPage extends HookConsumerWidget {
),
),
),
credentials.isEmpty
? TileContainer(child: _buildNoCredentialsTile(context))
: ListView.builder(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: credentials.length,
itemBuilder: (context, index) => TileContainer(
ListView.builder(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: credentials.length + 1,
itemBuilder: (context, index) => index < credentials.length
? TileContainer(
child:
_buildCredentialTile(context, ref, credentials[index]),
),
),
)
: TileContainer(child: _buildAddCredentialTile(context)),
),
],
);

Expand Down Expand Up @@ -242,9 +243,9 @@ class AccountPage extends HookConsumerWidget {
),
);

Widget _buildNoCredentialsTile(BuildContext context) => ListTile(
Widget _buildAddCredentialTile(BuildContext context) => ListTile(
title: Text(
Loc.of(context).noCredentialsIssuedYet,
Loc.of(context).addACredential,
style: Theme.of(context).textTheme.titleSmall,
),
leading: Container(
Expand All @@ -254,11 +255,16 @@ class AccountPage extends HookConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(Grid.xxs),
),
child: Center(
child:
Icon(Icons.error, color: Theme.of(context).colorScheme.outline),
child: const Center(
child: Icon(Icons.add),
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const VcsAddPage()),
);
},
);

Widget _buildFeatureFlagsList(
Expand Down
3 changes: 1 addition & 2 deletions lib/features/device/device_info_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class DeviceInfoService {

Future<bool> isPhysicalDevice() async {
if (kIsWeb) {
// On web, return false as there is no physical device
return false;
}
if (Platform.isIOS) {
Expand All @@ -20,7 +19,7 @@ class DeviceInfoService {
if (Platform.isAndroid) {
return (await _deviceInfo.androidInfo).isPhysicalDevice;
}
// Default return for unsupported platforms

return false;
}
}
4 changes: 2 additions & 2 deletions lib/features/did/did_form.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:didpay/features/qr/qr_tile.dart';
import 'package:didpay/features/did/did_qr_tile.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/next_button.dart';
import 'package:didpay/shared/theme/grid.dart';
Expand Down Expand Up @@ -49,7 +49,7 @@ class DidForm extends HookConsumerWidget {
),
),
),
QrTile(didTextController: textController),
DidQrTile(didTextController: textController),
NextButton(
onPressed: () async => onSubmit(textController.text),
title: buttonTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:web5/web5.dart';

class QrTile extends HookConsumerWidget {
class DidQrTile extends HookConsumerWidget {
final TextEditingController didTextController;

const QrTile({
const DidQrTile({
required this.didTextController,
super.key,
});
Expand Down
71 changes: 71 additions & 0 deletions lib/features/vcs/vcs_add_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:didpay/features/vcs/vcs_form.dart';
import 'package:didpay/features/vcs/vcs_notifier.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/confirmation_message.dart';
import 'package:didpay/shared/error_message.dart';
import 'package:didpay/shared/header.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';

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

@override
Widget build(BuildContext context, WidgetRef ref) {
final vc = useState<AsyncValue<String>?>(null);

return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: vc.value != null
? vc.value!.when(
data: (_) => ConfirmationMessage(
message: Loc.of(context).credentialAdded,
),
loading: () =>
LoadingMessage(message: Loc.of(context).addingCredential),
error: (error, _) => ErrorMessage(
message: error.toString(),
onRetry: () => vc.value = null,
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Header(
title: Loc.of(context).addACredential,
subtitle: Loc.of(context).enterACredentialJwt,
),
Expanded(
child: VcsForm(
buttonTitle: Loc.of(context).add,
onSubmit: (vcJwt) async =>
_addCredential(context, ref, vcJwt, vc),
),
),
],
),
),
);
}

Future<void> _addCredential(
BuildContext context,
WidgetRef ref,
String vcJwt,
ValueNotifier<AsyncValue<String>?> state,
) async {
state.value = const AsyncLoading();
try {
final credential = await ref.read(vcsProvider.notifier).addJwt(vcJwt);

if (context.mounted) {
state.value = AsyncData(credential);
}
} on Exception catch (e) {
state.value = AsyncError(e, StackTrace.current);
}
}
}
61 changes: 61 additions & 0 deletions lib/features/vcs/vcs_form.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:didpay/features/vcs/vcs_qr_tile.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/next_button.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';

class VcsForm extends HookConsumerWidget {
final String buttonTitle;
final Future<void> Function(String) onSubmit;

VcsForm({required this.buttonTitle, required this.onSubmit, super.key});

final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context, WidgetRef ref) {
final focusNode = useFocusNode();

final textController = useTextEditingController();

return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
focusNode: focusNode,
controller: textController,
onTapOutside: (_) => focusNode.unfocus(),
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: Loc.of(context).credentialHint,
),
validator: (value) => value == null || value.isEmpty
? Loc.of(context).thisFieldCannotBeEmpty
: null,
),
],
),
),
),
VcsQrTile(credentialTextController: textController),
NextButton(
onPressed: () async => onSubmit(textController.text),
title: buttonTitle,
),
],
),
);
}
}
21 changes: 18 additions & 3 deletions lib/features/vcs/vcs_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:didpay/features/kcc/lib.dart';
import 'package:didpay/features/vcs/vcs_service.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

Expand All @@ -9,13 +10,27 @@ final vcsProvider = StateNotifierProvider<VcsNotifier, List<String>>(
class VcsNotifier extends StateNotifier<List<String>> {
static const String storageKey = 'vcs';
final Box box;
final VcsService vcsService;

VcsNotifier._(this.box, List<String> state) : super(state);
VcsNotifier._(this.box, this.vcsService, List<String> state) : super(state);

static Future<VcsNotifier> create(Box box) async {
static Future<VcsNotifier> create(Box box, VcsService vcsService) async {
final List<String> vcs = await box.get(storageKey) ?? [];

return VcsNotifier._(box, vcs);
return VcsNotifier._(box, vcsService, vcs);
}

Future<String> addJwt(String vcJwt) async {
final parsedJwt = vcsService.parseCredential(vcJwt);

if (state.any((elem) => elem == vcJwt)) {
return parsedJwt;
}

state = [...state, parsedJwt];

await _save();
return parsedJwt;
}

Future<String> add(CredentialResponse response) async {
Expand Down
81 changes: 81 additions & 0 deletions lib/features/vcs/vcs_qr_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:didpay/features/device/device_info_service.dart';
import 'package:didpay/features/qr/qr_scan_page.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/snackbar/snackbar_service.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';

class VcsQrTile extends HookConsumerWidget {
final TextEditingController credentialTextController;

const VcsQrTile({
required this.credentialTextController,
super.key,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final isPhysicalDevice = useState(true);
final snackbarService = SnackbarService();

useEffect(
() {
Future.delayed(
Duration.zero,
() async => isPhysicalDevice.value =
await ref.read(deviceInfoServiceProvider).isPhysicalDevice(),
);
return null;
},
[],
);

return Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.xxs),
child: ListTile(
leading: const Icon(Icons.qr_code),
title: Text(
Loc.of(context).scanACredentialJwt,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => isPhysicalDevice.value
? _scanQrCode(
context,
credentialTextController,
)
: _simulateScanQrCode(
context,
credentialTextController,
snackbarService,
),
),
);
}

Future<void> _scanQrCode(
BuildContext context,
TextEditingController credentialTextController,
) async {
final qrValue = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (context) => const QrScanPage(),
),
);

credentialTextController.text = qrValue ?? '';
}

Future<void> _simulateScanQrCode(
BuildContext context,
TextEditingController credentialTextController,
SnackbarService snackbarService,
) async {
snackbarService.showSnackBar(
context,
Loc.of(context).credentialScanUnavailable,
);
}
}
10 changes: 10 additions & 0 deletions lib/features/vcs/vcs_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,14 @@ class VcsService {

return credentials.isEmpty ? null : credentials;
}

String parseCredential(String vcJwt) {
try {
final _ = Jwt.decode(vcJwt);
} on Exception {
rethrow;
}

return vcJwt;
}
}
Loading

0 comments on commit 09f3b3e

Please sign in to comment.