From d6d09acf16dd77501b0b4f7533b805be2469fccf Mon Sep 17 00:00:00 2001 From: Bibash Shrestha Date: Wed, 10 Apr 2024 11:52:29 +0545 Subject: [PATCH] feat: Add statis list cache button and caching for status list #2577 --- .../helper_functions/helper_functions.dart | 33 +++++++++++++++ .../view/oidc4vc_settings_menu.dart | 1 + .../widget/status_list_caching_widget.dart | 31 ++++++++++++++ .../ssi/oidc4vc_settngs/widget/widget.dart | 1 + .../cubit/credential_details_cubit.dart | 42 ++++++++++++------- .../profile/cubit/profile_cubit.dart | 2 + .../profile/models/profile_setting.dart | 20 +++------ lib/l10n/arb/app_en.arb | 4 +- lib/l10n/untranslated.json | 12 ++++-- 9 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/status_list_caching_widget.dart diff --git a/lib/app/shared/helper_functions/helper_functions.dart b/lib/app/shared/helper_functions/helper_functions.dart index fac078ec6..519d67f02 100644 --- a/lib/app/shared/helper_functions/helper_functions.dart +++ b/lib/app/shared/helper_functions/helper_functions.dart @@ -1918,3 +1918,36 @@ String? getWalletAddress(CredentialSubjectModel credentialSubjectModel) { } return null; } + +Future getCatchedGetData({ + required SecureStorageProvider secureStorageProvider, + required String url, + required Map headers, + required DioClient client, +}) async { + final cachedData = await secureStorageProvider.get(url); + + dynamic response; + + if (cachedData == null) { + response = await client.get(url, headers: headers); + final expiry = + DateTime.now().add(const Duration(days: 2)).millisecondsSinceEpoch; + + final value = {'expiry': expiry, 'data': response}; + await secureStorageProvider.set(url, jsonEncode(value)); + } else { + final cachedDataJson = jsonDecode(cachedData); + final expiry = int.parse(cachedDataJson['expiry'].toString()); + + final isExpired = DateTime.now().millisecondsSinceEpoch > expiry; + + if (isExpired) { + response = await client.get(url, headers: headers); + } else { + response = await cachedDataJson['data']; + } + } + + return response.toString(); +} diff --git a/lib/dashboard/drawer/ssi/oidc4vc_settngs/view/oidc4vc_settings_menu.dart b/lib/dashboard/drawer/ssi/oidc4vc_settngs/view/oidc4vc_settings_menu.dart index 069666a7f..2e185ddc9 100644 --- a/lib/dashboard/drawer/ssi/oidc4vc_settngs/view/oidc4vc_settings_menu.dart +++ b/lib/dashboard/drawer/ssi/oidc4vc_settngs/view/oidc4vc_settings_menu.dart @@ -49,6 +49,7 @@ class Oidc4vcSettingMenuView extends StatelessWidget { const ProofTypeWidget(), const ProofHeaderWidget(), const PushAuthorizationRequesWidget(), + const StatusListCachingWidget(), DrawerItem( title: l10n.walletMetadataForIssuers, onTap: () { diff --git a/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/status_list_caching_widget.dart b/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/status_list_caching_widget.dart new file mode 100644 index 000000000..668dc317a --- /dev/null +++ b/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/status_list_caching_widget.dart @@ -0,0 +1,31 @@ +import 'package:altme/dashboard/dashboard.dart'; +import 'package:altme/l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class StatusListCachingWidget extends StatelessWidget { + const StatusListCachingWidget({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BlocBuilder( + builder: (context, state) { + return OptionContainer( + title: l10n.statusListCachingTitle, + subtitle: l10n.statusListCachingSubTitle, + body: Switch( + onChanged: (value) async { + await context.read().updateProfileSetting( + statusListCaching: value, + ); + }, + value: state.model.profileSetting.selfSovereignIdentityOptions + .customOidc4vcProfile.statusListCache, + activeColor: Theme.of(context).colorScheme.primary, + ), + ); + }, + ); + } +} diff --git a/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/widget.dart b/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/widget.dart index ecff15b36..8fba43ada 100644 --- a/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/widget.dart +++ b/lib/dashboard/drawer/ssi/oidc4vc_settngs/widget/widget.dart @@ -10,4 +10,5 @@ export 'proof_type_widget.dart'; export 'push_authorization_request.dart'; export 'scope_parameter.dart'; export 'security_level_widget.dart'; +export 'status_list_caching_widget.dart'; export 'vc_format_widget.dart'; diff --git a/lib/dashboard/home/tab_bar/credentials/detail/cubit/credential_details_cubit.dart b/lib/dashboard/home/tab_bar/credentials/detail/cubit/credential_details_cubit.dart index c39040fc5..190afbca5 100644 --- a/lib/dashboard/home/tab_bar/credentials/detail/cubit/credential_details_cubit.dart +++ b/lib/dashboard/home/tab_bar/credentials/detail/cubit/credential_details_cubit.dart @@ -116,20 +116,25 @@ class CredentialDetailsCubit extends Cubit { final idx = statusList['idx']; if (idx != null && idx is int && uri != null && uri is String) { - final dynamic response = await client.get( - uri, - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'accept': 'application/statuslist+jwt', - }, + final headers = { + 'Content-Type': 'application/json; charset=UTF-8', + 'accept': 'application/statuslist+jwt', + }; + + final String response = await getCatchedGetData( + secureStorageProvider: secureStorageProvider, + url: uri, + headers: headers, + client: client, ); - final payload = jwtDecode.parseJwt(response.toString()); + + final payload = jwtDecode.parseJwt(response); /// verify the signature of the VC with the kid of the JWT final VerificationType isVerified = await verifyEncodedData( issuer: payload['iss']?.toString() ?? item.issuer, jwtDecode: jwtDecode, - jwt: response.toString(), + jwt: response, ); if (isVerified != VerificationType.verified) { @@ -187,21 +192,26 @@ class CredentialDetailsCubit extends Cubit { if (iteratedData is Map) { final data = CredentialStatusField.fromJson(iteratedData); - final dynamic response = await client.get( - data.statusListCredential, - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - 'accept': 'application/statuslist+jwt', - }, + final url = data.statusListCredential; + final headers = { + 'Content-Type': 'application/json; charset=UTF-8', + 'accept': 'application/statuslist+jwt', + }; + + final String response = await getCatchedGetData( + secureStorageProvider: secureStorageProvider, + url: url, + headers: headers, + client: client, ); - final payload = jwtDecode.parseJwt(response.toString()); + final payload = jwtDecode.parseJwt(response); // verify the signature of the VC with the kid of the JWT final VerificationType isVerified = await verifyEncodedData( issuer: payload['iss']?.toString() ?? item.issuer, jwtDecode: jwtDecode, - jwt: response.toString(), + jwt: response, ); if (isVerified != VerificationType.verified) { diff --git a/lib/dashboard/profile/cubit/profile_cubit.dart b/lib/dashboard/profile/cubit/profile_cubit.dart index da4cb3e7c..818852678 100644 --- a/lib/dashboard/profile/cubit/profile_cubit.dart +++ b/lib/dashboard/profile/cubit/profile_cubit.dart @@ -399,6 +399,7 @@ class ProfileCubit extends Cubit { ProofHeaderType? proofHeaderType, ProofType? proofType, bool? pushAuthorizationRequest, + bool? statusListCaching, }) async { final profileModel = state.model.copyWith( profileSetting: state.model.profileSetting.copyWith( @@ -429,6 +430,7 @@ class ProfileCubit extends Cubit { vcFormatType: vcFormatType, proofType: proofType, pushAuthorizationRequest: pushAuthorizationRequest, + statusListCache: statusListCaching, ), ), ), diff --git a/lib/dashboard/profile/models/profile_setting.dart b/lib/dashboard/profile/models/profile_setting.dart index 6c34fd5a2..d206a9438 100644 --- a/lib/dashboard/profile/models/profile_setting.dart +++ b/lib/dashboard/profile/models/profile_setting.dart @@ -542,6 +542,7 @@ class CustomOidc4VcProfile extends Equatable { this.proofHeader = ProofHeaderType.kid, this.proofType = ProofType.jwt, this.pushAuthorizationRequest = false, + this.statusListCache = true, }); factory CustomOidc4VcProfile.initial() => CustomOidc4VcProfile( @@ -570,16 +571,12 @@ class CustomOidc4VcProfile extends Equatable { final String? clientSecret; final bool cryptoHolderBinding; final DidKeyType defaultDid; - //TODO(bibash): temporary solution to avoid who have chosen 12 - @JsonKey( - includeFromJson: true, - fromJson: oidc4vciDraftFromJson, - ) final OIDC4VCIDraftType oidc4vciDraft; final OIDC4VPDraftType oidc4vpDraft; final bool scope; final ProofHeaderType proofHeader; final bool securityLevel; + final bool statusListCache; final bool pushAuthorizationRequest; final SIOPV2DraftType siopv2Draft; @JsonKey(name: 'subjectSyntaxeType') @@ -590,16 +587,6 @@ class CustomOidc4VcProfile extends Equatable { Map toJson() => _$CustomOidc4VcProfileToJson(this); - static OIDC4VCIDraftType oidc4vciDraftFromJson(dynamic value) { - if (value == '11') { - return OIDC4VCIDraftType.draft11; - } else if (value == '12' || value == '13') { - return OIDC4VCIDraftType.draft13; - } else { - throw Exception(); - } - } - CustomOidc4VcProfile copyWith({ ClientAuthentication? clientAuthentication, bool? credentialManifestSupport, @@ -612,6 +599,7 @@ class CustomOidc4VcProfile extends Equatable { bool? scope, ProofHeaderType? proofHeader, bool? securityLevel, + bool? statusListCache, bool? pushAuthorizationRequest, SIOPV2DraftType? siopv2Draft, ClientType? clientType, @@ -629,6 +617,7 @@ class CustomOidc4VcProfile extends Equatable { scope: scope ?? this.scope, proofHeader: proofHeader ?? this.proofHeader, securityLevel: securityLevel ?? this.securityLevel, + statusListCache: statusListCache ?? this.statusListCache, pushAuthorizationRequest: pushAuthorizationRequest ?? this.pushAuthorizationRequest, siopv2Draft: siopv2Draft ?? this.siopv2Draft, @@ -652,6 +641,7 @@ class CustomOidc4VcProfile extends Equatable { scope, proofHeader, securityLevel, + statusListCache, pushAuthorizationRequest, siopv2Draft, clientType, diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 870bfb779..27cf795af 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1051,5 +1051,7 @@ "statusIsInvalid": "Status is invalid", "statuslListSignatureFailed": "Status list signature failed", "walletMetadataForIssuers": "Wallet metadata for issuers", - "walletMetadataForVerifiers": "Wallet metadata for verifiers" + "walletMetadataForVerifiers": "Wallet metadata for verifiers", + "statusListCachingTitle": "StatusList caching", + "statusListCachingSubTitle": "Default: On\nSwitch off to reload StatusList when needed" } diff --git a/lib/l10n/untranslated.json b/lib/l10n/untranslated.json index a1fd28bc3..751fe6965 100644 --- a/lib/l10n/untranslated.json +++ b/lib/l10n/untranslated.json @@ -18,7 +18,9 @@ "statusIsInvalid", "statuslListSignatureFailed", "walletMetadataForIssuers", - "walletMetadataForVerifiers" + "walletMetadataForVerifiers", + "statusListCachingTitle", + "statusListCachingSubTitle" ], "es": [ @@ -40,7 +42,9 @@ "statusIsInvalid", "statuslListSignatureFailed", "walletMetadataForIssuers", - "walletMetadataForVerifiers" + "walletMetadataForVerifiers", + "statusListCachingTitle", + "statusListCachingSubTitle" ], "fr": [ @@ -52,6 +56,8 @@ "statusIsInvalid", "statuslListSignatureFailed", "walletMetadataForIssuers", - "walletMetadataForVerifiers" + "walletMetadataForVerifiers", + "statusListCachingTitle", + "statusListCachingSubTitle" ] }