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

Commit 09f3b3e

Browse files
authored
feat: add VcsAddPage (#323)
1 parent 459c1f1 commit 09f3b3e

16 files changed

+349
-32
lines changed

lib/features/account/account_page.dart

+20-14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:didpay/features/pfis/pfi.dart';
55
import 'package:didpay/features/pfis/pfis_add_page.dart';
66
import 'package:didpay/features/pfis/pfis_notifier.dart';
77
import 'package:didpay/features/qr/qr_tabs.dart';
8+
import 'package:didpay/features/vcs/vcs_add_page.dart';
89
import 'package:didpay/features/vcs/vcs_notifier.dart';
910
import 'package:didpay/l10n/app_localizations.dart';
1011
import 'package:didpay/shared/modal/modal_manage_item.dart';
@@ -200,17 +201,17 @@ class AccountPage extends HookConsumerWidget {
200201
),
201202
),
202203
),
203-
credentials.isEmpty
204-
? TileContainer(child: _buildNoCredentialsTile(context))
205-
: ListView.builder(
206-
physics: const BouncingScrollPhysics(),
207-
shrinkWrap: true,
208-
itemCount: credentials.length,
209-
itemBuilder: (context, index) => TileContainer(
204+
ListView.builder(
205+
physics: const BouncingScrollPhysics(),
206+
shrinkWrap: true,
207+
itemCount: credentials.length + 1,
208+
itemBuilder: (context, index) => index < credentials.length
209+
? TileContainer(
210210
child:
211211
_buildCredentialTile(context, ref, credentials[index]),
212-
),
213-
),
212+
)
213+
: TileContainer(child: _buildAddCredentialTile(context)),
214+
),
214215
],
215216
);
216217

@@ -242,9 +243,9 @@ class AccountPage extends HookConsumerWidget {
242243
),
243244
);
244245

