diff --git a/lib/app/shared/helper_functions/helper_functions.dart b/lib/app/shared/helper_functions/helper_functions.dart index 23c73aa13..33d30801b 100644 --- a/lib/app/shared/helper_functions/helper_functions.dart +++ b/lib/app/shared/helper_functions/helper_functions.dart @@ -500,7 +500,14 @@ bool isSIOPV2OROIDC4VPUrl(Uri uri) { return isOpenIdUrl || isAuthorizeEndPoint || isSiopv2Url; } -Future getOIDC4VCTypeForIssuance({ +Future< + ( + OIDC4VCType?, + Map?, + Map?, + dynamic, + String?, + )> getIssuanceData({ required String url, required DioClient client, }) async { @@ -509,12 +516,13 @@ Future getOIDC4VCTypeForIssuance({ final keys = []; uri.queryParameters.forEach((key, value) => keys.add(key)); + dynamic credentialOfferJson; String? issuer; if (keys.contains('credential_offer') || keys.contains('credential_offer_uri')) { /// issuance case 2 - final dynamic credentialOfferJson = await getCredentialOfferJson( + credentialOfferJson = await getCredentialOfferJson( scannedResponse: uri.toString(), dioClient: client, ); @@ -529,7 +537,7 @@ Future getOIDC4VCTypeForIssuance({ } if (issuer == null) { - return null; + return (null, null, null, null, null); } final openidConfigurationResponse = await getOpenIdConfig( @@ -542,6 +550,7 @@ Future getOIDC4VCTypeForIssuance({ List? subjectSyntaxTypesSupported; String? tokenEndpoint; + Map? authorizationServerConfiguration; if (openidConfigurationResponse .containsKey('subject_syntax_types_supported')) { @@ -555,23 +564,23 @@ Future getOIDC4VCTypeForIssuance({ } if (authorizationServer != null) { - final openidConfigurationResponseSecond = await getOpenIdConfig( + authorizationServerConfiguration = await getOpenIdConfig( baseUrl: authorizationServer.toString(), client: client.dio, ); if (subjectSyntaxTypesSupported == null && - openidConfigurationResponseSecond + authorizationServerConfiguration .containsKey('subject_syntax_types_supported')) { subjectSyntaxTypesSupported = - openidConfigurationResponseSecond['subject_syntax_types_supported'] + authorizationServerConfiguration['subject_syntax_types_supported'] as List; } if (tokenEndpoint == null && - openidConfigurationResponseSecond.containsKey('token_endpoint')) { + authorizationServerConfiguration.containsKey('token_endpoint')) { tokenEndpoint = - openidConfigurationResponseSecond['token_endpoint'].toString(); + authorizationServerConfiguration['token_endpoint'].toString(); } } @@ -634,19 +643,51 @@ Future getOIDC4VCTypeForIssuance({ if (oidc4vcType == OIDC4VCType.DEFAULT || oidc4vcType == OIDC4VCType.EBSIV3) { if (credSupported['trust_framework'] == null) { - return OIDC4VCType.DEFAULT; + return ( + OIDC4VCType.DEFAULT, + openidConfigurationResponse, + authorizationServerConfiguration, + credentialOfferJson, + issuer + ); } if (credSupported['trust_framework']['name'] == 'ebsi') { - return OIDC4VCType.EBSIV3; + return ( + OIDC4VCType.EBSIV3, + openidConfigurationResponse, + authorizationServerConfiguration, + credentialOfferJson, + issuer + ); } else { - return OIDC4VCType.DEFAULT; + return ( + OIDC4VCType.DEFAULT, + openidConfigurationResponse, + authorizationServerConfiguration, + credentialOfferJson, + issuer + ); } } - return oidc4vcType; + + return ( + oidc4vcType, + openidConfigurationResponse, + authorizationServerConfiguration, + credentialOfferJson, + issuer + ); } } - return null; + + return ( + null, + openidConfigurationResponse, + authorizationServerConfiguration, + credentialOfferJson, + issuer + ); } Future isEBSIV3ForVerifiers({ @@ -910,3 +951,33 @@ bool hasVPToken(String responseType) { bool hasIDTokenOrVPToken(String responseType) { return responseType.contains('id_token') || responseType.contains('vp_token'); } + +String getFormattedStringOIDC4VCI({ + OIDC4VCType? oidc4vcType, + Map? openidConfigurationResponse, + Map? authorizationServerConfiguration, + dynamic credentialOfferJson, +}) { + return ''' +SCHEME : ${oidc4vcType!.offerPrefix} +\n +CREDENTIAL OFFER : +${credentialOfferJson != null ? const JsonEncoder.withIndent(' ').convert(credentialOfferJson) : 'None'} +\n +ENDPOINTS : + authorization server endpoint : ${openidConfigurationResponse?['authorization_server'] ?? 'None'} + token endpoint : ${openidConfigurationResponse?['token_endpoint'] ?? authorizationServerConfiguration?['token_endpoint'] ?? 'None'} + credential endpoint : ${openidConfigurationResponse?['credential_endpoint'] ?? 'None'} + deferred endpoint : ${openidConfigurationResponse?['deferred_endpoint'] ?? 'None'} + batch endpoint : ${openidConfigurationResponse?['batch_endpoint'] ?? 'None'} +\n +CREDENTIAL SUPPORTED : +${openidConfigurationResponse?['credentials_supported'] != null ? const JsonEncoder.withIndent(' ').convert(openidConfigurationResponse?['credentials_supported']) : 'None'} +\n +AUTHORIZATION SERVER CONFIGURATION : +${authorizationServerConfiguration != null ? const JsonEncoder.withIndent(' ').convert(authorizationServerConfiguration) : 'None'} +\n +CRDENTIAL ISSUER CONFIGURATION : +${openidConfigurationResponse != null ? const JsonEncoder.withIndent(' ').convert(openidConfigurationResponse) : 'None'} +'''; +} diff --git a/lib/dashboard/dashboard.dart b/lib/dashboard/dashboard.dart index 837604caa..e89cf643f 100644 --- a/lib/dashboard/dashboard.dart +++ b/lib/dashboard/dashboard.dart @@ -6,6 +6,7 @@ export 'discover/discover.dart'; export 'drawer/drawer.dart'; export 'general_information/general_information.dart'; export 'home/home.dart'; +export 'json_viewer/json_viewer.dart'; export 'missing_creentials/missing_credentials.dart'; export 'mnemonic_verification/mnemonic_verification.dart'; export 'profile/profile.dart'; diff --git a/lib/dashboard/json_viewer/json_viewer.dart b/lib/dashboard/json_viewer/json_viewer.dart new file mode 100644 index 000000000..c590b3b38 --- /dev/null +++ b/lib/dashboard/json_viewer/json_viewer.dart @@ -0,0 +1 @@ +export 'view/json_viewer_page.dart'; diff --git a/lib/dashboard/json_viewer/view/json_viewer_page.dart b/lib/dashboard/json_viewer/view/json_viewer_page.dart new file mode 100644 index 000000000..0c85250d0 --- /dev/null +++ b/lib/dashboard/json_viewer/view/json_viewer_page.dart @@ -0,0 +1,59 @@ +import 'package:altme/app/app.dart'; +import 'package:flutter/material.dart'; + +class JsonViewerPage extends StatelessWidget { + const JsonViewerPage({ + super.key, + required this.title, + required this.data, + }); + + final String title; + final String data; + + static Route route({ + required String title, + required String data, + }) => + MaterialPageRoute( + builder: (_) => JsonViewerPage( + title: title, + data: data, + ), + settings: const RouteSettings(name: '/JsonViewerPage'), + ); + + @override + Widget build(BuildContext context) { + return JsonViewerView( + title: title, + data: data, + ); + } +} + +class JsonViewerView extends StatelessWidget { + const JsonViewerView({ + super.key, + required this.title, + required this.data, + }); + + final String title; + final String data; + + @override + Widget build(BuildContext context) { + return BasePage( + title: title, + titleAlignment: Alignment.topCenter, + scrollView: true, + titleLeading: const BackLeadingButton(), + padding: const EdgeInsets.symmetric(horizontal: 10), + body: Text( + data, + style: Theme.of(context).textTheme.bodyMedium, + ), + ); + } +} diff --git a/lib/dashboard/qr_code/qr_code.dart b/lib/dashboard/qr_code/qr_code.dart index 302813ff0..b358bf3b1 100644 --- a/lib/dashboard/qr_code/qr_code.dart +++ b/lib/dashboard/qr_code/qr_code.dart @@ -1,2 +1,3 @@ export 'qr_code_scan/qr_code_scan.dart'; export 'qr_display/qr_display.dart'; +export 'widget/widget.dart'; diff --git a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart index ea99cbe26..dff359281 100644 --- a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart +++ b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart @@ -226,17 +226,14 @@ class QRCodeScanCubit extends Cubit { late final dynamic data; try { - if (isOIDC4VCIUrl(state.uri!)) { + if (isOIDC4VCIUrl(state.uri!) && oidcType != null) { /// issuer side (oidc4VCI) - - if (oidcType != null) { - await startOIDC4VCCredentialIssuance( - scannedResponse: state.uri.toString(), - isEBSIV3: oidcType == OIDC4VCType.EBSIV3, - qrCodeScanCubit: qrCodeScanCubit, - ); - return; - } + await startOIDC4VCCredentialIssuance( + scannedResponse: state.uri.toString(), + isEBSIV3: oidcType == OIDC4VCType.EBSIV3, + qrCodeScanCubit: qrCodeScanCubit, + ); + return; } if (isSIOPV2OROIDC4VPUrl(state.uri!)) { @@ -749,8 +746,8 @@ class QRCodeScanCubit extends Cubit { try { emit(state.loading()); - final OIDC4VCType? currentOIIDC4VCTypeForIssuance = - await getOIDC4VCTypeForIssuance( + final (OIDC4VCType? currentOIIDC4VCTypeForIssuance, _, _, _, _) = + await getIssuanceData( url: credentialModel.pendingInfo!.url, client: client, ); diff --git a/lib/dashboard/qr_code/widget/developer_mode_dialog.dart b/lib/dashboard/qr_code/widget/developer_mode_dialog.dart new file mode 100644 index 000000000..a6471ea19 --- /dev/null +++ b/lib/dashboard/qr_code/widget/developer_mode_dialog.dart @@ -0,0 +1,76 @@ +import 'package:altme/app/app.dart'; +import 'package:altme/l10n/l10n.dart'; +import 'package:altme/theme/theme.dart'; +import 'package:flutter/material.dart'; + +class DeveloperModeDialog extends StatelessWidget { + const DeveloperModeDialog({ + super.key, + required this.onDisplay, + required this.onDownload, + required this.onSkip, + }); + + final VoidCallback onDisplay; + final VoidCallback onDownload; + final VoidCallback onSkip; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.primary; + final background = Theme.of(context).colorScheme.popupBackground; + final textColor = Theme.of(context).colorScheme.dialogText; + + final l10n = context.l10n; + return AlertDialog( + backgroundColor: background, + surfaceTintColor: Colors.transparent, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(25)), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + IconStrings.cardReceive, + width: 50, + height: 50, + color: textColor, + ), + const SizedBox(height: 15), + MyElevatedButton( + text: l10n.displayConfiguration, + verticalSpacing: 14, + backgroundColor: color, + borderRadius: Sizes.smallRadius, + fontSize: 15, + elevation: 0, + onPressed: onDisplay, + ), + const SizedBox(height: Sizes.spaceSmall), + MyElevatedButton( + text: l10n.downloadConfiguration, + verticalSpacing: 14, + backgroundColor: color, + borderRadius: Sizes.smallRadius, + fontSize: 15, + elevation: 0, + onPressed: onDownload, + ), + const SizedBox(height: Sizes.spaceSmall), + MyElevatedButton( + text: l10n.skip, + verticalSpacing: 14, + backgroundColor: color, + borderRadius: Sizes.smallRadius, + fontSize: 15, + elevation: 0, + onPressed: onSkip, + ), + const SizedBox(height: 15), + ], + ), + ); + } +} diff --git a/lib/dashboard/qr_code/widget/widget.dart b/lib/dashboard/qr_code/widget/widget.dart new file mode 100644 index 000000000..7a458d82d --- /dev/null +++ b/lib/dashboard/qr_code/widget/widget.dart @@ -0,0 +1 @@ +export 'developer_mode_dialog.dart'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7bdd24b3b..fd1dc6f35 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -989,7 +989,9 @@ "type": "Type", "credentialExpired": "Credential Expired", "incorrectSignature": "Incorrect Signature", - "revokedOrSuspendedCredential": "Revoked or Suspended Credential" - + "revokedOrSuspendedCredential": "Revoked or Suspended Credential", + "displayConfiguration": "Display configuration", + "downloadConfiguration": "Download configuration", + "successfullyDownloaded": "Successfully Downloaded" } diff --git a/lib/l10n/untranslated.json b/lib/l10n/untranslated.json index 7fa79dc4d..2f7ee5a42 100644 --- a/lib/l10n/untranslated.json +++ b/lib/l10n/untranslated.json @@ -895,7 +895,10 @@ "type", "credentialExpired", "incorrectSignature", - "revokedOrSuspendedCredential" + "revokedOrSuspendedCredential", + "displayConfiguration", + "downloadConfiguration", + "successfullyDownloaded" ], "es": [ @@ -1794,7 +1797,10 @@ "type", "credentialExpired", "incorrectSignature", - "revokedOrSuspendedCredential" + "revokedOrSuspendedCredential", + "displayConfiguration", + "downloadConfiguration", + "successfullyDownloaded" ], "fr": [ @@ -1996,7 +2002,10 @@ "type", "credentialExpired", "incorrectSignature", - "revokedOrSuspendedCredential" + "revokedOrSuspendedCredential", + "displayConfiguration", + "downloadConfiguration", + "successfullyDownloaded" ], "it": [ @@ -2895,6 +2904,9 @@ "type", "credentialExpired", "incorrectSignature", - "revokedOrSuspendedCredential" + "revokedOrSuspendedCredential", + "displayConfiguration", + "downloadConfiguration", + "successfullyDownloaded" ] } diff --git a/lib/splash/bloclisteners/blocklisteners.dart b/lib/splash/bloclisteners/blocklisteners.dart index e87db3a83..b18882de7 100644 --- a/lib/splash/bloclisteners/blocklisteners.dart +++ b/lib/splash/bloclisteners/blocklisteners.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:altme/app/app.dart'; import 'package:altme/connection_bridge/connection_bridge.dart'; @@ -15,6 +16,7 @@ import 'package:altme/splash/splash.dart'; import 'package:altme/wallet/wallet.dart'; import 'package:beacon_flutter/beacon_flutter.dart'; import 'package:dio/dio.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -236,11 +238,95 @@ final qrCodeBlocListener = BlocListener( .startsWith(Parameters.authorizeEndPoint) || state.uri.toString().startsWith(Parameters.oidc4vcUniversalLink); - final OIDC4VCType? oidc4vcTypeForIssuance = - await getOIDC4VCTypeForIssuance( - url: state.uri.toString(), - client: DioClient('', Dio()), - ); + OIDC4VCType? oidc4vcTypeForIssuance; + + if (isOpenIDUrl || isFromDeeplink) { + final ( + OIDC4VCType? oidc4vcType, + Map? openidConfigurationResponse, + Map? authorizationServerConfiguration, + dynamic credentialOfferJson, + String? issuer, + ) = await getIssuanceData( + url: state.uri.toString(), + client: DioClient('', Dio()), + ); + + oidc4vcTypeForIssuance = oidc4vcType; + + /// if dev mode is ON show some dialog to show data + if (profileCubit.state.model.isDeveloperMode && + oidc4vcTypeForIssuance != null) { + final formattedData = getFormattedStringOIDC4VCI( + oidc4vcType: oidc4vcTypeForIssuance, + authorizationServerConfiguration: + authorizationServerConfiguration, + credentialOfferJson: credentialOfferJson, + openidConfigurationResponse: openidConfigurationResponse, + ); + LoadingView().hide(); + final bool moveAhead = await showDialog( + context: context, + builder: (_) { + return DeveloperModeDialog( + onDisplay: () async { + Navigator.of(context).pop(false); + await Navigator.of(context).push( + JsonViewerPage.route( + title: l10n.displayConfiguration, + data: formattedData, + ), + ); + return; + }, + onDownload: () async { + Navigator.of(context).pop(false); + final isPermissionStatusGranted = + await getStoragePermission(); + if (!isPermissionStatusGranted) { + throw ResponseMessage( + message: ResponseString + .STORAGE_PERMISSION_DENIED_MESSAGE, + ); + } + + final dateTime = getDateTimeWithoutSpace(); + final fileName = 'oidc4vci-data-$dateTime'; + + final fileSaver = FileSaver.instance; + + final fileBytes = + Uint8List.fromList(utf8.encode(formattedData)); + + final filePath = await fileSaver.saveAs( + name: fileName, + bytes: fileBytes, + ext: 'txt', + mimeType: MimeType.text, + ); + if (filePath != null && filePath.isEmpty) { + // + } else { + AlertMessage.showStateMessage( + context: context, + stateMessage: StateMessage.success( + showDialog: false, + stringMessage: l10n.successfullyDownloaded, + ), + ); + } + }, + onSkip: () { + Navigator.of(context).pop(true); + }, + ); + }, + ) ?? + true; + + if (!moveAhead) return; + } + } if (showPrompt) { if (isOpenIDUrl || isFromDeeplink) {