diff --git a/lib/app/shared/enum/message/response_string/response_string.dart b/lib/app/shared/enum/message/response_string/response_string.dart index 9341d2625..d8d456f42 100644 --- a/lib/app/shared/enum/message/response_string/response_string.dart +++ b/lib/app/shared/enum/message/response_string/response_string.dart @@ -150,4 +150,5 @@ enum ResponseString { RESPONSE_STRING_pleaseSwitchPolygonNetwork, RESPONSE_STRING_pleaseSwitchToCorrectOIDC4VCProfile, RESPONSE_STRING_authenticationSuccess, + RESPONSE_STRING_youcanSelectOnlyXCredential, } diff --git a/lib/app/shared/enum/message/response_string/response_string_extension.dart b/lib/app/shared/enum/message/response_string/response_string_extension.dart index 90f31ad7b..0c6db1b43 100644 --- a/lib/app/shared/enum/message/response_string/response_string_extension.dart +++ b/lib/app/shared/enum/message/response_string/response_string_extension.dart @@ -472,6 +472,11 @@ extension ResponseStringX on ResponseString { case ResponseString.RESPONSE_STRING_authenticationSuccess: return globalMessage.RESPONSE_STRING_authenticationSuccess; + + case ResponseString.RESPONSE_STRING_youcanSelectOnlyXCredential: + return globalMessage.RESPONSE_STRING_youcanSelectOnlyXCredential( + injectedMessage ?? '', + ); } } } diff --git a/lib/app/shared/message_handler/global_message.dart b/lib/app/shared/message_handler/global_message.dart index 2dde0d785..1ea2c9f2d 100644 --- a/lib/app/shared/message_handler/global_message.dart +++ b/lib/app/shared/message_handler/global_message.dart @@ -376,4 +376,7 @@ class GlobalMessage { l10n.pleaseSwitchToCorrectOIDC4VCProfile; String get RESPONSE_STRING_authenticationSuccess => l10n.authenticationSuccess; + + String RESPONSE_STRING_youcanSelectOnlyXCredential(String value) => + l10n.youcanSelectOnlyXCredential(value); } diff --git a/lib/app/shared/message_handler/response_message.dart b/lib/app/shared/message_handler/response_message.dart index af34b2117..6f2606ac0 100644 --- a/lib/app/shared/message_handler/response_message.dart +++ b/lib/app/shared/message_handler/response_message.dart @@ -699,6 +699,13 @@ class ResponseMessage with MessageHandler { return ResponseString.RESPONSE_STRING_authenticationSuccess.localise( context, ); + + case ResponseString.RESPONSE_STRING_youcanSelectOnlyXCredential: + return ResponseString.RESPONSE_STRING_youcanSelectOnlyXCredential + .localise( + context, + injectedMessage: injectedMessage, + ); } } return ''; diff --git a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_cubit.dart b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_cubit.dart index cd16dd25f..84b8cab79 100644 --- a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_cubit.dart +++ b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_cubit.dart @@ -1,3 +1,6 @@ +import 'dart:math'; + +import 'package:altme/app/shared/shared.dart'; import 'package:altme/dashboard/dashboard.dart'; import 'package:credential_manifest/credential_manifest.dart'; import 'package:equatable/equatable.dart'; @@ -30,6 +33,45 @@ class CredentialManifestPickCubit extends Cubit { required int inputDescriptorIndex, required bool? isJwtVpInJwtVCRequired, }) { + if (presentationDefinition.submissionRequirements != null) { + final inputDescriptors = List.of(presentationDefinition.inputDescriptors); + + final newInputDescriptor = []; + + /// grouping + while (inputDescriptors.isNotEmpty) { + final currentFirst = inputDescriptors.removeAt(0); + final group = currentFirst.group.toString(); + + final descriptorsWithSameGroup = inputDescriptors + .where((descriptor) => descriptor.group.toString() == group) + .toList(); + + if (descriptorsWithSameGroup.isNotEmpty) { + final mergedDescriptor = InputDescriptor( + id: '${currentFirst.id},${descriptorsWithSameGroup.map((e) => e.id).join(",")}', // ignore: lines_longer_than_80_chars + name: + '${currentFirst.name},${descriptorsWithSameGroup.map((e) => e.name).join(",")}', // ignore: lines_longer_than_80_chars + constraints: Constraints([ + ...?currentFirst.constraints?.fields, + for (final descriptor in descriptorsWithSameGroup) + ...?descriptor.constraints?.fields, + ]), + group: currentFirst.group, + purpose: + '${currentFirst.purpose},${descriptorsWithSameGroup.map((e) => e.purpose).join(",")}', // ignore: lines_longer_than_80_chars + ); + newInputDescriptor.add(mergedDescriptor); + inputDescriptors.removeWhere( + (descriptor) => descriptor.group.toString() == group); + } else { + newInputDescriptor.add(currentFirst); + } + } + + presentationDefinition.inputDescriptors = newInputDescriptor; + } + /// Get instruction to filter credentials of the wallet final filteredCredentialList = getCredentialsFromPresentationDefinition( presentationDefinition: presentationDefinition, @@ -38,26 +80,84 @@ class CredentialManifestPickCubit extends Cubit { isJwtVpInJwtVCRequired: isJwtVpInJwtVCRequired, ); - emit(state.copyWith(filteredCredentialList: filteredCredentialList)); + emit( + state.copyWith( + filteredCredentialList: filteredCredentialList, + presentationDefinition: presentationDefinition, + ), + ); } - void toggle(int index) { - if (state.selected.contains(index)) { - emit( - state.copyWith( - selected: List.from(state.selected) - ..removeWhere((element) => element == index), - ), - ); + void toggle({ + required int index, + required InputDescriptor inputDescriptor, + }) { + final bool isSelected = state.selected.contains(index); + + late List selected; + + if (isSelected) { + /// deSelecting the credential + selected = List.from(state.selected) + ..removeWhere((element) => element == index); + } else { + /// selecting the credential + selected = [ + ...state.selected, + ...[index] + ]; + } + + bool isButtonEnabled = selected.isNotEmpty; + + if (state.presentationDefinition!.submissionRequirements != null) { + final requirement = state.presentationDefinition!.submissionRequirements! + .where((element) => element.from == inputDescriptor.group?[0]) + .firstOrNull; + + if (requirement != null) { + final count = requirement.count; + final atLeast = requirement.min; + if (count != null) { + if (!isSelected) { + /// selecting the credential + + if (state.selected.length >= count) { + /// show message that limit is (count) + emit( + state.copyWith( + message: StateMessage.info( + messageHandler: ResponseMessage( + ResponseString + .RESPONSE_STRING_youcanSelectOnlyXCredential, + ), + injectedMessage: count.toString(), + ), + ), + ); + return; + } + } + + isButtonEnabled = selected.length == count; + } else if (atLeast != null) { + isButtonEnabled = selected.length < atLeast; + } else { + throw Exception(); + } + } else { + throw Exception(); + } } else { - emit( - state.copyWith( - selected: [ - ...state.selected, - ...[index], - ], - ), - ); + /// normal case + isButtonEnabled = selected.isNotEmpty; } + + emit( + state.copyWith( + selected: selected, + isButtonEnabled: isButtonEnabled, + ), + ); } } diff --git a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_state.dart b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_state.dart index 777293290..e8ac74554 100644 --- a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_state.dart +++ b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/cubit/credential_manifest_pick_state.dart @@ -3,24 +3,37 @@ part of 'credential_manifest_pick_cubit.dart'; @JsonSerializable() class CredentialManifestPickState extends Equatable { const CredentialManifestPickState({ + this.message, this.selected = const [], required this.filteredCredentialList, + this.presentationDefinition, + this.isButtonEnabled = false, }); factory CredentialManifestPickState.fromJson(Map json) => _$CredentialManifestPickStateFromJson(json); + final StateMessage? message; final List selected; final List filteredCredentialList; + final PresentationDefinition? presentationDefinition; + final bool isButtonEnabled; CredentialManifestPickState copyWith({ List? selected, List? filteredCredentialList, + PresentationDefinition? presentationDefinition, + bool? isButtonEnabled, + StateMessage? message, }) { return CredentialManifestPickState( selected: selected ?? this.selected, filteredCredentialList: filteredCredentialList ?? this.filteredCredentialList, + presentationDefinition: + presentationDefinition ?? this.presentationDefinition, + isButtonEnabled: isButtonEnabled ?? this.isButtonEnabled, + message: message, ); } @@ -30,5 +43,8 @@ class CredentialManifestPickState extends Equatable { List get props => [ selected, filteredCredentialList, + presentationDefinition, + isButtonEnabled, + message, ]; } diff --git a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart index a95688516..7cf446c1a 100644 --- a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart +++ b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart @@ -96,176 +96,162 @@ class CredentialManifestOfferPickView extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; - final PresentationDefinition presentationDefinition = - credential.credentialManifest!.presentationDefinition!; - - return BlocBuilder( - builder: (context, walletState) { - return BlocBuilder( - builder: (context, credentialManifestState) { - final allInputDescriptorConsidered = - presentationDefinition.submissionRequirements != null; - - final purpose = allInputDescriptorConsidered - ? presentationDefinition.purpose - : presentationDefinition - .inputDescriptors[inputDescriptorIndex].purpose; + return BlocConsumer( + listener: (context, state) { + if (state.message != null) { + AlertMessage.showStateMessage( + context: context, + stateMessage: state.message!, + ); + } + }, + builder: (context, credentialManifestState) { + final PresentationDefinition? presentationDefinition = + credentialManifestState.presentationDefinition; - final status = allInputDescriptorConsidered - ? '1/1' - : '${inputDescriptorIndex + 1}/${presentationDefinition.inputDescriptors.length}'; + return BlocListener( + listener: (context, scanState) { + if (scanState.status == ScanStatus.loading) { + LoadingView().show(context: context); + } else { + LoadingView().hide(); + } + if (scanState.message != null) { + AlertMessage.showStateMessage( + context: context, + stateMessage: scanState.message!, + ); + } + }, + child: credentialManifestState.filteredCredentialList.isEmpty + ? const RequiredCredentialNotFound() + : BasePage( + title: l10n.credentialPickTitle, + titleAlignment: Alignment.topCenter, + titleTrailing: const WhiteCloseButton(), + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 16, + ), + body: presentationDefinition == null + ? Container() + : Column( + children: [ + Text( + '${inputDescriptorIndex + 1}/${presentationDefinition.inputDescriptors.length}', + style: + Theme.of(context).textTheme.credentialSteps, + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.all(8), + child: Text( + presentationDefinition + .inputDescriptors[inputDescriptorIndex] + .purpose ?? + l10n.credentialPickSelect, + style: Theme.of(context) + .textTheme + .credentialSubtitle, + ), + ), + const SizedBox(height: 12), + ...List.generate( + credentialManifestState + .filteredCredentialList.length, + (index) { + final credentialModel = credentialManifestState + .filteredCredentialList[index]; - return BlocListener( - listener: (context, scanState) { - if (scanState.status == ScanStatus.loading) { - LoadingView().show(context: context); - } else { - LoadingView().hide(); - } - if (scanState.message != null) { - AlertMessage.showStateMessage( - context: context, - stateMessage: scanState.message!, - ); - } - }, - child: credentialManifestState.filteredCredentialList.isEmpty - ? const RequiredCredentialNotFound() - : BasePage( - title: l10n.credentialPickTitle, - titleAlignment: Alignment.topCenter, - titleTrailing: const WhiteCloseButton(), - padding: const EdgeInsets.symmetric( - vertical: 24, - horizontal: 16, - ), - body: Column( - children: [ - Text( - status, - style: Theme.of(context).textTheme.credentialSteps, - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(8), - child: Text( - purpose ?? l10n.credentialPickSelect, - style: Theme.of(context) - .textTheme - .credentialSubtitle, + return CredentialsListPageItem( + credentialModel: credentialModel, + selected: credentialManifestState.selected + .contains(index), + onTap: () { + context + .read() + .toggle( + index: index, + inputDescriptor: + presentationDefinition + .inputDescriptors[ + inputDescriptorIndex], + ); + }, + ); + }, ), - ), - const SizedBox(height: 12), - ...List.generate( - credentialManifestState - .filteredCredentialList.length, - (index) { - final credentialModel = credentialManifestState - .filteredCredentialList[index]; + ], + ), + navigation: credentialManifestState + .filteredCredentialList.isNotEmpty + ? SafeArea( + child: Container( + padding: const EdgeInsets.all(16), + child: Tooltip( + message: l10n.credentialPickPresent, + child: Builder( + builder: (context) { + final inputDescriptor = + presentationDefinition!.inputDescriptors[ + inputDescriptorIndex]; - if (allInputDescriptorConsidered) { - final atMost = presentationDefinition - .submissionRequirements![0].count; - final atLeast = presentationDefinition - .submissionRequirements![0].min; - if (atMost != null) { - // - } else if (atLeast != null) { - // - } else { - throw Exception(); - } - } + final bool isOptional = inputDescriptor + .constraints + ?.fields + ?.first + .optional ?? + false; - return CredentialsListPageItem( - credentialModel: credentialModel, - selected: credentialManifestState.selected - .contains(index), - onTap: () => context - .read() - .toggle(index), - ); - }, - ), - ], - ), - navigation: credentialManifestState - .filteredCredentialList.isNotEmpty - ? SafeArea( - child: Container( - padding: const EdgeInsets.all(16), - child: Tooltip( - message: l10n.credentialPickPresent, - child: Builder( - builder: (context) { - final inputDescriptor = + final bool isOngoingStep = + inputDescriptorIndex + 1 != presentationDefinition - .inputDescriptors[ - inputDescriptorIndex]; - - bool isOptional = inputDescriptor - .constraints - ?.fields - ?.first - .optional ?? - false; - - bool isOngoingStep = - inputDescriptorIndex + 1 != - presentationDefinition - .inputDescriptors.length; - - if (allInputDescriptorConsidered) { - isOptional = false; - isOngoingStep = false; - } + .inputDescriptors.length; - if (isOptional) { - return MyGradientButton( - onPressed: () => present( - context: context, - credentialManifestState: - credentialManifestState, - presentationDefinition: - presentationDefinition, - skip: credentialManifestState - .selected.isEmpty, - ), - text: credentialManifestState - .selected.isEmpty - ? l10n.skip - : isOngoingStep - ? l10n.next - : l10n.credentialPickPresent, - ); - } else { - return MyGradientButton( - onPressed: credentialManifestState - .selected.isEmpty - ? null - : () => present( - context: context, - credentialManifestState: - credentialManifestState, - presentationDefinition: - presentationDefinition, - skip: false, - ), - text: isOngoingStep + if (isOptional) { + return MyGradientButton( + onPressed: () => present( + context: context, + credentialManifestState: + credentialManifestState, + presentationDefinition: + presentationDefinition, + skip: credentialManifestState + .selected.isEmpty, + ), + text: credentialManifestState + .selected.isEmpty + ? l10n.skip + : isOngoingStep ? l10n.next : l10n.credentialPickPresent, - ); - } - }, - ), - ), + ); + } else { + return MyGradientButton( + onPressed: !credentialManifestState + .isButtonEnabled + ? null + : () => present( + context: context, + credentialManifestState: + credentialManifestState, + presentationDefinition: + presentationDefinition, + skip: false, + ), + text: isOngoingStep + ? l10n.next + : l10n.credentialPickPresent, + ); + } + }, ), - ) - : const SizedBox.shrink(), - ), - ); - }, + ), + ), + ) + : const SizedBox.shrink(), + ), ); }, ); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 9c76c1e24..5990b441f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -934,5 +934,12 @@ "pleaseInsertTheSecredCodeReceived": "Please insert the secret code received.", "userConsentForIssuerAccess": "User consent for issuer access", "userConsentForVerifierAccess": "User consent for verifier access", - "userPINCodeForAuthentication": "User PIN code for authentication" + "userPINCodeForAuthentication": "User PIN code for authentication", + "youcanSelectOnlyXCredential": "You can select only {count} credential(s).", + "@youcanSelectOnlyXCredential": { + "description": "", + "placeholders": { + "count": {} + } + } } \ No newline at end of file diff --git a/packages/credential_manifest/lib/src/models/presentation_definition.dart b/packages/credential_manifest/lib/src/models/presentation_definition.dart index e190e0ea2..f9b7875f8 100644 --- a/packages/credential_manifest/lib/src/models/presentation_definition.dart +++ b/packages/credential_manifest/lib/src/models/presentation_definition.dart @@ -21,7 +21,7 @@ class PresentationDefinition { final String? id; @JsonKey(name: 'input_descriptors') - final List inputDescriptors; + List inputDescriptors; @JsonKey(name: 'submission_requirements') List? submissionRequirements; String? name;