245-
Widget _buildNoCredentialsTile(BuildContext context) => ListTile(
246+
Widget _buildAddCredentialTile(BuildContext context) => ListTile(
246247
title: Text(
247-
Loc.of(context).noCredentialsIssuedYet,
248+
Loc.of(context).addACredential,
248249
style: Theme.of(context).textTheme.titleSmall,
249250
),
250251
leading: Container(
@@ -254,11 +255,16 @@ class AccountPage extends HookConsumerWidget {
254255
color: Theme.of(context).colorScheme.surfaceContainer,
255256
borderRadius: BorderRadius.circular(Grid.xxs),
256257
),
257-
child: Center(
258-
child:
259-
Icon(Icons.error, color: Theme.of(context).colorScheme.outline),
258+
child: const Center(
259+
child: Icon(Icons.add),
260260
),
261261
),
262+
onTap: () {
263+
Navigator.push(
264+
context,
265+
MaterialPageRoute(builder: (context) => const VcsAddPage()),
266+
);
267+
},
262268
);
263269

264270
Widget _buildFeatureFlagsList(

lib/features/device/device_info_service.dart

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class DeviceInfoService {
1111

1212
Future<bool> isPhysicalDevice() async {
1313
if (kIsWeb) {
14-
// On web, return false as there is no physical device
1514
return false;
1615
}
1716
if (Platform.isIOS) {
@@ -20,7 +19,7 @@ class DeviceInfoService {
2019
if (Platform.isAndroid) {
2120
return (await _deviceInfo.androidInfo).isPhysicalDevice;
2221
}
23-
// Default return for unsupported platforms
22+
2423
return false;
2524
}
2625
}

lib/features/did/did_form.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:didpay/features/qr/qr_tile.dart';
1+
import 'package:didpay/features/did/did_qr_tile.dart';
22
import 'package:didpay/l10n/app_localizations.dart';
33
import 'package:didpay/shared/next_button.dart';
44
import 'package:didpay/shared/theme/grid.dart';
@@ -49,7 +49,7 @@ class DidForm extends HookConsumerWidget {
4949
),
5050
),
5151
),
52-
QrTile(didTextController: textController),
52+
DidQrTile(didTextController: textController),
5353
NextButton(
5454
onPressed: () async => onSubmit(textController.text),
5555
title: buttonTitle,

lib/features/qr/qr_tile.dart lib/features/did/did_qr_tile.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
88
import 'package:hooks_riverpod/hooks_riverpod.dart';
99
import 'package:web5/web5.dart';
1010

11-
class QrTile extends HookConsumerWidget {
11+
class DidQrTile extends HookConsumerWidget {
1212
final TextEditingController didTextController;
1313

14-
const QrTile({
14+
const DidQrTile({
1515
required this.didTextController,
1616
super.key,
1717
});

lib/features/vcs/vcs_add_page.dart

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:didpay/features/vcs/vcs_form.dart';
2+
import 'package:didpay/features/vcs/vcs_notifier.dart';
3+
import 'package:didpay/l10n/app_localizations.dart';
4+
import 'package:didpay/shared/confirmation_message.dart';
5+
import 'package:didpay/shared/error_message.dart';
6+
import 'package:didpay/shared/header.dart';
7+
import 'package:didpay/shared/loading_message.dart';
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter_hooks/flutter_hooks.dart';
10+
import 'package:hooks_riverpod/hooks_riverpod.dart';
11+
12+
class VcsAddPage extends HookConsumerWidget {
13+
const VcsAddPage({super.key});
14+
15+
@override
16+
Widget build(BuildContext context, WidgetRef ref) {
17+
final vc = useState<AsyncValue<String>?>(null);
18+
19+
return Scaffold(
20+
appBar: AppBar(),
21+
body: SafeArea(
22+
child: vc.value != null
23+
? vc.value!.when(
24+
data: (_) => ConfirmationMessage(
25+
message: Loc.of(context).credentialAdded,
26+
),
27+
loading: () =>
28+
LoadingMessage(message: Loc.of(context).addingCredential),
29+
error: (error, _) => ErrorMessage(
30+
message: error.toString(),
31+
onRetry: () => vc.value = null,
32+
),
33+
)
34+
: Column(
35+
crossAxisAlignment: CrossAxisAlignment.stretch,
36+
children: [
37+
Header(
38+
title: Loc.of(context).addACredential,
39+
subtitle: Loc.of(context).enterACredentialJwt,
40+
),
41+
Expanded(
42+
child: VcsForm(
43+
buttonTitle: Loc.of(context).add,
44+
onSubmit: (vcJwt) async =>
45+
_addCredential(context, ref, vcJwt, vc),
46+
),
47+
),
48+
],
49+
),
50+
),
51+
);
52+
}
53+
54+
Future<void> _addCredential(
55+
BuildContext context,
56+
WidgetRef ref,
57+
String vcJwt,
58+
ValueNotifier<AsyncValue<String>?> state,
59+
) async {
60+
state.value = const AsyncLoading();
61+
try {
62+
final credential = await ref.read(vcsProvider.notifier).addJwt(vcJwt);
63+
64+
if (context.mounted) {
65+
state.value = AsyncData(credential);
66+
}
67+
} on Exception catch (e) {
68+
state.value = AsyncError(e, StackTrace.current);
69+
}
70+
}
71+
}

lib/features/vcs/vcs_form.dart

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'package:didpay/features/vcs/vcs_qr_tile.dart';
2+
import 'package:didpay/l10n/app_localizations.dart';
3+
import 'package:didpay/shared/next_button.dart';
4+
import 'package:didpay/shared/theme/grid.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_hooks/flutter_hooks.dart';
7+
import 'package:hooks_riverpod/hooks_riverpod.dart';
8+
9+
class VcsForm extends HookConsumerWidget {
10+
final String buttonTitle;
11+
final Future<void> Function(String) onSubmit;
12+
13+
VcsForm({required this.buttonTitle, required this.onSubmit, super.key});
14+
15+
final _formKey = GlobalKey<FormState>();
16+
17+
@override
18+
Widget build(BuildContext context, WidgetRef ref) {
19+
final focusNode = useFocusNode();
20+
21+
final textController = useTextEditingController();
22+
23+
return Form(
24+
key: _formKey,
25+
child: Column(
26+
crossAxisAlignment: CrossAxisAlignment.stretch,
27+
children: [
28+
Expanded(
29+
child: SingleChildScrollView(
30+
physics: const BouncingScrollPhysics(),
31+
padding: const EdgeInsets.symmetric(horizontal: Grid.side),
32+
child: Column(
33+
crossAxisAlignment: CrossAxisAlignment.stretch,
34+
children: [
35+
TextFormField(
36+
focusNode: focusNode,
37+
controller: textController,
38+
onTapOutside: (_) => focusNode.unfocus(),
39+
enableSuggestions: false,
40+
autocorrect: false,
41+
decoration: InputDecoration(
42+
labelText: Loc.of(context).credentialHint,
43+
),
44+
validator: (value) => value == null || value.isEmpty
45+
? Loc.of(context).thisFieldCannotBeEmpty
46+
: null,
47+
),
48+
],
49+
),
50+
),
51+
),
52+
VcsQrTile(credentialTextController: textController),
53+
NextButton(
54+
onPressed: () async => onSubmit(textController.text),
55+
title: buttonTitle,
56+
),
57+
],
58+
),
59+
);
60+
}
61+
}

lib/features/vcs/vcs_notifier.dart

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:didpay/features/kcc/lib.dart';
2+
import 'package:didpay/features/vcs/vcs_service.dart';
23
import 'package:hive/hive.dart';
34
import 'package:hooks_riverpod/hooks_riverpod.dart';
45

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

13-
VcsNotifier._(this.box, List<String> state) : super(state);
15+
VcsNotifier._(this.box, this.vcsService, List<String> state) : super(state);
1416

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

18-
return VcsNotifier._(box, vcs);
20+
return VcsNotifier._(box, vcsService, vcs);
21+
}
22+
23+
Future<String> addJwt(String vcJwt) async {
24+
final parsedJwt = vcsService.parseCredential(vcJwt);
25+
26+
if (state.any((elem) => elem == vcJwt)) {
27+
return parsedJwt;
28+
}
29+
30+
state = [...state, parsedJwt];
31+
32+
await _save();
33+
return parsedJwt;
1934
}
2035

2136
Future<String> add(CredentialResponse response) async {

lib/features/vcs/vcs_qr_tile.dart

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import 'package:didpay/features/device/device_info_service.dart';
2+
import 'package:didpay/features/qr/qr_scan_page.dart';
3+
import 'package:didpay/l10n/app_localizations.dart';
4+
import 'package:didpay/shared/snackbar/snackbar_service.dart';
5+
import 'package:didpay/shared/theme/grid.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_hooks/flutter_hooks.dart';
8+
import 'package:hooks_riverpod/hooks_riverpod.dart';
9+
10+
class VcsQrTile extends HookConsumerWidget {
11+
final TextEditingController credentialTextController;
12+
13+
const VcsQrTile({
14+
required this.credentialTextController,
15+
super.key,
16+
});
17+
18+
@override
19+
Widget build(BuildContext context, WidgetRef ref) {
20+
final isPhysicalDevice = useState(true);
21+
final snackbarService = SnackbarService();
22+
23+
useEffect(
24+
() {
25+
Future.delayed(
26+
Duration.zero,
27+
() async => isPhysicalDevice.value =
28+
await ref.read(deviceInfoServiceProvider).isPhysicalDevice(),
29+
);
30+
return null;
31+
},
32+
[],
33+
);
34+
35+
return Padding(
36+
padding: const EdgeInsets.symmetric(vertical: Grid.xxs),
37+
child: ListTile(
38+
leading: const Icon(Icons.qr_code),
39+
title: Text(
40+
Loc.of(context).scanACredentialJwt,
41+
style: Theme.of(context).textTheme.bodyMedium,
42+
),
43+
trailing: const Icon(Icons.chevron_right),
44+
onTap: () => isPhysicalDevice.value
45+
? _scanQrCode(
46+
context,
47+
credentialTextController,
48+
)
49+
: _simulateScanQrCode(
50+
context,
51+
credentialTextController,
52+
snackbarService,
53+
),
54+
),
55+
);
56+
}
57+
58+
Future<void> _scanQrCode(
59+
BuildContext context,
60+
TextEditingController credentialTextController,
61+
) async {
62+
final qrValue = await Navigator.of(context).push<String>(
63+
MaterialPageRoute(
64+
builder: (context) => const QrScanPage(),
65+
),
66+
);
67+
68+
credentialTextController.text = qrValue ?? '';
69+
}
70+
71+
Future<void> _simulateScanQrCode(
72+
BuildContext context,
73+
TextEditingController credentialTextController,
74+
SnackbarService snackbarService,
75+
) async {
76+
snackbarService.showSnackBar(
77+
context,
78+
Loc.of(context).credentialScanUnavailable,
79+
);
80+
}
81+
}

lib/features/vcs/vcs_service.dart

+10
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,14 @@ class VcsService {
1212

1313
return credentials.isEmpty ? null : credentials;
1414
}
15+
16+
String parseCredential(String vcJwt) {
17+
try {
18+
final _ = Jwt.decode(vcJwt);
19+
} on Exception {
20+
rethrow;
21+
}
22+
23+
return vcJwt;
24+
}
1525
}

0 commit comments

Comments
 (0)