From 835f3bcd39eb4dfd6c87912ff50804115dab8ecc Mon Sep 17 00:00:00 2001 From: vafeini <129304399+vafeini@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:58:33 +0200 Subject: [PATCH] Reworked UI (#92) * Stepper approach to define presentation request. First select scheme, then select presentation scenario and attestation details and in the end specify the attributes * Popup dialog for selecting attestation attributes * Moved scheme config to top menu, present summary of presentation request, init transaction with collected * Added badges to indicate number of attributes selected * Added link for custom request preparation * Improvements * Removed un-used routes, moved selectable attributes component to presentation preparation module, clean up un-used components * Removed un-used routes, moved selectable attributes component to presentation preparation module, clean up un-used components * When re-opening attributes selection popup, remember previous selection, bug fixes * Display unsupported formats as disabled options, minor fixes * Replaced qrcodejs with angularx-qrcode for QR code rendering * Bump version to 0.6.0 --- angular.json | 5 +- package.json | 4 +- src/QRCode.d.ts | 1 - src/app/app-routing.module.ts | 23 +-- src/app/core/constants/attestations.ts | 141 +++++++++++++++ .../constants/{constants.ts => general.ts} | 0 .../core/constants/presentation-scenarios.ts | 54 ++++++ src/app/core/data/MsoMdocDocuments.ts | 106 +++-------- .../wallet-layout-header.component.html | 3 + .../wallet-layout-header.component.ts | 11 ++ src/app/core/models/MsoMdoc.ts | 11 -- .../core/models/attestation/Attestation.ts | 13 ++ .../{ => attestation}/AttestationFormat.ts | 0 .../models/attestation/AttestationType.ts | 6 + .../models/attestation/MsoMdocAttestation.ts | 7 + ...InputDescriptors.ts => InputDescriptor.ts} | 2 +- .../presentation/PresentationDefinition.ts | 4 +- .../models/presentation/SharedAttestation.ts | 2 +- .../attestation-selectable-model.service.ts | 6 +- .../services/decoders-registry.service.ts | 2 +- .../services/decoders/AttestationDecoder.ts | 2 +- .../core/services/decoders/DecodingUtils.ts | 25 +++ .../decoders/JwtVcJsonAttestationDecoder.ts | 31 +--- .../decoders/MsoMdocAttestationDecoder.ts | 32 +--- .../services/mso-mdoc-presentation.service.ts | 64 ++++--- .../services/verifier-endpoint.service.ts | 2 +- .../home/components/home/home.component.html | 34 ---- .../home/components/home/home.component.scss | 83 --------- .../components/home/home.component.spec.ts | 40 ---- .../home/components/home/home.component.ts | 141 --------------- .../input-scheme/input-scheme.component.html | 18 -- src/app/features/home/models/menu-option.ts | 6 - .../home/services/home.service.spec.ts | 19 -- .../features/home/services/home.service.ts | 50 ----- .../components/qr-code/qr-code.component.html | 19 +- .../components/qr-code/qr-code.component.ts | 28 +-- .../wallet-response-processor.service.ts | 2 +- .../attestation/attestation.component.html | 31 ++++ .../attestation/attestation.component.ts | 83 +++++++++ .../attestation/model/format-select-option.ts | 5 + .../attribute-selection.component.html | 15 ++ .../attribute-selection.component.ts | 171 ++++++++++++++++++ .../scenario/scenario.component.html | 23 +++ .../components/scenario/scenario.component.ts | 83 +++++++++ .../model/DialogData.ts | 10 + .../model/DialogResult.ts | 7 + ...able-attestation-attributes.component.html | 34 ++++ ...ble-attestation-attributes.component.scss} | 2 +- ...-attestation-attributes.component.spec.ts} | 10 +- ...ctable-attestation-attributes.component.ts | 168 +++++++++++++++++ .../home/home.component.html | 60 ++++++ .../home/home.component.spec.ts | 30 +++ .../home/home.component.ts | 105 +++++++++++ .../models/PresentationScenario.ts | 6 + .../models/ScenarioAttestation.ts | 11 ++ .../models/ScenarioSelection.ts | 15 ++ ...tion-request-preparation-routing.module.ts | 23 +++ ...presentation-request-preparation.module.ts | 15 ++ .../components/home/home.component.html | 9 - .../components/home/home.component.spect.ts | 31 ---- .../components/home/home.component.ts | 46 ----- ...electable-presentation-form.component.html | 23 --- .../selectable-presentation-form.component.ts | 128 ------------- .../selectable-presentation-routing.module.ts | 21 --- .../selectable-presentation.module.ts | 35 ---- ...electable-form-next-action.service.spec.ts | 18 -- .../selectable-form-next-action.service.ts | 34 ---- .../resolver/wallet-redirect-resolver.ts | 2 +- .../input-scheme/error-state-matcher.ts | 0 .../input-scheme/input-scheme.component.html | 26 +++ .../input-scheme/input-scheme.component.scss | 0 .../input-scheme.component.spec.ts | 2 +- .../input-scheme/input-scheme.component.ts | 10 +- 73 files changed, 1321 insertions(+), 968 deletions(-) delete mode 100644 src/QRCode.d.ts create mode 100644 src/app/core/constants/attestations.ts rename src/app/core/constants/{constants.ts => general.ts} (100%) create mode 100644 src/app/core/constants/presentation-scenarios.ts delete mode 100644 src/app/core/models/MsoMdoc.ts create mode 100644 src/app/core/models/attestation/Attestation.ts rename src/app/core/models/{ => attestation}/AttestationFormat.ts (100%) create mode 100644 src/app/core/models/attestation/AttestationType.ts create mode 100644 src/app/core/models/attestation/MsoMdocAttestation.ts rename src/app/core/models/presentation/{InputDescriptors.ts => InputDescriptor.ts} (88%) create mode 100644 src/app/core/services/decoders/DecodingUtils.ts delete mode 100644 src/app/features/home/components/home/home.component.html delete mode 100644 src/app/features/home/components/home/home.component.scss delete mode 100644 src/app/features/home/components/home/home.component.spec.ts delete mode 100644 src/app/features/home/components/home/home.component.ts delete mode 100644 src/app/features/home/components/input-scheme/input-scheme.component.html delete mode 100644 src/app/features/home/models/menu-option.ts delete mode 100644 src/app/features/home/services/home.service.spec.ts delete mode 100644 src/app/features/home/services/home.service.ts create mode 100644 src/app/features/presentation-request-preparation/components/attestation/attestation.component.html create mode 100644 src/app/features/presentation-request-preparation/components/attestation/attestation.component.ts create mode 100644 src/app/features/presentation-request-preparation/components/attestation/model/format-select-option.ts create mode 100644 src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.html create mode 100644 src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.ts create mode 100644 src/app/features/presentation-request-preparation/components/scenario/scenario.component.html create mode 100644 src/app/features/presentation-request-preparation/components/scenario/scenario.component.ts create mode 100644 src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogData.ts create mode 100644 src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogResult.ts create mode 100644 src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.html rename src/app/features/{selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss => presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.scss} (97%) rename src/app/features/{selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts => presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.spec.ts} (76%) create mode 100644 src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.ts create mode 100644 src/app/features/presentation-request-preparation/home/home.component.html create mode 100644 src/app/features/presentation-request-preparation/home/home.component.spec.ts create mode 100644 src/app/features/presentation-request-preparation/home/home.component.ts create mode 100644 src/app/features/presentation-request-preparation/models/PresentationScenario.ts create mode 100644 src/app/features/presentation-request-preparation/models/ScenarioAttestation.ts create mode 100644 src/app/features/presentation-request-preparation/models/ScenarioSelection.ts create mode 100644 src/app/features/presentation-request-preparation/presentation-request-preparation-routing.module.ts create mode 100644 src/app/features/presentation-request-preparation/presentation-request-preparation.module.ts delete mode 100644 src/app/features/selectable-presentation/components/home/home.component.html delete mode 100644 src/app/features/selectable-presentation/components/home/home.component.spect.ts delete mode 100644 src/app/features/selectable-presentation/components/home/home.component.ts delete mode 100644 src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html delete mode 100644 src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts delete mode 100644 src/app/features/selectable-presentation/selectable-presentation-routing.module.ts delete mode 100644 src/app/features/selectable-presentation/selectable-presentation.module.ts delete mode 100644 src/app/features/selectable-presentation/services/selectable-form-next-action.service.spec.ts delete mode 100644 src/app/features/selectable-presentation/services/selectable-form-next-action.service.ts rename src/app/{features/home/components => shared/elements}/input-scheme/error-state-matcher.ts (100%) create mode 100644 src/app/shared/elements/input-scheme/input-scheme.component.html rename src/app/{features/home/components => shared/elements}/input-scheme/input-scheme.component.scss (100%) rename src/app/{features/home/components => shared/elements}/input-scheme/input-scheme.component.spec.ts (95%) rename src/app/{features/home/components => shared/elements}/input-scheme/input-scheme.component.ts (71%) diff --git a/angular.json b/angular.json index ccc5d35..73d2543 100644 --- a/angular.json +++ b/angular.json @@ -44,10 +44,7 @@ "includePaths": [ "src/template.sass" ] - }, - "scripts": [ - "./node_modules/qrcodejs/qrcode.min.js" - ] + } }, "configurations": { "production": { diff --git a/package.json b/package.json index db906ad..3be91dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "verifier-ui", - "version": "0.5.2-SNAPSHOT", + "version": "0.6.0", "scripts": { "ng": "ng", "start": "npm run config && ng serve --proxy-config src/proxy.conf.json", @@ -37,7 +37,7 @@ "jsonpath-plus": "^9.0.0", "jwt-decode": "3.1.2", "nofilter": "^4.0.2", - "qrcodejs": "^1.0.0", + "angularx-qrcode": "^15.0.1", "rxjs": "~7.8.0", "stream-browserify": "^3.0.0", "thememirror": "^2.0.1", diff --git a/src/QRCode.d.ts b/src/QRCode.d.ts deleted file mode 100644 index 40277bd..0000000 --- a/src/QRCode.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'QRCode'; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index a729c69..7caaba8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,25 +5,18 @@ import {NavigateService} from '@core/services/navigate.service'; import {VerifierEndpointService} from "@core/services/verifier-endpoint.service"; const routes: Routes = [ - { path: '', redirectTo: 'home', pathMatch: 'full' }, - { path: 'home', loadComponent: () => import('./features/home/components/home/home.component').then(c => c.HomeComponent) }, + { path: '', + redirectTo: 'home', + pathMatch: 'full' }, + { path: 'home', loadComponent: () => import('@features/presentation-request-preparation/home/home.component') + .then(c => c.HomeComponent) }, { path: 'custom-request', loadChildren: () => import('@features/custom-presentation-request/custom-presentation-request.module'). then(m => m.CustomPresentationRequestModule )}, - { path: 'selectable', - loadChildren: () => import('@features/selectable-presentation/selectable-presentation.module'). - then(m => m.SelectablePresentationModule ) - }, - { path: 'invoke-wallet', + { path: 'invoke-wallet', loadChildren: () => import('@features/invoke-wallet/invoke-wallet.module'). - then(m => m.InvokeWalletModule) - }, - { path: 'mdl-selectable', - loadChildren: () => import('@features/selectable-presentation/selectable-presentation.module'). - then(m => m.SelectablePresentationModule ), - }, - { - path: 'get-wallet-code', + then(m => m.InvokeWalletModule) }, + { path: 'get-wallet-code', loadComponent: () => import('./features/wallet-redirect/wallet-redirect.component').then(c => c.WalletRedirectComponent), providers: [VerifierEndpointService, NavigateService], resolve: { diff --git a/src/app/core/constants/attestations.ts b/src/app/core/constants/attestations.ts new file mode 100644 index 0000000..3e24d9f --- /dev/null +++ b/src/app/core/constants/attestations.ts @@ -0,0 +1,141 @@ +import {Attestation} from "@core/models/attestation/Attestation"; +import {AttestationType} from "@core/models/attestation/AttestationType"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; + +export const PID_ATTESTATION: Attestation = { + name: "Person Identification Data (PID)", + type: AttestationType.PID, + dataSet: [ + { identifier: 'family_name', attribute: 'Family name'}, + { identifier: 'given_name', attribute: 'Given name'}, + { identifier: 'birth_date', attribute: 'Birthdate'}, + { identifier: 'age_over_18', attribute: 'Age over 18'}, + { identifier: 'age_in_years', attribute: 'Age in years'}, + { identifier: 'age_birth_year', attribute: 'Age birth year'}, + { identifier: 'family_name_birth', attribute: 'Family name birth'}, + { identifier: 'given_name_birth', attribute: 'Given name birth'}, + { identifier: 'birth_place', attribute: 'Birth place'}, + { identifier: 'birth_country', attribute: 'Birth country'}, + { identifier: 'birth_state', attribute: 'Birth state'}, + { identifier: 'birth_city', attribute: 'Birth city'}, + { identifier: 'resident_address', attribute: 'Resident address'}, + { identifier: 'resident_country', attribute: 'Resident country'}, + { identifier: 'resident_state', attribute: 'Resident state'}, + { identifier: 'resident_city', attribute: 'Resident city'}, + { identifier: 'resident_postal_code', attribute: 'Resident postal code'}, + { identifier: 'resident_street', attribute: 'Resident street'}, + { identifier: 'resident_house_number', attribute: 'Resident house number'}, + { identifier: 'gender', attribute: 'Gender'}, + { identifier: 'nationality', attribute: 'Nationality'}, + { identifier: 'issuance_date', attribute: 'Issuance date'}, + { identifier: 'expiry_date', attribute: 'Expiry date'}, + { identifier: 'issuing_authority', attribute: 'Issuing authority'}, + { identifier: 'document_number', attribute: 'Document number'}, + { identifier: 'administrative_number', attribute: 'Administrative number'}, + { identifier: 'issuing_country', attribute: 'Issuing country'}, + { identifier: 'issuing_jurisdiction', attribute: 'Issuing jurisdiction'} + ] +} + +export const MDL_ATTESTATION: Attestation = { + name: "Mobile Driving Licence (MDL)", + type: AttestationType.MDL, + dataSet: [ + { identifier: 'family_name', attribute: 'Family name' }, + { identifier: 'given_name', attribute: 'Given name'}, + { identifier: 'birth_date', attribute: 'Birthdate'}, + { identifier: 'issue_date', attribute: 'Issue date'}, + { identifier: 'expiry_date', attribute: 'Expiry date'}, + { identifier: 'age_over_18', attribute: 'Age over 18'}, + { identifier: 'age_over_21', attribute: 'Age over 21'}, + { identifier: 'age_in_years', attribute: 'Age in years'}, + { identifier: 'age_birth_year', attribute: 'Age birth year'}, + { identifier: 'issuing_authority', attribute: 'Issuing authority'}, + { identifier: 'document_number', attribute: 'Document number'}, + { identifier: 'portrait', attribute: 'Portrait'}, + { identifier: 'driving_privileges', attribute: 'Driving privileges'}, + { identifier: 'un_distinguishing_sign', attribute: 'Un-distinguishing sign'}, + { identifier: 'administrative_number', attribute: 'Administrative number'}, + { identifier: 'sex', attribute: 'Sex'}, + { identifier: 'height', attribute: 'Height'}, + { identifier: 'weight', attribute: 'Weight'}, + { identifier: 'eye_colour', attribute: 'Eye colour'}, + { identifier: 'hair_colour', attribute: 'Hair colour'}, + { identifier: 'birth_place', attribute: 'Birth place'}, + { identifier: 'resident_address', attribute: 'Resident address'}, + { identifier: 'portrait_capture_date', attribute: 'Portrait capture date'}, + { identifier: 'nationality', attribute: 'Nationality'}, + { identifier: 'resident_city', attribute: 'Resident city'}, + { identifier: 'resident_state', attribute: 'Resident state'}, + { identifier: 'resident_postal_code', attribute: 'Resident postal code'}, + { identifier: 'resident_country', attribute: 'Resident country'}, + { identifier: 'family_name_national_character', attribute: 'Family name national character'}, + { identifier: 'given_name_national_character', attribute: 'Given name national character'}, + { identifier: 'signature_usual_mark', attribute: 'Signature usual mark'} + ] +} + +export const AGE_OVER_18_ATTESTATION: Attestation = { + name: "Age Over 18", + type: AttestationType.AGE_OVER_18, + dataSet: [ + { identifier: 'age_over_18', attribute: 'Age over 18'}, + { identifier: 'user_pseudonym', attribute: 'User pseudonym'}, + { identifier: 'issuance_date', attribute: 'Issuance date'}, + { identifier: 'expiry_date', attribute: 'Expiry date'}, + { identifier: 'issuing_authority', attribute: 'Issuing authority'}, + { identifier: 'issuing_jurisdiction', attribute: 'Issuing jurisdiction'}, + { identifier: 'issuing_country', attribute: 'Issuing country'} + ] +} + +export const PHOTO_ID_ATTESTATION: Attestation = { + name: "Photo ID", + type: AttestationType.PHOTO_ID, + dataSet: [ + { identifier: 'portrait', attribute: 'Portrait'}, + { identifier: 'portrait_capture_date', attribute: 'Portrait capture date'}, + { identifier: 'person_id', attribute: 'Person id'}, + { identifier: 'family_name', attribute: 'Family_name'}, + { identifier: 'given_name', attribute: 'Given name'}, + { identifier: 'birth_date', attribute: 'Birth date'}, + { identifier: 'age_over_18', attribute: 'Age over 18'}, + { identifier: 'age_over_NN', attribute: 'Age over NN'}, + { identifier: 'age_in_years', attribute: 'Age in years'}, + { identifier: 'age_birth_year', attribute: 'Age birth year'}, + { identifier: 'family_name_birth', attribute: 'Family name birth'}, + { identifier: 'given_name_birth', attribute: 'Given name birth'}, + { identifier: 'birth_place', attribute: 'Birth place'}, + { identifier: 'birth_country', attribute: 'Birth country'}, + { identifier: 'birth_state', attribute: 'Birth state'}, + { identifier: 'birth_city', attribute: 'Birth city'}, + { identifier: 'resident_address', attribute: 'Resident address'}, + { identifier: 'resident_country', attribute: 'Resident country'}, + { identifier: 'resident_state', attribute: 'Resident state'}, + { identifier: 'resident_city', attribute: 'Resident city'}, + { identifier: 'resident_postal_code', attribute: 'Resident postal code'}, + { identifier: 'resident_street', attribute: 'Resident street'}, + { identifier: 'resident_house_number', attribute: 'Resident house number'}, + { identifier: 'gender', attribute: 'Gender'}, + { identifier: 'nationality', attribute: 'Nationality'}, + { identifier: 'issuance_date', attribute: 'Issuance date'}, + { identifier: 'expiry_date', attribute: 'Expiry date'}, + { identifier: 'issuing_authority', attribute: 'Issuing authority'}, + { identifier: 'document_number', attribute: 'Document number'}, + { identifier: 'administrative_number', attribute: 'Administrative number'}, + { identifier: 'issuing_country', attribute: 'Issuing country'}, + { identifier: 'issuing_jurisdiction', attribute: 'Issuing jurisdiction'} + ] +} + +export const SUPPORTED_ATTESTATIONS: { [id: string]: Attestation } = { + "pid": PID_ATTESTATION, + "mdl": MDL_ATTESTATION, + "photo_id": PHOTO_ID_ATTESTATION, + "age_over_18": AGE_OVER_18_ATTESTATION, +} + +export const SUPPORTED_FORMATS: AttestationFormat[] = [ + AttestationFormat.MSO_MDOC +] + diff --git a/src/app/core/constants/constants.ts b/src/app/core/constants/general.ts similarity index 100% rename from src/app/core/constants/constants.ts rename to src/app/core/constants/general.ts diff --git a/src/app/core/constants/presentation-scenarios.ts b/src/app/core/constants/presentation-scenarios.ts new file mode 100644 index 0000000..f4f05e1 --- /dev/null +++ b/src/app/core/constants/presentation-scenarios.ts @@ -0,0 +1,54 @@ +import {PresentationScenario} from "@features/presentation-request-preparation/models/PresentationScenario"; +import {AttributeSelectionMethod} from "@features/presentation-request-preparation/models/ScenarioAttestation"; +import {AttestationType} from "@core/models/attestation/AttestationType"; + +export const PID_AUTHENTICATION_SCENARIO: PresentationScenario = { + name: "PID Authentication", + attestations: [{ + attestationType: AttestationType.PID, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE, AttributeSelectionMethod.ALL_ATTRIBUTES] + }] +} + +export const MDL_AUTHENTICATION_SCENARIO: PresentationScenario = { + name: "MDL Authentication", + attestations: [{ + attestationType: AttestationType.MDL, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE, AttributeSelectionMethod.ALL_ATTRIBUTES] + }] +} + +export const AGE_VERIFICATION_SCENARIO: PresentationScenario = { + name: "Age Verification", + attestations: [{ + attestationType: AttestationType.AGE_OVER_18, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE] + }, { + attestationType: AttestationType.PID, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE] + }] +} + +export const MULTIPLE_ATTESTATIONS_SCENARIO: PresentationScenario = { + name: "Multiple Attestations", + attestations: [{ + attestationType: AttestationType.AGE_OVER_18, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE, AttributeSelectionMethod.ALL_ATTRIBUTES] + }, { + attestationType: AttestationType.PHOTO_ID, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE, AttributeSelectionMethod.ALL_ATTRIBUTES] + }, { + attestationType: AttestationType.PID, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE, AttributeSelectionMethod.ALL_ATTRIBUTES] + }, { + attestationType: AttestationType.MDL, + attributeSelectionMethods: [AttributeSelectionMethod.SELECTABLE, AttributeSelectionMethod.ALL_ATTRIBUTES] + }] +} + +export const PRESENTATION_SCENARIOS: PresentationScenario[] = [ + PID_AUTHENTICATION_SCENARIO, + MDL_AUTHENTICATION_SCENARIO, + AGE_VERIFICATION_SCENARIO, + MULTIPLE_ATTESTATIONS_SCENARIO +] diff --git a/src/app/core/data/MsoMdocDocuments.ts b/src/app/core/data/MsoMdocDocuments.ts index e4da7a0..b5f7610 100644 --- a/src/app/core/data/MsoMdocDocuments.ts +++ b/src/app/core/data/MsoMdocDocuments.ts @@ -1,93 +1,37 @@ -import {MsoMdoc} from "@core/models/MsoMdoc"; +import {MsoMdocAttestation} from "@core/models/attestation/MsoMdocAttestation"; +import {AGE_OVER_18_ATTESTATION, MDL_ATTESTATION, PHOTO_ID_ATTESTATION, PID_ATTESTATION} from "@core/constants/attestations"; /* eslint-disable quotes */ -export const MDL_MSO_MDOC: MsoMdoc = { - name: 'Mobile Driving Licence', +export const MDL_MSO_MDOC: MsoMdocAttestation = { + attestation: MDL_ATTESTATION, doctype: 'org.iso.18013.5.1.mDL', - namespace: 'org.iso.18013.5.1', - attributes: [ - { value: 'family_name', text: 'Family name' }, - { value: 'given_name', text: 'Given name'}, - { value: 'birth_date', text: 'Birthdate'}, - { value: 'issue_date', text: 'Issue date'}, - { value: 'expiry_date', text: 'Expiry date'}, - { value: 'age_over_18', text: 'Age over 18'}, - { value: 'age_over_21', text: 'Age over 21'}, - { value: 'age_in_years', text: 'Age in years'}, - { value: 'age_birth_year', text: 'Age birth year'}, - { value: 'issuing_authority', text: 'Issuing authority'}, - { value: 'document_number', text: 'Document number'}, - { value: 'portrait', text: 'Portrait'}, - { value: 'driving_privileges', text: 'Driving privileges'}, - { value: 'un_distinguishing_sign', text: 'Un-distinguishing sign'}, - { value: 'administrative_number', text: 'Administrative number'}, - { value: 'sex', text: 'Sex'}, - { value: 'height', text: 'Height'}, - { value: 'weight', text: 'Weight'}, - { value: 'eye_colour', text: 'Eye colour'}, - { value: 'hair_colour', text: 'Hair colour'}, - { value: 'birth_place', text: 'Birth place'}, - { value: 'resident_address', text: 'Resident address'}, - { value: 'portrait_capture_date', text: 'Portrait capture date'}, - { value: 'nationality', text: 'Nationality'}, - { value: 'resident_city', text: 'Resident city'}, - { value: 'resident_state', text: 'Resident state'}, - { value: 'resident_postal_code', text: 'Resident postal code'}, - { value: 'resident_country', text: 'Resident country'}, - { value: 'family_name_national_character', text: 'Family name national character'}, - { value: 'given_name_national_character', text: 'Given name national character'}, - { value: 'signature_usual_mark', text: 'Signature usual mark'}] + namespace: 'org.iso.18013.5.1' } /* eslint-disable quotes */ -export const PID_MSO_MDOC: MsoMdoc = { - name: 'EUDI PID', +export const PID_MSO_MDOC: MsoMdocAttestation = { + attestation: PID_ATTESTATION, doctype: 'eu.europa.ec.eudi.pid.1', - namespace: 'eu.europa.ec.eudi.pid.1', - attributes: [ - { value: 'family_name', text: 'Family name'}, - { value: 'given_name', text: 'Given name'}, - { value: 'birth_date', text: 'Birthdate'}, - { value: 'age_over_18', text: 'Age over 18'}, - { value: 'age_in_years', text: 'Age in years'}, - { value: 'age_birth_year', text: 'Age birth year'}, - { value: 'family_name_birth', text: 'Family name birth'}, - { value: 'given_name_birth', text: 'Given name birth'}, - { value: 'birth_place', text: 'Birth place'}, - { value: 'birth_country', text: 'Birth country'}, - { value: 'birth_state', text: 'Birth state'}, - { value: 'birth_city', text: 'Birth city'}, - { value: 'resident_address', text: 'Resident address'}, - { value: 'resident_country', text: 'Resident country'}, - { value: 'resident_state', text: 'Resident state'}, - { value: 'resident_city', text: 'Resident city'}, - { value: 'resident_postal_code', text: 'Resident postal code'}, - { value: 'resident_street', text: 'Resident street'}, - { value: 'resident_house_number', text: 'Resident house number'}, - { value: 'gender', text: 'Gender'}, - { value: 'nationality', text: 'Nationality'}, - { value: 'issuance_date', text: 'Issuance date'}, - { value: 'expiry_date', text: 'Expiry date'}, - { value: 'issuing_authority', text: 'Issuing authority'}, - { value: 'document_number', text: 'Document number'}, - { value: 'administrative_number', text: 'Administrative number'}, - { value: 'issuing_country', text: 'Issuing country'}, - { value: 'issuing_jurisdiction', text: 'Issuing jurisdiction'} - ] + namespace: 'eu.europa.ec.eudi.pid.1' } /* eslint-disable quotes */ -export const AGE_OVER_18_MSO_MDOC: MsoMdoc = { - name: 'Age Over 18 Attestation', +export const AGE_OVER_18_MSO_MDOC: MsoMdocAttestation = { + attestation: AGE_OVER_18_ATTESTATION, doctype: 'eu.europa.ec.eudi.pseudonym.age_over_18.1', - namespace: 'eu.europa.ec.eudi.pseudonym.age_over_18.1', - attributes: [ - { value: 'age_over_18', text: 'Age over 18'}, - { value: 'user_pseudonym', text: 'User pseudonym'}, - { value: 'issuance_date', text: 'Issuance date'}, - { value: 'expiry_date', text: 'Expiry date'}, - { value: 'issuing_authority', text: 'Issuing authority'}, - { value: 'issuing_jurisdiction', text: 'Issuing jurisdiction'}, - { value: 'issuing_country', text: 'Issuing country'} - ] + namespace: 'eu.europa.ec.eudi.pseudonym.age_over_18.1' +} + +/* eslint-disable quotes */ +export const PHOTO_ID_MSO_MDOC: MsoMdocAttestation = { + attestation: PHOTO_ID_ATTESTATION, + doctype: 'org.iso.23220.2.photoid.1', + namespace: 'org.iso.23220.2.photoid.1' +} + +export const MSO_MDOC_BY_TYPE: { [id: string]: MsoMdocAttestation } = { + "pid": PID_MSO_MDOC, + "mdl": MDL_MSO_MDOC, + "photo_id": PHOTO_ID_MSO_MDOC, + "age_over_18": AGE_OVER_18_MSO_MDOC, } diff --git a/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.html b/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.html index d482a64..af1f2d6 100644 --- a/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.html +++ b/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.html @@ -9,4 +9,7 @@ + diff --git a/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.ts b/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.ts index ae361b4..206482d 100644 --- a/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.ts +++ b/src/app/core/layout/wallet-layout/wallet-layout-header/wallet-layout-header.component.ts @@ -6,6 +6,7 @@ import { MatMenuModule } from "@angular/material/menu"; import { MatToolbarModule } from '@angular/material/toolbar'; import { OpenLogsComponent } from "@shared/elements/open-logs/open-logs.component"; import { MatDialog, MatDialogModule } from "@angular/material/dialog"; +import {InputSchemeComponent} from "@shared/elements/input-scheme/input-scheme.component"; @Component({ selector: 'vc-wallet-layout-header', @@ -38,4 +39,14 @@ export class WalletLayoutHeaderComponent { }, }); } + + changeCustomScheme () { + this.dialog.open(InputSchemeComponent, { + data: { + transactionId: '', + label: 'Inspect transaction logs', + isInspectLogs: true + }, + }); + } } diff --git a/src/app/core/models/MsoMdoc.ts b/src/app/core/models/MsoMdoc.ts deleted file mode 100644 index 005542d..0000000 --- a/src/app/core/models/MsoMdoc.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type MsoMdoc = { - name: string; - doctype: string, - namespace: string, - attributes: Attribute[] -} - -export type Attribute = { - value: string, - text: string -} diff --git a/src/app/core/models/attestation/Attestation.ts b/src/app/core/models/attestation/Attestation.ts new file mode 100644 index 0000000..3c5122d --- /dev/null +++ b/src/app/core/models/attestation/Attestation.ts @@ -0,0 +1,13 @@ +import {AttestationType} from "@core/models/attestation/AttestationType"; + +export type Attestation = { + type: AttestationType, + name: string, + dataSet: DataElement[] +} + +export type DataElement = { + identifier: string, + attribute: string, + description?: string, +} diff --git a/src/app/core/models/AttestationFormat.ts b/src/app/core/models/attestation/AttestationFormat.ts similarity index 100% rename from src/app/core/models/AttestationFormat.ts rename to src/app/core/models/attestation/AttestationFormat.ts diff --git a/src/app/core/models/attestation/AttestationType.ts b/src/app/core/models/attestation/AttestationType.ts new file mode 100644 index 0000000..54f1c44 --- /dev/null +++ b/src/app/core/models/attestation/AttestationType.ts @@ -0,0 +1,6 @@ +export enum AttestationType { + PID = "pid", + MDL = "mdl", + PHOTO_ID = "photo_id", + AGE_OVER_18 = "age_over_18" +} diff --git a/src/app/core/models/attestation/MsoMdocAttestation.ts b/src/app/core/models/attestation/MsoMdocAttestation.ts new file mode 100644 index 0000000..78e01b1 --- /dev/null +++ b/src/app/core/models/attestation/MsoMdocAttestation.ts @@ -0,0 +1,7 @@ +import {Attestation} from "@core/models/attestation/Attestation"; + +export type MsoMdocAttestation = { + doctype: string, + namespace: string, + attestation: Attestation +} diff --git a/src/app/core/models/presentation/InputDescriptors.ts b/src/app/core/models/presentation/InputDescriptor.ts similarity index 88% rename from src/app/core/models/presentation/InputDescriptors.ts rename to src/app/core/models/presentation/InputDescriptor.ts index 8937059..e995891 100644 --- a/src/app/core/models/presentation/InputDescriptors.ts +++ b/src/app/core/models/presentation/InputDescriptor.ts @@ -1,6 +1,6 @@ import { Constraint } from './Constraint'; -export type InputDescriptors = { +export type InputDescriptor = { id: string, name?: string, purpose?: string, diff --git a/src/app/core/models/presentation/PresentationDefinition.ts b/src/app/core/models/presentation/PresentationDefinition.ts index 8bb4e20..4ade7d2 100644 --- a/src/app/core/models/presentation/PresentationDefinition.ts +++ b/src/app/core/models/presentation/PresentationDefinition.ts @@ -1,6 +1,6 @@ -import { InputDescriptors } from './InputDescriptors'; +import { InputDescriptor } from './InputDescriptor'; export type PresentationDefinition = { id: string, - input_descriptors: InputDescriptors[] + input_descriptors: InputDescriptor[] } diff --git a/src/app/core/models/presentation/SharedAttestation.ts b/src/app/core/models/presentation/SharedAttestation.ts index b258522..79241e2 100644 --- a/src/app/core/models/presentation/SharedAttestation.ts +++ b/src/app/core/models/presentation/SharedAttestation.ts @@ -1,5 +1,5 @@ import {KeyValue} from "@angular/common"; -import {AttestationFormat} from "@core/models/AttestationFormat"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; export type SharedAttestation = Single | Enveloped diff --git a/src/app/core/services/attestation-selectable-model.service.ts b/src/app/core/services/attestation-selectable-model.service.ts index 9e81083..3688fcd 100644 --- a/src/app/core/services/attestation-selectable-model.service.ts +++ b/src/app/core/services/attestation-selectable-model.service.ts @@ -1,14 +1,14 @@ import {Injectable} from "@angular/core"; import {PID_MSO_MDOC} from '@core/data/MsoMdocDocuments'; import {MDL_MSO_MDOC} from '@core/data/MsoMdocDocuments'; -import {MsoMdoc} from "@core/models/MsoMdoc"; +import {MsoMdocAttestation} from "@core/models/attestation/MsoMdocAttestation"; @Injectable({ providedIn: 'root' }) export class AttestationSelectableModelService { - private selectableModel: MsoMdoc | null = null; + private selectableModel: MsoMdocAttestation | null = null; private presentationPurpose!: string; setPresentationPurpose(presentationPurpose: string) { @@ -23,7 +23,7 @@ export class AttestationSelectableModelService { } } - getModel(): MsoMdoc { + getModel(): MsoMdocAttestation { return JSON.parse(JSON.stringify(this.selectableModel)); } diff --git a/src/app/core/services/decoders-registry.service.ts b/src/app/core/services/decoders-registry.service.ts index 517e267..9118103 100644 --- a/src/app/core/services/decoders-registry.service.ts +++ b/src/app/core/services/decoders-registry.service.ts @@ -1,5 +1,5 @@ import {Injectable} from "@angular/core"; -import {AttestationFormat} from "@core/models/AttestationFormat"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; import {AttestationDecoder} from "@core/services/decoders/AttestationDecoder"; import {MsoMdocAttestationDecoder} from "@core/services/decoders/MsoMdocAttestationDecoder"; import {JwtVcJsonAttestationDecoder} from "@core/services/decoders/JwtVcJsonAttestationDecoder"; diff --git a/src/app/core/services/decoders/AttestationDecoder.ts b/src/app/core/services/decoders/AttestationDecoder.ts index bc5aef0..f0d3844 100644 --- a/src/app/core/services/decoders/AttestationDecoder.ts +++ b/src/app/core/services/decoders/AttestationDecoder.ts @@ -1,4 +1,4 @@ -import {AttestationFormat} from "@core/models/AttestationFormat"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; import {SharedAttestation} from "@core/models/presentation/SharedAttestation"; export interface AttestationDecoder { diff --git a/src/app/core/services/decoders/DecodingUtils.ts b/src/app/core/services/decoders/DecodingUtils.ts new file mode 100644 index 0000000..2788be3 --- /dev/null +++ b/src/app/core/services/decoders/DecodingUtils.ts @@ -0,0 +1,25 @@ + +export function elementAsString(element: any, prepend?: string): string { + if ((typeof element) === "object") { + + if (Array.isArray(element)) { + return (element as string[]).map((it) => { + return JSON.stringify(it); + }).join(', ') + + } else { + let str = "" + if (typeof prepend !== 'undefined') { + str += "
" + } else { + prepend = "" + } + return str + Object.keys(element).map((it) => { + return prepend + "  " + it + ": " + elementAsString(element[it], "  ").toString() + }).join("
"); + } + + } else { + return element.toString(); + } +} diff --git a/src/app/core/services/decoders/JwtVcJsonAttestationDecoder.ts b/src/app/core/services/decoders/JwtVcJsonAttestationDecoder.ts index a69799c..6e23aba 100644 --- a/src/app/core/services/decoders/JwtVcJsonAttestationDecoder.ts +++ b/src/app/core/services/decoders/JwtVcJsonAttestationDecoder.ts @@ -1,9 +1,10 @@ import {Injectable} from "@angular/core"; import {AttestationDecoder} from "@core/services/decoders/AttestationDecoder"; import {SharedAttestation, Single} from "@core/models/presentation/SharedAttestation"; -import {AttestationFormat} from "@core/models/AttestationFormat"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; import {JWTService} from "@core/services/jwt.service"; import {KeyValue} from "@angular/common"; +import {elementAsString} from "@core/services/decoders/DecodingUtils"; const TYPE_VerifiableAttestation = "VerifiableAttestation" const TYPE_VerifiableCredential = "VerifiableCredential" @@ -64,7 +65,7 @@ export class JwtVcJsonAttestationDecoder implements AttestationDecoder { Object.keys(credentialSubject).forEach((item) => { result.push({ key: item.replaceAll('_', ' '), - value: this.asString(credentialSubject[item]) + value: elementAsString(credentialSubject[item]) }); }); return result; @@ -77,35 +78,11 @@ export class JwtVcJsonAttestationDecoder implements AttestationDecoder { if (item !== "credentialSubject") { result.push({ key: item.replaceAll('_', ' '), - value: this.asString(vcElement[item]) + value: elementAsString(vcElement[item]) }); } }); return result; } - asString(element: any, prepend?: string): string { - if ((typeof element) === "object") { - - if (Array.isArray(element)) { - return (element as string[]).map((it) => { - return JSON.stringify(it); - }).join(', ') - - } else { - let str = "" - if (typeof prepend !== 'undefined') { - str += "
" - } else { - prepend = "" - } - return str + Object.keys(element).map((it) => { - return prepend + "  " + it + ": " + this.asString(element[it], "  ").toString() - }).join("
"); - } - - } else { - return element.toString(); - } - } } diff --git a/src/app/core/services/decoders/MsoMdocAttestationDecoder.ts b/src/app/core/services/decoders/MsoMdocAttestationDecoder.ts index 24e4550..ae3ed28 100644 --- a/src/app/core/services/decoders/MsoMdocAttestationDecoder.ts +++ b/src/app/core/services/decoders/MsoMdocAttestationDecoder.ts @@ -1,11 +1,12 @@ import {Injectable} from "@angular/core"; import {AttestationDecoder} from "@core/services/decoders/AttestationDecoder"; -import {AttestationFormat} from "@core/models/AttestationFormat"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; import {decode} from "cbor-x"; import {Buffer} from 'buffer'; import {SharedAttestation, Single} from "@core/models/presentation/SharedAttestation"; import {KeyValue} from "@angular/common"; +import {elementAsString} from "@core/services/decoders/DecodingUtils"; @Injectable({ providedIn: 'root' @@ -42,7 +43,7 @@ export class MsoMdocAttestationDecoder implements AttestationDecoder { const decodedElement = this.decodeCborData(element.value); attributes.push({ key: it + ":" + decodedElement.elementIdentifier, - value: this.asString(decodedElement.elementValue) + value: elementAsString(decodedElement.elementValue) }); } @@ -74,31 +75,4 @@ export class MsoMdocAttestationDecoder implements AttestationDecoder { } } - asString(element: any, prepend?: string): string { - if ((typeof element) === "object") { - - if (Array.isArray(element)) { - return (element as string[]).map((it) => { - return JSON.stringify(it); - }).join(', ') - - } else { - let str = "" - if (typeof prepend !== 'undefined') { - str += "
" - } else { - prepend = "" - } - return str + Object.keys(element).map((it) => { - return prepend + "  " + it + ": " + this.asString(element[it], "  ").toString() - }).join("
"); - } - - } else { - return element.toString(); - } - } - - - } diff --git a/src/app/core/services/mso-mdoc-presentation.service.ts b/src/app/core/services/mso-mdoc-presentation.service.ts index ed76ca2..bf3be1c 100644 --- a/src/app/core/services/mso-mdoc-presentation.service.ts +++ b/src/app/core/services/mso-mdoc-presentation.service.ts @@ -1,9 +1,11 @@ import {Injectable} from "@angular/core"; -import {Attribute, MsoMdoc} from "@core/models/MsoMdoc"; +import {MsoMdocAttestation} from "@core/models/attestation/MsoMdocAttestation"; import {TransactionInitializationRequest} from "@core/models/TransactionInitializationRequest"; -import {FieldConstraint} from "@core/models/presentation/FieldConstraint"; import {v4 as uuidv4} from 'uuid'; import {AGE_OVER_18_MSO_MDOC, MDL_MSO_MDOC, PID_MSO_MDOC} from "@core/data/MsoMdocDocuments"; +import {InputDescriptor} from "@core/models/presentation/InputDescriptor"; +import {FieldConstraint} from "@core/models/presentation/FieldConstraint"; +import {DataElement} from "@core/models/attestation/Attestation"; @Injectable({ providedIn: 'root' @@ -31,40 +33,51 @@ export class MsoMdocPresentationService { } presentationOf( - document: MsoMdoc, presentationPurpose: string, - includeAttributes?: string[] + document: MsoMdocAttestation, + presentationPurpose: string, + includeAttributes?: string[] ): TransactionInitializationRequest { return { type: 'vp_token', presentation_definition: { id: uuidv4(), - input_descriptors: [{ - id: document.doctype, - name: document.name, - purpose: presentationPurpose, - format: { - 'mso_mdoc': { - 'alg': [ - "ES256", - "ES384", - "ES512" - ] - } - }, - constraints: { - fields: this.fieldConstraints(document, includeAttributes) - } - }] + input_descriptors: [ + this.msoMdocInputDescriptorOf(document, presentationPurpose, includeAttributes) + ] }, nonce: uuidv4() }; } - fieldConstraints(document: MsoMdoc, includeAttributes?: string[]): FieldConstraint[] { + msoMdocInputDescriptorOf( + document: MsoMdocAttestation, + presentationPurpose: string, + includeAttributes?: string[] + ): InputDescriptor { + return { + id: document.doctype, + name: document.attestation.name, + purpose: presentationPurpose, + format: { + mso_mdoc: { + alg: [ + "ES256", + "ES384", + "ES512" + ] + } + }, + constraints: { + fields: this.msoMdocFieldConstraints(document, includeAttributes) + } + }; + } + + msoMdocFieldConstraints(document: MsoMdocAttestation, includeAttributes?: string[]): FieldConstraint[] { const fieldConstraints: FieldConstraint[] = []; - document.attributes.forEach((attribute: Attribute) => { - if (typeof includeAttributes == 'undefined' || includeAttributes.includes(attribute.value)) { - fieldConstraints.push(this.fieldConstraint(document.namespace, attribute.value)); + document.attestation.dataSet.forEach((dataElement: DataElement) => { + if (typeof includeAttributes == 'undefined' || includeAttributes.includes(dataElement.identifier)) { + fieldConstraints.push(this.fieldConstraint(document.namespace, dataElement.identifier)); } }) return fieldConstraints; @@ -81,4 +94,5 @@ export class MsoMdocPresentationService { } } + } diff --git a/src/app/core/services/verifier-endpoint.service.ts b/src/app/core/services/verifier-endpoint.service.ts index ae4a3ee..e0bc245 100644 --- a/src/app/core/services/verifier-endpoint.service.ts +++ b/src/app/core/services/verifier-endpoint.service.ts @@ -3,7 +3,7 @@ import {Observable} from 'rxjs'; import {map, tap} from 'rxjs/operators'; import {HttpService} from '@network/http/http.service'; import {LocalStorageService} from './local-storage.service'; -import * as constants from '@core/constants/constants'; +import * as constants from '@core/constants/general'; import {DeviceDetectorService} from './device-detector.service'; import {TransactionInitializationRequest} from "@core/models/TransactionInitializationRequest"; import {InitializedTransaction} from "@core/models/InitializedTransaction"; diff --git a/src/app/features/home/components/home/home.component.html b/src/app/features/home/components/home/home.component.html deleted file mode 100644 index 7c9b28b..0000000 --- a/src/app/features/home/components/home/home.component.html +++ /dev/null @@ -1,34 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
diff --git a/src/app/features/home/components/home/home.component.scss b/src/app/features/home/components/home/home.component.scss deleted file mode 100644 index 83ffe96..0000000 --- a/src/app/features/home/components/home/home.component.scss +++ /dev/null @@ -1,83 +0,0 @@ -@use '/src/template' as temp; -@use '/src/layout-breakpoint' as points; - -:host { - .container { - max-width: temp.$max-body-panel; - margin-top: 5rem; - } - .container { - .show-logs { - display: flex; - margin-bottom: 1em; - .log-button { - display: flex; - justify-content: center; - } - } - ::ng-deep { - .mdc-tab { - background: var(--background-paper) 0% 0% no-repeat padding-box; - box-shadow: 0px -3px 6px #00000029; - border-radius: 22px 22px 0px 0px; - opacity: 1; - margin-right: 5px; - } - .mdc-tab--active::after { - content: ""; - position: absolute; - width: 12px; - height: 12px; - background: #60E087 0% 0% no-repeat padding-box; - opacity: 1; - top: 17px; - left: 9px; - border-radius: 12px; - } - .mdc-tab--active { - background: #FAFAFA 0% 0% no-repeat padding-box; - } - .mat-mdc-tab-body-wrapper { - background: #FAFAFA 0% 0% no-repeat padding-box; - box-shadow: 0px 0px 6px #00000029; - border-radius: 0px 22px 22px 22px; - opacity: 1; - padding: 15px; - min-width: 550px; - } - .mdc-tab-indicator { - display: none; - } - .mat-mdc-tab-list { - margin: 10px 0 -5px 0px; - display: ruby; - } - .mdc-tab__text-label { - color: #000000DE; - font-weight: 500; - } - .mat-mdc-tab-ripple, - .mdc-tab__ripple { - border-radius: 22px 22px 0px 0px; - } - .mat-mdc-tab-labels:first-child { - margin-left: 4px; - } - @media (max-width: map-get(points.$breakpoints, xs)) and (min-width: map-get(points.$breakpoints, sm)){ - .mat-mdc-tab-body-wrapper { - min-width: unset; - } - } - @media (max-width: map-get(points.$breakpoints, sm)) { - .mat-mdc-tab-body-wrapper { - min-width: unset; - } - } - @media (max-width: map-get(points.$breakpoints, md)) and (min-width: map-get(points.$breakpoints, sm)){ - .mat-mdc-tab-body-wrapper { - min-width: unset; - } - } - } - } -} diff --git a/src/app/features/home/components/home/home.component.spec.ts b/src/app/features/home/components/home/home.component.spec.ts deleted file mode 100644 index f8de9b1..0000000 --- a/src/app/features/home/components/home/home.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HomeComponent } from './home.component'; -import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; -import { SharedModule } from '@app/shared/shared.module'; -import { HomeService } from '../../services/home.service'; -import { VerifierEndpointService } from '@core/services/verifier-endpoint.service'; -import { DataService } from '@app/core/services/data.service'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -describe('CBOR HomeComponent', () => { - let component: HomeComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - // declarations: [ HomeComponent ] - imports: [ - SharedModule, WalletLayoutComponent, HttpClientModule,BrowserAnimationsModule, NoopAnimationsModule - ], - providers: [ - // HttpService - DataService, - VerifierEndpointService, - HomeService - ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(HomeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/home/components/home/home.component.ts b/src/app/features/home/components/home/home.component.ts deleted file mode 100644 index 4caa452..0000000 --- a/src/app/features/home/components/home/home.component.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {ChangeDetectionStrategy, Component, inject, OnInit} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {DataService} from '@app/core/services/data.service'; -import {NavigateService} from '@app/core/services/navigate.service'; -import {VerifierEndpointService} from '@core/services/verifier-endpoint.service'; -import {RadioGroupComponent} from '@app/shared/elements/radio-group/radio-group.component'; -import {SharedModule} from '@app/shared/shared.module'; -import {HomeService} from '../../services/home.service'; -import {MenuOption} from '../../models/menu-option'; -import {WalletLayoutComponent} from '@app/core/layout/wallet-layout/wallet-layout.component'; -import {BodyAction} from '@app/shared/elements/body-actions/models/BodyAction'; -import {HOME_ACTIONS} from '@core/constants/pages-actions'; -import {LocalStorageService} from '@app/core/services/local-storage.service'; -import * as constants from '@core/constants/constants'; -import {InputSchemeComponent} from '../input-scheme/input-scheme.component'; -import {MatTabsModule} from '@angular/material/tabs'; -import {AttestationSelectableModelService} from '@app/core/services/attestation-selectable-model.service'; -import {OpenLogsComponent} from '@app/shared/elements/open-logs/open-logs.component'; -import {MatDialog, MatDialogModule} from '@angular/material/dialog'; -import {MsoMdocPresentationService} from "@core/services/mso-mdoc-presentation.service"; - -@Component({ - standalone: true, - imports: [ - CommonModule, - MatTabsModule, - RadioGroupComponent, - SharedModule, - InputSchemeComponent, - WalletLayoutComponent, - OpenLogsComponent, - MatDialogModule - ], - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'], - providers: [VerifierEndpointService, HomeService, MatDialog], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class HomeComponent implements OnInit { - - actions: BodyAction[] = HOME_ACTIONS; - optionsCustomRequest: MenuOption[] = []; - optionsPIDAuthentication: MenuOption[] = []; - optionsMDLAuthentication: MenuOption[] = []; - optionsAgeVerification: MenuOption[] = []; - - private dialog: MatDialog = inject(MatDialog); - - constructor( - private navigateService: NavigateService, - private readonly verifierEndpointService: VerifierEndpointService, - private readonly dataService: DataService, - private readonly attestationSelectableModelService: AttestationSelectableModelService, - private readonly homeService: HomeService, - private readonly localStorageService: LocalStorageService, - private readonly msoMdocPresentationService: MsoMdocPresentationService - ) { - this.localStorageService.remove(constants.ACTIVE_TRANSACTION); - } - - ngOnInit(): void { - this.optionsCustomRequest = this.homeService.optionsCustomRequest; - this.optionsPIDAuthentication = this.homeService.optionsPIDAuthentication; - this.optionsMDLAuthentication = this.homeService.optionsMDLAuthentication; - this.optionsAgeVerification = this.homeService.optionsAgeVerification; - } - - private navTarget = ''; - - setNavigateTarget(choose: string) { - if (choose === 'PID_full') { - this.navTarget = 'pid-full'; - } else if (choose === 'PID_Selectable') { - this.navTarget = 'selectable/pid-create'; - } else if (choose === 'AgeOver18_attestation') { - this.navTarget = 'age-attestation'; - } else if (choose === 'AgeOver18_pid') { - this.navTarget = 'pid-age-over-18'; - } else if (choose === 'MDL_Selectable') { - this.navTarget = 'selectable/mdl-create'; - } else if (choose === 'MDL_Full') { - this.navTarget = 'mdl-full'; - } else if (choose === 'PD_Custom_Request') { - this.navTarget = 'custom-request'; - } - this.actions = [...this.actions].map((item) => { - item.disabled = false; - return item; - }); - } - - submit() { - if (this.navTarget === 'pid-full') { - let presentationRequest = this.msoMdocPresentationService.presentationOfFullPid(); - this.verifierEndpointService.initializeTransaction(presentationRequest, (data) => { - this.navigateService.navigateTo('invoke-wallet'); - }); - - } else if (this.navTarget === 'selectable/pid-create') { - this.prepareDataForSelectableView('PID', 'We need to verify your identity') - this.navigateService.navigateTo('selectable/create'); - - } else if (this.navTarget === 'mdl-full') { - let presentationRequest = this.msoMdocPresentationService.presentationOfFullMdl(); - this.verifierEndpointService.initializeTransaction(presentationRequest, (data) => { - this.navigateService.navigateTo('invoke-wallet'); - }); - - } else if (this.navTarget === 'selectable/mdl-create') { - this.prepareDataForSelectableView('MDL', 'We need to verify your mobile driving licence') - this.navigateService.navigateTo('selectable/create'); - - } else if (this.navTarget === 'pid-age-over-18') { - let presentationRequest = this.msoMdocPresentationService.presentationOfPidOver18(); - this.verifierEndpointService.initializeTransaction(presentationRequest, (data) => { - this.navigateService.navigateTo('invoke-wallet'); - }); - - } else if (this.navTarget === 'age-attestation') { - let presentationRequest = this.msoMdocPresentationService.presentationOfAgeAttestationOver18(); - this.verifierEndpointService.initializeTransaction(presentationRequest, (data) => { - this.navigateService.navigateTo('invoke-wallet'); - }); - - } else if (this.navTarget === 'custom-request') { - this.navigateService.navigateTo('/custom-request'); - } - } - - prepareDataForSelectableView(selectableModel: string, presentationPurpose: string) { - this.attestationSelectableModelService.setPresentationPurpose(presentationPurpose); - this.attestationSelectableModelService.setModel(selectableModel); - } - - selectedIndexChange(_event: number) { - this.actions = [...this.actions].map((item) => { - item.disabled = true; - return item; - }); - } -} diff --git a/src/app/features/home/components/input-scheme/input-scheme.component.html b/src/app/features/home/components/input-scheme/input-scheme.component.html deleted file mode 100644 index afadc53..0000000 --- a/src/app/features/home/components/input-scheme/input-scheme.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Authorization endpoint scheme: {{schemeValue}} - - -
- - Scheme - - Please enter a valid scheme - scheme is required - -
- - - -
diff --git a/src/app/features/home/models/menu-option.ts b/src/app/features/home/models/menu-option.ts deleted file mode 100644 index 92870ce..0000000 --- a/src/app/features/home/models/menu-option.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type MenuOption = { - key: string, - value: string, - isDisabled: boolean, - checked?: boolean -} diff --git a/src/app/features/home/services/home.service.spec.ts b/src/app/features/home/services/home.service.spec.ts deleted file mode 100644 index e70a618..0000000 --- a/src/app/features/home/services/home.service.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { HomeService } from './home.service'; - -describe('HomeService', () => { - let service: HomeService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [HomeService] - }); - service = TestBed.inject(HomeService); - }); - - it('should be created', () => { - // @ts-ignore - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/features/home/services/home.service.ts b/src/app/features/home/services/home.service.ts deleted file mode 100644 index 5644df5..0000000 --- a/src/app/features/home/services/home.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MenuOption } from '../models/menu-option'; - -@Injectable() -export class HomeService { - - optionsPIDAuthentication: MenuOption[] = [ - { - key: 'PID_Selectable', - value: 'Request to share specific attributes from PID', - isDisabled: false, - }, - { - key: 'PID_full', - value: 'Request for the entire PID', - isDisabled: false, - } - ]; - optionsMDLAuthentication: MenuOption[] = [ - { - key: 'MDL_Selectable', - value: 'Request to share specific attributes from mDL', - isDisabled: false, - }, - { - key: 'MDL_Full', - value: 'Request for the entire mDL', - isDisabled: false, - } - ]; - optionsAgeVerification: MenuOption[] = [ - { - key: 'AgeOver18_attestation', - value: 'Age over 18 (age attestation)', - isDisabled: false, - }, - { - key: 'AgeOver18_pid', - value: 'Age over 18 (PID)', - isDisabled: false, - } - ]; - optionsCustomRequest: MenuOption[] = [ - { - key: 'PD_Custom_Request', - value: 'Custom request (for testing)', - isDisabled: false, - } - ]; -} diff --git a/src/app/features/invoke-wallet/components/qr-code/qr-code.component.html b/src/app/features/invoke-wallet/components/qr-code/qr-code.component.html index f91fe42..97d3056 100644 --- a/src/app/features/invoke-wallet/components/qr-code/qr-code.component.html +++ b/src/app/features/invoke-wallet/components/qr-code/qr-code.component.html @@ -1,17 +1,30 @@
-

Scan the QR code with your phone's camera app

+

Scan the + QR code + below with your phone

-
+

- OR -

- AUTHENTICATE WITH WALLET + OPEN WITH YOUR WALLET
View transaction logs diff --git a/src/app/features/invoke-wallet/components/qr-code/qr-code.component.ts b/src/app/features/invoke-wallet/components/qr-code/qr-code.component.ts index 677d112..a76def0 100644 --- a/src/app/features/invoke-wallet/components/qr-code/qr-code.component.ts +++ b/src/app/features/invoke-wallet/components/qr-code/qr-code.component.ts @@ -18,20 +18,27 @@ import {InitializedTransaction} from '@core/models/InitializedTransaction'; import {PresentationsResultsComponent} from '../presentations-results/presentations-results.component'; import {DeviceDetectorService} from '@core/services/device-detector.service'; import {LocalStorageService} from '@core/services/local-storage.service'; -import * as constants from '@core/constants/constants'; +import * as constants from '@core/constants/general'; import {MatDialog, MatDialogModule} from '@angular/material/dialog'; import {OpenLogsComponent} from '@shared/elements/open-logs/open-logs.component'; import {VerifierEndpointService} from "@core/services/verifier-endpoint.service"; import {WalletResponse} from "@core/models/WalletResponse"; import {ConcludedTransaction} from "@core/models/ConcludedTransaction"; +import { QRCodeModule } from 'angularx-qrcode'; +import {SafeUrl} from "@angular/platform-browser"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -declare let QRCode: any; @Component({ selector: 'vc-qr-code', standalone: true, - imports: [CommonModule, SharedModule, PresentationsResultsComponent, OpenLogsComponent, MatDialogModule], + imports: [ + CommonModule, + SharedModule, + PresentationsResultsComponent, + OpenLogsComponent, + MatDialogModule, + QRCodeModule + ], templateUrl: './qr-code.component.html', styleUrls: ['./qr-code.component.scss'], providers: [VerifierEndpointService], @@ -44,13 +51,13 @@ export class QrCodeComponent implements OnInit, OnDestroy { destroy$ = new Subject(); stopPlay$ = new ReplaySubject(1); - @ViewChild('qrCode') qrCode!: ElementRef; isCrossDevice = true; transaction!: InitializedTransaction; - redirectUrl!: string; + deepLinkTxt!: string; scheme!: string; + qrCodeDownloadLink!: SafeUrl; readonly dialog!: MatDialog; @Output() transactionConcludedEvent = new EventEmitter(); @@ -82,19 +89,18 @@ export class QrCodeComponent implements OnInit, OnDestroy { if (!this.transaction) { this.navigateService.goHome(); } else { - this.redirectUrl = this.buildQrCode(this.transaction); + this.deepLinkTxt = this.buildQrCode(this.transaction); if (this.isCrossDevice) { this.pollingRequest(this.transaction.transaction_id); } } } - ngAfterViewInit() { - if (this.isCrossDevice) { - new QRCode(this.qrCode.nativeElement, this.redirectUrl); - } + onChangeURL(url: SafeUrl) { + this.qrCodeDownloadLink = url; } + pollingRequest(transaction_id: string) { const source = interval(2000); source diff --git a/src/app/features/invoke-wallet/services/wallet-response-processor.service.ts b/src/app/features/invoke-wallet/services/wallet-response-processor.service.ts index dd49796..12b2f81 100644 --- a/src/app/features/invoke-wallet/services/wallet-response-processor.service.ts +++ b/src/app/features/invoke-wallet/services/wallet-response-processor.service.ts @@ -1,6 +1,6 @@ import {Injectable} from "@angular/core"; import {SharedAttestation} from "@core/models/presentation/SharedAttestation"; -import {AttestationFormat} from "@core/models/AttestationFormat"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; import {JSONPath} from "jsonpath-plus"; import {ConcludedTransaction} from "@core/models/ConcludedTransaction"; import {DecodersRegistryService} from "@core/services/decoders-registry.service"; diff --git a/src/app/features/presentation-request-preparation/components/attestation/attestation.component.html b/src/app/features/presentation-request-preparation/components/attestation/attestation.component.html new file mode 100644 index 0000000..148f337 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/attestation/attestation.component.html @@ -0,0 +1,31 @@ + + + + {{ nameOf(attestation) }} + + + +
+ + -- attributes by -- + + + {{ labelOf(method) }} + + + Please choose a method + + + + -- format -- + + + {{ format.value }} + + + Please choose an format + +
+ +
+ diff --git a/src/app/features/presentation-request-preparation/components/attestation/attestation.component.ts b/src/app/features/presentation-request-preparation/components/attestation/attestation.component.ts new file mode 100644 index 0000000..4a399d8 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/attestation/attestation.component.ts @@ -0,0 +1,83 @@ +import {Component, EventEmitter, Input, Output} from "@angular/core"; +import {CommonModule, KeyValue} from "@angular/common"; +import {SharedModule} from "@shared/shared.module"; +import {WalletLayoutComponent} from "@core/layout/wallet-layout/wallet-layout.component"; +import {MatInputModule} from "@angular/material/input"; +import {MatSelectModule} from "@angular/material/select"; +import {MatRadioModule} from "@angular/material/radio"; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {AttributeSelectionMethod, ScenarioAttestation} from "@features/presentation-request-preparation/models/ScenarioAttestation"; +import {SUPPORTED_ATTESTATIONS, SUPPORTED_FORMATS} from "@core/constants/attestations"; +import {MatExpansionModule} from "@angular/material/expansion"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; +import {AttestationSelection} from "@features/presentation-request-preparation/models/ScenarioSelection"; +import {FormatSelectOption} from "@features/presentation-request-preparation/components/attestation/model/format-select-option"; + +@Component({ + selector: 'vc-scenario-attestation', + standalone: true, + imports: [ + CommonModule, + SharedModule, + WalletLayoutComponent, + MatInputModule, + MatSelectModule, + MatRadioModule, + ReactiveFormsModule, + FormsModule, + MatExpansionModule, + ], + templateUrl: './attestation.component.html' +}) +export class AttestationComponent { + + @Input() attestation!: ScenarioAttestation; + @Output() attestationSelectionEvent = new EventEmitter(); + + protected readonly supportedFormats: FormatSelectOption[] = this.formatOptions() + + methodControl = new FormControl(null, Validators.required); + formatControl = new FormControl(null, Validators.required); + + selectedMethod: AttributeSelectionMethod | null = null; + selectedFormat: AttestationFormat | null = null; + + labelOf(method: AttributeSelectionMethod): string { + if (method == AttributeSelectionMethod.ALL_ATTRIBUTES) { + return "All attributes"; + } else { + return "Specific attributes"; + } + } + + formatOptions(): FormatSelectOption[] { + function enumKeys(obj: O): K[] { + return Object.keys(obj).filter(k => !Number.isNaN(k)) as K[] + } + + let result: FormatSelectOption[] = []; + for (const enumKey of enumKeys(AttestationFormat)) { + const format = AttestationFormat[enumKey]; + result.push({ + key: format, + value: format, + disabled: !SUPPORTED_FORMATS.includes(format) + }) + } + return result + } + + emit() { + this.attestationSelectionEvent.emit({ + type: this.attestation.attestationType, + format: this.selectedFormat, + attributeSelectionMethod: this.selectedMethod + }); + } + + nameOf(attestation: ScenarioAttestation): string { + return SUPPORTED_ATTESTATIONS[attestation.attestationType as string].name + } + + protected readonly frameElement = frameElement; +} diff --git a/src/app/features/presentation-request-preparation/components/attestation/model/format-select-option.ts b/src/app/features/presentation-request-preparation/components/attestation/model/format-select-option.ts new file mode 100644 index 0000000..4c3e5a3 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/attestation/model/format-select-option.ts @@ -0,0 +1,5 @@ +export type FormatSelectOption = { + key: string, + value: string, + disabled: boolean +} diff --git a/src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.html b/src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.html new file mode 100644 index 0000000..6f8105e --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.html @@ -0,0 +1,15 @@ + + + + {{ nameOf(selection.type) }} + in {{ selection.format }} format + + + + + + All Attributes + + +
+
diff --git a/src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.ts b/src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.ts new file mode 100644 index 0000000..cbd0ec0 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/attribute-selection/attribute-selection.component.ts @@ -0,0 +1,171 @@ +import {Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {SharedModule} from "@shared/shared.module"; +import {WalletLayoutComponent} from "@core/layout/wallet-layout/wallet-layout.component"; +import {AttestationSelection, ScenarioSelection} from "@features/presentation-request-preparation/models/ScenarioSelection"; +import {AttestationType} from "@core/models/attestation/AttestationType"; +import {SUPPORTED_ATTESTATIONS} from "@core/constants/attestations"; +import {AttributeSelectionMethod} from "@features/presentation-request-preparation/models/ScenarioAttestation"; +import {MatButtonModule} from "@angular/material/button"; +import {MatCardModule} from "@angular/material/card"; +import {MatDialog} from "@angular/material/dialog"; +import { + SelectableAttestationAttributesComponent +} from "@features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; +import {InputDescriptor} from "@core/models/presentation/InputDescriptor"; +import {DialogResult} from "@features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogResult"; +import {MSO_MDOC_BY_TYPE} from "@core/data/MsoMdocDocuments"; +import {MsoMdocPresentationService} from "@core/services/mso-mdoc-presentation.service"; +import {MatBadgeModule} from "@angular/material/badge"; + +@Component({ + selector: 'vc-attribute-selection', + standalone: true, + imports: [ + CommonModule, + SharedModule, + WalletLayoutComponent, + MatButtonModule, + MatCardModule, + MatBadgeModule, + ], + providers: [MsoMdocPresentationService], + templateUrl: './attribute-selection.component.html' +}) +export class AttributeSelectionComponent implements OnInit, OnChanges { + + constructor( + private readonly msoMdocPresentationService: MsoMdocPresentationService, + ) { + } + + @Input() scenarioSelection!: ScenarioSelection; + @Output() attributesCollectedEvent = new EventEmitter(); + + readonly dialog: MatDialog = inject(MatDialog); + + inputDescriptorsByType: { [id: string]: InputDescriptor } = {} + + ngOnInit(): void { + this.prepareDescriptorsForNonSelectable() + } + + prepareDescriptorsForNonSelectable() { + let allAttributesSelections = this.scenarioSelection.selections.filter((selection: AttestationSelection) => + selection.attributeSelectionMethod === AttributeSelectionMethod.ALL_ATTRIBUTES + ) + allAttributesSelections.forEach((selection: AttestationSelection) => { + switch (selection.format) { + case AttestationFormat.MSO_MDOC: + let msoMdoc = MSO_MDOC_BY_TYPE[selection.type as string]; + let inputDescriptor = this.msoMdocPresentationService.msoMdocInputDescriptorOf(msoMdoc, "") + this.inputDescriptorsByType[selection.type] = inputDescriptor; + break; + case AttestationFormat.SD_JWT_VC: + console.error("Format " + AttestationFormat.SD_JWT_VC + " not suppoerted yet"); + break; + case AttestationFormat.JWT_VC_JSON: + console.error("Format " + AttestationFormat.JWT_VC_JSON + " not suppoerted yet"); + break; + } + }) + } + + nameOf(attestationType: AttestationType) { + return SUPPORTED_ATTESTATIONS[attestationType as string].name; + } + + isSelectable(selection: AttestationSelection) { + return selection.attributeSelectionMethod == AttributeSelectionMethod.SELECTABLE; + } + + popAttributesSelector(format: AttestationFormat, type: AttestationType, attestationName: string) { + const dialogRef = this.dialog.open(SelectableAttestationAttributesComponent, { + data: { + type: type, + format: format, + attestationName: attestationName, + seed: this.inputDescriptorsByType[type as string] + } + }); + dialogRef.afterClosed().subscribe(result => { + // result can be null or undefined if popup is closed without saving selection (clicking on 'close' button or focus lost) + if (result) { + this.updateInputDescriptorsDictionary(result.data as DialogResult); + } + }); + } + + updateInputDescriptorsDictionary(dialogResult: DialogResult) { + if (dialogResult.inputDescriptor) { + this.inputDescriptorsByType[dialogResult.attestationType as string] = dialogResult.inputDescriptor; + } else { + delete this.inputDescriptorsByType[dialogResult.attestationType as string] + } + this.emitAttributesCollectedEvent(); + } + + emitAttributesCollectedEvent() { + let result: InputDescriptor[] = []; + Object.keys(this.inputDescriptorsByType).forEach((item) => { + result.push(this.inputDescriptorsByType[item]); + }); + this.attributesCollectedEvent.emit(result); + } + + ngOnChanges(changes: SimpleChanges): void { + if (!changes["scenarioSelection"].firstChange) { + let currentSelection = changes["scenarioSelection"].currentValue as ScenarioSelection; + let previousSelection = changes["scenarioSelection"].previousValue as ScenarioSelection; + if (currentSelection.scenarioName !== previousSelection.scenarioName) { + console.log("scenario changed...re-setting everything") + this.inputDescriptorsByType = {}; + } else { + Object.keys(this.inputDescriptorsByType).forEach((item) => { + if (this.attributeSelectionChanged(currentSelection, previousSelection, item as AttestationType)) { + delete this.inputDescriptorsByType[item]; + } + }); + } + } + this.prepareDescriptorsForNonSelectable(); + this.emitAttributesCollectedEvent(); + } + + fieldsSelectedNo(type: AttestationType): number { + if (this.inputDescriptorsByType[type as string]) + return this.inputDescriptorsByType[type as string].constraints.fields.length; + else + return 0; + } + + canShowFieldsSelected(type: AttestationType): boolean { + if (this.inputDescriptorsByType[type as string]) + return this.inputDescriptorsByType[type as string].constraints.fields.length > 0; + else + return false; + } + + private attributeSelectionChanged( + currentScenario: ScenarioSelection, + previousScenario: ScenarioSelection, + attestationType: AttestationType + ): boolean { + let current = currentScenario.selections.filter((selection: AttestationSelection) => + selection.type === attestationType + ) + let previous = previousScenario.selections.filter((selection: AttestationSelection) => + selection.type === attestationType + ) + // Previously existed and now removed + if (previous && previous.length >= 1 && (!current || current.length == 0)) { + return true + } + // Selection method or format changed + if (previous && previous.length >= 1 && current && current.length >= 1) { + return previous[0].attributeSelectionMethod != current[0].attributeSelectionMethod || previous[0].format != current[0].format; + } + return false; + } +} diff --git a/src/app/features/presentation-request-preparation/components/scenario/scenario.component.html b/src/app/features/presentation-request-preparation/components/scenario/scenario.component.html new file mode 100644 index 0000000..d3a2eaf --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/scenario/scenario.component.html @@ -0,0 +1,23 @@ +
+
+ + + select scenario + + + {{ scenario.name }} + + + +
+ + +
+ + + + + +
+
+
diff --git a/src/app/features/presentation-request-preparation/components/scenario/scenario.component.ts b/src/app/features/presentation-request-preparation/components/scenario/scenario.component.ts new file mode 100644 index 0000000..9f38ef1 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/scenario/scenario.component.ts @@ -0,0 +1,83 @@ +import {CommonModule} from '@angular/common'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {SharedModule} from "@shared/shared.module"; +import {PresentationScenario} from "@features/presentation-request-preparation/models/PresentationScenario"; +import {PRESENTATION_SCENARIOS} from "@core/constants/presentation-scenarios"; +import {MatInputModule} from "@angular/material/input"; +import {MatSelectModule} from "@angular/material/select"; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {WalletLayoutComponent} from "@core/layout/wallet-layout/wallet-layout.component"; +import {MatRadioModule} from "@angular/material/radio"; +import {AttestationComponent} from "@features/presentation-request-preparation/components/attestation/attestation.component"; +import {MatExpansionModule} from "@angular/material/expansion"; +import {AttestationSelection, ScenarioSelection} from "@features/presentation-request-preparation/models/ScenarioSelection"; + +@Component({ + selector: 'vc-presentation-scenario', + standalone: true, + imports: [ + CommonModule, + SharedModule, + WalletLayoutComponent, + MatInputModule, + MatSelectModule, + MatRadioModule, + ReactiveFormsModule, + FormsModule, + AttestationComponent, + MatExpansionModule, + ], + templateUrl: './scenario.component.html' +}) +export class ScenarioComponent implements OnInit { + + @Output() selectionChangedEvent = new EventEmitter(); + + scenarios!: PresentationScenario[]; + selectedScenario: PresentationScenario | null = null; + attestationSelections: {[id: string]: AttestationSelection} = {}; + + scenarioFormControl = + new FormControl('', Validators.required); + + ngOnInit(): void { + this.scenarios = Object.assign([], PRESENTATION_SCENARIOS); + } + + handleAttestationSelectionEvent($event: AttestationSelection) { + if ($event.format != null && $event.attributeSelectionMethod != null) { + if (this.newSelectionOrAttestationSelectionChanged($event)) { + this.attestationSelections[$event.type as string] = $event; + this.selectionChangedEvent.emit( this.constructScenarioSelection() ) + } + } else { + delete this.attestationSelections[$event.type as string]; + this.selectionChangedEvent.emit( this.constructScenarioSelection() ) + } + } + + newSelectionOrAttestationSelectionChanged($event: AttestationSelection): boolean { + let attestationSelection = this.attestationSelections[$event.type as string]; + return !attestationSelection || (attestationSelection && + (attestationSelection.format != $event.format || + attestationSelection.attributeSelectionMethod != $event.attributeSelectionMethod)) + } + + constructScenarioSelection(): ScenarioSelection { + let selections: AttestationSelection[] = []; + Object.keys(this.attestationSelections).forEach((item ) => { + selections.push(this.attestationSelections[item]) + }) + return { + scenarioName: this.selectedScenario!.name, + selections: selections + } + } + + clearSelections() { + this.attestationSelections = {} + this.selectionChangedEvent.emit( + this.constructScenarioSelection() + ) + } +} diff --git a/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogData.ts b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogData.ts new file mode 100644 index 0000000..82c1c65 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogData.ts @@ -0,0 +1,10 @@ +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; +import {AttestationType} from "@core/models/attestation/AttestationType"; +import {InputDescriptor} from "@core/models/presentation/InputDescriptor"; + +export interface DialogData { + type: AttestationType, + format: AttestationFormat, + attestationName: string, + seed?: InputDescriptor +} diff --git a/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogResult.ts b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogResult.ts new file mode 100644 index 0000000..cebe8be --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogResult.ts @@ -0,0 +1,7 @@ +import {AttestationType} from "@core/models/attestation/AttestationType"; +import {InputDescriptor} from "@core/models/presentation/InputDescriptor"; + +export interface DialogResult { + attestationType: AttestationType, + inputDescriptor: InputDescriptor +} diff --git a/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.html b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.html new file mode 100644 index 0000000..d79ec2e --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.html @@ -0,0 +1,34 @@ +

{{ data.attestationName }}

+ + + +
+
+
+

Select attributes of the attestation to be included in the request

+ {{ selectedFields.length }} selected +
+ +
+ + {{ field.label }} + +
+
+
+
+ + + + Input descriptor + + + + + +
+ + + + + diff --git a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.scss similarity index 97% rename from src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss rename to src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.scss index 1630943..d8a0893 100644 --- a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss +++ b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.scss @@ -1,5 +1,5 @@ @use '/src/template' as temp; -@use '/src//layout-breakpoint' as points; +@use '/src/layout-breakpoint' as points; :host { max-width: 40rem; diff --git a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.spec.ts similarity index 76% rename from src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts rename to src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.spec.ts index b6d95c1..f71b0d3 100644 --- a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts +++ b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; import { RouterModule } from '@angular/router'; import { SharedModule } from '@app/shared/shared.module'; -import { SelectablePresentationFormComponent } from './selectable-presentation-form.component'; +import { SelectableAttestationAttributesComponent } from './selectable-attestation-attributes.component'; import { HttpClientModule } from '@angular/common/http'; import { SelectableFormNextAction } from '../../services/selectable-form-next-action.service'; import { MatExpansionModule } from '@angular/material/expansion'; @@ -11,8 +11,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatCheckboxModule } from '@angular/material/checkbox'; describe('CBOR CreateAScenarioComponent', () => { - let component: SelectablePresentationFormComponent; - let fixture: ComponentFixture; + let component: SelectableAttestationAttributesComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -26,14 +26,14 @@ describe('CBOR CreateAScenarioComponent', () => { SharedModule, MatCheckboxModule, ], - declarations: [ SelectablePresentationFormComponent ], + declarations: [ SelectableAttestationAttributesComponent ], providers: [ SelectableFormNextAction ] }) .compileComponents(); - fixture = TestBed.createComponent(SelectablePresentationFormComponent); + fixture = TestBed.createComponent(SelectableAttestationAttributesComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.ts b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.ts new file mode 100644 index 0000000..db38743 --- /dev/null +++ b/src/app/features/presentation-request-preparation/components/selectable-attestation-attributes/selectable-attestation-attributes.component.ts @@ -0,0 +1,168 @@ +import {Component, inject, OnInit} from '@angular/core'; +import {MsoMdocPresentationService} from "@app/core/services/mso-mdoc-presentation.service"; +import {VerifierEndpointService} from "@core/services/verifier-endpoint.service"; +import {FieldConstraint, Filter} from "@core/models/presentation/FieldConstraint"; +import {FormSelectableField} from "@core/models/FormSelectableField"; +import {InputDescriptor} from "@core/models/presentation/InputDescriptor"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; +import {MatCheckboxModule} from "@angular/material/checkbox"; +import {MatExpansionModule} from "@angular/material/expansion"; +import {SharedModule} from "@shared/shared.module"; +import {AttestationType} from "@core/models/attestation/AttestationType"; +import {MSO_MDOC_BY_TYPE} from "@core/data/MsoMdocDocuments"; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog"; +import {CommonModule} from "@angular/common"; +import {MatButtonModule} from "@angular/material/button"; +import {DialogData} from "@features/presentation-request-preparation/components/selectable-attestation-attributes/model/DialogData"; + +@Component({ + standalone: true, + selector: 'vc-selectable-attestation-attributes', + templateUrl: './selectable-attestation-attributes.component.html', + styleUrls: ['./selectable-attestation-attributes.component.scss'], + imports: [ + CommonModule, + MatCheckboxModule, + MatExpansionModule, + SharedModule, + MatDialogModule, + MatButtonModule + ], + providers: [VerifierEndpointService] +}) +export class SelectableAttestationAttributesComponent implements OnInit { + + readonly data = inject(MAT_DIALOG_DATA); + + attestationType!: AttestationType; + attestationFormat!: AttestationFormat; + seed: InputDescriptor | undefined; + + formFields!: FormSelectableField[]; + draftInputDescriptor!: InputDescriptor; + inputDescriptorText!: string; + selectedFields: FieldConstraint[] = []; + + constructor( + private readonly msoMdocPresentationService: MsoMdocPresentationService, + private dialogRef: MatDialogRef + ) { + } + + ngOnInit(): void { + this.attestationFormat = this.data.format; + this.attestationType = this.data.type; + this.seed = this.data.seed; + this.formFields = this.extractFormFieldsFromModel() + if (this.seed) { + this.selectedFields = this.seed.constraints.fields + this.draftInputDescriptor = this.seed + } else { + this.initEmptyInputDescriptor(); + } + this.inputDescriptorText = this.convertJSONtoString(this.draftInputDescriptor); + } + + initEmptyInputDescriptor() { + switch (this.attestationFormat) { + case AttestationFormat.MSO_MDOC: + let msomdoc = MSO_MDOC_BY_TYPE[this.attestationType as string]; + this.draftInputDescriptor = this.msoMdocPresentationService.msoMdocInputDescriptorOf(msomdoc, "", []) + return + case AttestationFormat.SD_JWT_VC: + console.error("Format " + AttestationFormat.SD_JWT_VC + " not suppoerted yet"); + return [] + case AttestationFormat.JWT_VC_JSON: + console.error("Format " + AttestationFormat.JWT_VC_JSON + " not suppoerted yet"); + return [] + } + } + + handle(data: FormSelectableField) { + const value = data?.value; + if (!this.exists(value.path[0])) { + this.selectedFields.push(value); + } else if (this.exists(value.path[0])) { + this.selectedFields = this.selectedFields.filter((item: FieldConstraint) => { + return String(item.path) !== String(value.path[0]); + }); + } + // Update draft presentation with selected fields + this.draftInputDescriptor.constraints.fields = this.selectedFields; + // refresh descriptor text from model + this.inputDescriptorText = this.convertJSONtoString(this.draftInputDescriptor); + } + + convertJSONtoString(obj: object) { + return JSON.stringify(obj, null, '\t'); + } + + exists(path: string) { + const exists = this.selectedFields.filter((item) => item.path.includes(path)); + return exists.length > 0; + } + + extractFormFieldsFromModel(): FormSelectableField[] { + switch (this.attestationFormat) { + case AttestationFormat.MSO_MDOC: + let msomdoc = MSO_MDOC_BY_TYPE[this.attestationType as string]; + return msomdoc.attestation.dataSet.map((attr, index) => { + return { + id: index, + label: attr.attribute, + value: this.msoMdocPresentationService.fieldConstraint(msomdoc.namespace, attr.identifier) + } + }) + case AttestationFormat.SD_JWT_VC: + console.error("Format " + AttestationFormat.SD_JWT_VC + " not suppoerted yet"); + return [] + case AttestationFormat.JWT_VC_JSON: + console.error("Format " + AttestationFormat.JWT_VC_JSON + " not suppoerted yet"); + return [] + } + } + + trackByFn(_index: number, data: FormSelectableField) { + return data.id; + } + + saveSelection() { + this.dialogRef.close({ + data: { + attestationType: this.attestationType, + inputDescriptor: this.selectedFields.length == 0 ? null : this.draftInputDescriptor + } + }); + } + + isChecked(field: FormSelectableField) { + return this.selectedFields.filter((item: FieldConstraint) => { + return this.areEqualConstraints(item, field.value); + }).length > 0 + } + + isSomethingSelected(): boolean { + return this.selectedFields.length > 0; + } + + areEqualConstraints(one: FieldConstraint, other: FieldConstraint): boolean { + function pathsAreEqual(a: string[], b: string[]): boolean { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + return JSON.stringify(a) == JSON.stringify(b); + } + + function filtersAreEqual(a: Filter | undefined, b: Filter | undefined): boolean { + if (a === b) return true; + if (a == null || b == null) return false; + + return a.type == b.type && JSON.stringify(a.contains) == JSON.stringify(b.contains); + } + + return one.intent_to_retain === other.intent_to_retain && + pathsAreEqual(one.path, other.path) && + filtersAreEqual(one.filter, other.filter) + } +} diff --git a/src/app/features/presentation-request-preparation/home/home.component.html b/src/app/features/presentation-request-preparation/home/home.component.html new file mode 100644 index 0000000..0196b55 --- /dev/null +++ b/src/app/features/presentation-request-preparation/home/home.component.html @@ -0,0 +1,60 @@ + + +
+

Define your presentation request

+

+ Follow the steps to specify what will be requested from the wallet to present. +

+ +
+ +
+ ... specify presentation scenario + +
+
+ + +
+
+ + +
+ ...select attestation(s) attributes + +
+
+ + +
+
+ + +
+ ...review and proceed + + + Your presentation request so far... + +
{{initializationRequest | json}}
+
..nothing prepared yet...
+
+
+
+ + +
+
+
+
+ +

+ Or, if this doesn't work for you, go straight to defining your presentation request + here +

+ +
+
diff --git a/src/app/features/presentation-request-preparation/home/home.component.spec.ts b/src/app/features/presentation-request-preparation/home/home.component.spec.ts new file mode 100644 index 0000000..3dc172b --- /dev/null +++ b/src/app/features/presentation-request-preparation/home/home.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterModule } from '@angular/router'; +import { HomeComponent } from './home.component'; +import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; +import { SharedModule } from '@app/shared/shared.module'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + WalletLayoutComponent, + RouterModule, + SharedModule + ], + declarations: [ HomeComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/presentation-request-preparation/home/home.component.ts b/src/app/features/presentation-request-preparation/home/home.component.ts new file mode 100644 index 0000000..51c089e --- /dev/null +++ b/src/app/features/presentation-request-preparation/home/home.component.ts @@ -0,0 +1,105 @@ +import {Component, inject} from '@angular/core'; +import {NavigateService} from '@app/core/services/navigate.service'; +import {HOME_ACTIONS} from '@core/constants/pages-actions'; +import {ActionCode} from '@app/shared/elements/body-actions/models/ActionCode'; +import {BodyAction} from '@app/shared/elements/body-actions/models/BodyAction'; +import {CommonModule} from "@angular/common"; +import {MatTabsModule} from "@angular/material/tabs"; +import {RadioGroupComponent} from "@shared/elements/radio-group/radio-group.component"; +import {SharedModule} from "@shared/shared.module"; +import {InputSchemeComponent} from "@shared/elements/input-scheme/input-scheme.component"; +import {WalletLayoutComponent} from "@core/layout/wallet-layout/wallet-layout.component"; +import {OpenLogsComponent} from "@shared/elements/open-logs/open-logs.component"; +import {MatDialogModule} from "@angular/material/dialog"; +import {RouterLink, RouterLinkActive, RouterOutlet} from "@angular/router"; +import {ScenarioComponent} from "@features/presentation-request-preparation/components/scenario/scenario.component"; +import {MatStepperModule} from "@angular/material/stepper"; +import {FormBuilder, ReactiveFormsModule, Validators} from "@angular/forms"; +import {MatButtonModule} from "@angular/material/button"; +import {ScenarioSelection} from "@features/presentation-request-preparation/models/ScenarioSelection"; +import { + AttributeSelectionComponent +} from "@features/presentation-request-preparation/components/attribute-selection/attribute-selection.component"; +import {TransactionInitializationRequest} from "@core/models/TransactionInitializationRequest"; +import {InputDescriptor} from "@core/models/presentation/InputDescriptor"; +import {v4 as uuidv4} from "uuid"; +import {VerifierEndpointService} from "@core/services/verifier-endpoint.service"; +import {MatExpansionModule} from "@angular/material/expansion"; + +@Component({ + standalone: true, + imports: [ + CommonModule, + MatTabsModule, + RadioGroupComponent, + SharedModule, + InputSchemeComponent, + WalletLayoutComponent, + OpenLogsComponent, + MatDialogModule, + RouterOutlet, + ScenarioComponent, + MatStepperModule, + ReactiveFormsModule, + MatButtonModule, + AttributeSelectionComponent, + MatExpansionModule, + RouterLinkActive, + RouterLink + ], + providers: [VerifierEndpointService], + selector: 'vc-presentation-preparation-home', + templateUrl: './home.component.html' +}) +export class HomeComponent { + constructor( + private readonly navigateService: NavigateService, + private readonly verifierEndpointService: VerifierEndpointService, + ) { } + + actions: BodyAction[] = HOME_ACTIONS; + + private _formBuilder = inject(FormBuilder); + formGroup = this._formBuilder.group({ + selectAttestationCtrl: ['', Validators.required] + }); + + scenarioSelection: ScenarioSelection | null = null; + initializationRequest: TransactionInitializationRequest | null = null; + + handleSelectionChangedEvent($event: ScenarioSelection) { + this.scenarioSelection = $event; + } + + handleAttributesCollectedEvent($event: InputDescriptor[]) { + if ($event != null && $event.length > 0) { + this.initializationRequest = this.prepareInitializationRequest($event); + } else + this.initializationRequest = null + } + + prepareInitializationRequest(inputDescriptors: InputDescriptor[]): TransactionInitializationRequest { + return { + type: "vp_token", + presentation_definition: { + id: uuidv4(), + input_descriptors: inputDescriptors + }, + nonce: uuidv4() + } + } + + proceedToInvokeWallet() { + if (this.initializationRequest != null) { + this.verifierEndpointService.initializeTransaction(this.initializationRequest, (_) => { + this.navigateService.navigateTo('invoke-wallet'); + }); + } else { + alert("nothing to submit") + } + } + + canProceed() { + return this.initializationRequest !== null; + } +} diff --git a/src/app/features/presentation-request-preparation/models/PresentationScenario.ts b/src/app/features/presentation-request-preparation/models/PresentationScenario.ts new file mode 100644 index 0000000..32e10c5 --- /dev/null +++ b/src/app/features/presentation-request-preparation/models/PresentationScenario.ts @@ -0,0 +1,6 @@ +import {ScenarioAttestation} from "@features/presentation-request-preparation/models/ScenarioAttestation"; + +export type PresentationScenario = { + name: string, + attestations: ScenarioAttestation[] +} diff --git a/src/app/features/presentation-request-preparation/models/ScenarioAttestation.ts b/src/app/features/presentation-request-preparation/models/ScenarioAttestation.ts new file mode 100644 index 0000000..0da3bec --- /dev/null +++ b/src/app/features/presentation-request-preparation/models/ScenarioAttestation.ts @@ -0,0 +1,11 @@ +import {AttestationType} from "@core/models/attestation/AttestationType"; + +export type ScenarioAttestation = { + attestationType: AttestationType, + attributeSelectionMethods: AttributeSelectionMethod[] +} + +export enum AttributeSelectionMethod { + ALL_ATTRIBUTES = "all_attributes", + SELECTABLE = "selectable" +} diff --git a/src/app/features/presentation-request-preparation/models/ScenarioSelection.ts b/src/app/features/presentation-request-preparation/models/ScenarioSelection.ts new file mode 100644 index 0000000..c2a36b0 --- /dev/null +++ b/src/app/features/presentation-request-preparation/models/ScenarioSelection.ts @@ -0,0 +1,15 @@ +import {AttestationType} from "@core/models/attestation/AttestationType"; +import {AttestationFormat} from "@core/models/attestation/AttestationFormat"; +import {AttributeSelectionMethod} from "@features/presentation-request-preparation/models/ScenarioAttestation"; + +export type ScenarioSelection = { + scenarioName: string, + selections: AttestationSelection[] +} + +export type AttestationSelection = { + type: AttestationType, + format: AttestationFormat | null, + attributeSelectionMethod: AttributeSelectionMethod | null + +} diff --git a/src/app/features/presentation-request-preparation/presentation-request-preparation-routing.module.ts b/src/app/features/presentation-request-preparation/presentation-request-preparation-routing.module.ts new file mode 100644 index 0000000..a2bd8b1 --- /dev/null +++ b/src/app/features/presentation-request-preparation/presentation-request-preparation-routing.module.ts @@ -0,0 +1,23 @@ +import {NgModule} from "@angular/core"; +import {RouterModule, Routes} from "@angular/router"; +import {HomeComponent} from "@features/presentation-request-preparation/home/home.component"; + +const routes: Routes = [ + { + path: '', + component: HomeComponent, + children: [ + { + path: '', + loadComponent: () => import('@features/presentation-request-preparation/components/scenario/scenario.component') + .then(c => c.ScenarioComponent) + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class PresentationRequestPreparationRoutingModule {} diff --git a/src/app/features/presentation-request-preparation/presentation-request-preparation.module.ts b/src/app/features/presentation-request-preparation/presentation-request-preparation.module.ts new file mode 100644 index 0000000..58b2696 --- /dev/null +++ b/src/app/features/presentation-request-preparation/presentation-request-preparation.module.ts @@ -0,0 +1,15 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {WalletLayoutComponent} from "@core/layout/wallet-layout/wallet-layout.component"; +import {InvokeWalletRoutingModule} from "@features/invoke-wallet/invoke-wallet-routing.module"; +import {SharedModule} from "@shared/shared.module"; + +@NgModule({ + imports: [ + CommonModule, + WalletLayoutComponent, + InvokeWalletRoutingModule, + SharedModule + ] +}) +export class PresentationRequestPreparationModule {} diff --git a/src/app/features/selectable-presentation/components/home/home.component.html b/src/app/features/selectable-presentation/components/home/home.component.html deleted file mode 100644 index ed3710b..0000000 --- a/src/app/features/selectable-presentation/components/home/home.component.html +++ /dev/null @@ -1,9 +0,0 @@ - -
-
Proceed to authentication
-
- -
- -
-
diff --git a/src/app/features/selectable-presentation/components/home/home.component.spect.ts b/src/app/features/selectable-presentation/components/home/home.component.spect.ts deleted file mode 100644 index b37e1f8..0000000 --- a/src/app/features/selectable-presentation/components/home/home.component.spect.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HomeComponent } from './home.component'; -import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; -import { RouterModule } from '@angular/router'; -import { SharedModule } from '@app/shared/shared.module'; - -describe('CBOR selectable HomeComponent', () => { - let component: HomeComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - WalletLayoutComponent, - RouterModule, - SharedModule - ], - declarations: [ HomeComponent ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(HomeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/selectable-presentation/components/home/home.component.ts b/src/app/features/selectable-presentation/components/home/home.component.ts deleted file mode 100644 index c2a43af..0000000 --- a/src/app/features/selectable-presentation/components/home/home.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; -import { filter } from 'rxjs/operators'; -import { SELECTABLE_ACTIONS } from '@core/constants/pages-actions'; -import { ActionCode } from '@app/shared/elements/body-actions/models/ActionCode'; -import { BodyAction } from '@app/shared/elements/body-actions/models/BodyAction'; -import { SelectableFormNextAction } from '../../services/selectable-form-next-action.service'; -import { NavigateService } from '@app/core/services/navigate.service'; - -@Component({ - selector: 'vc-home', - templateUrl: './home.component.html' -}) -export class HomeComponent implements OnInit { - - actions: BodyAction[] = SELECTABLE_ACTIONS; - isCreatePage = true; - constructor ( - private readonly selectableFormNextAction: SelectableFormNextAction, - private readonly navigateService: NavigateService, - private readonly router: Router - ) {} - ngOnInit (): void { - this.router.events - .pipe( - filter((event): event is NavigationEnd => event instanceof NavigationEnd) - ) - .subscribe((event) => { - this.isCreatePage = !event.url.includes('verifiable'); - if (this.isCreatePage) { - this.actions = SELECTABLE_ACTIONS; - } - }); - } - - runActions (data: BodyAction) { - if (data.code === ActionCode.NEXT) { - this.selectableFormNextAction.next('go next'); - this.actions = this.actions.filter((item) => item.code !== ActionCode.NEXT); - // Clear subscriptions to 'next step' observable - this.selectableFormNextAction.clear(); - } else if(data.code === ActionCode.BACK) { - this.navigateService.goBack(); - } - } -} diff --git a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html deleted file mode 100644 index 1e45187..0000000 --- a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
-
-

Select the information requested from the user

- {{selectedFields.length}} selected -
- -
- - {{field.label}} - -
-
-
-
- - - - Presentation definition - - - - diff --git a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts deleted file mode 100644 index 94da6e2..0000000 --- a/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts +++ /dev/null @@ -1,128 +0,0 @@ -import {ChangeDetectorRef, Component, Injector, OnInit} from '@angular/core'; -import {DataService} from '@app/core/services/data.service'; -import {NavigateService} from '@app/core/services/navigate.service'; -import {SelectableFormNextAction} from '../../services/selectable-form-next-action.service'; -import {LocalStorageService} from '@app/core/services/local-storage.service'; -import * as constants from '@core/constants/constants'; -import {Modification} from '@app/shared/elements/body-actions/models/modification'; -import {BodyActionsService} from '@app/shared/elements/body-actions/body-actions.service'; -import {AttestationSelectableModelService} from "@app/core/services/attestation-selectable-model.service"; -import {MsoMdocPresentationService} from "@app/core/services/mso-mdoc-presentation.service"; -import {MsoMdoc} from "@core/models/MsoMdoc"; -import {VerifierEndpointService} from "@core/services/verifier-endpoint.service"; -import {TransactionInitializationRequest} from "@core/models/TransactionInitializationRequest"; -import {FieldConstraint} from "@core/models/presentation/FieldConstraint"; -import {FormSelectableField} from "@core/models/FormSelectableField"; - -@Component({ - selector: 'vc-create-a-scenario', - templateUrl: './selectable-presentation-form.component.html', - styleUrls: ['./selectable-presentation-form.component.scss'], - providers: [VerifierEndpointService] -}) -export class SelectablePresentationFormComponent implements OnInit { - - formFields!: FormSelectableField[]; - buttonMode = 'none'; - attestationModel!: MsoMdoc; - draftPresentation!: TransactionInitializationRequest; - presentationDefinitionText!: string; - selectedFields: FieldConstraint[] = []; - private readonly navigateService!: NavigateService; - private readonly localStorageService!: LocalStorageService; - private readonly bodyActionsService!: BodyActionsService; - - constructor( - private readonly selectableFormNextAction: SelectableFormNextAction, - private readonly verifierEndpointService: VerifierEndpointService, - private readonly attestationSelectableModelService: AttestationSelectableModelService, - private readonly msoMdocPresentationService: MsoMdocPresentationService, - private readonly dataService: DataService, - private readonly changeDetectorRef: ChangeDetectorRef, - private readonly injector: Injector, - ) { - this.navigateService = this.injector.get(NavigateService); - this.localStorageService = this.injector.get(LocalStorageService); - this.bodyActionsService = this.injector.get(BodyActionsService); - - this.enableNextButton(); - } - - ngOnInit(): void { - this.localStorageService.remove(constants.ACTIVE_TRANSACTION); - this.initPresentationModel(); - // Init form from model - this.formFields = this.extractFormFieldsFromModel() - this.selectableFormNextAction.subscribe(_ => { - this.initializePresentationTransaction() - }); - } - - initPresentationModel() { - this.attestationModel = this.attestationSelectableModelService.getModel(); - const presentationPurpose = this.attestationSelectableModelService.getPresentationPurpose(); - this.draftPresentation = this.msoMdocPresentationService.presentationOf(this.attestationModel, presentationPurpose, []) - } - - initializePresentationTransaction() { - let draftPresentationRequest = this.convertJSONtoString(this.draftPresentation); - if (draftPresentationRequest) { - let initializationRequest = JSON.parse(draftPresentationRequest) as TransactionInitializationRequest - this.verifierEndpointService.initializeTransaction(initializationRequest, (data) => { - this.buttonMode = 'none'; - this.navigateService.navigateTo('/invoke-wallet'); - this.changeDetectorRef.detectChanges(); - }); - } else { - console.error('invalid JSON format'); - } - } - - handle(data: FormSelectableField) { - const value = data?.value; - if (!this.isExist(value.path[0])) { - this.selectedFields.push(value); - } else if (this.isExist(value.path[0])) { - this.selectedFields = this.selectedFields.filter((item: FieldConstraint) => { - return String(item.path) !== String(value.path[0]); - }); - } - // Update draft presentation with selected fields - this.draftPresentation.presentation_definition.input_descriptors[0].constraints.fields = this.selectedFields; - // refresh PD text from model - this.presentationDefinitionText = this.convertJSONtoString(this.draftPresentation.presentation_definition); - this.enableNextButton(); - this.changeDetectorRef.detectChanges(); - } - - convertJSONtoString(obj: object) { - return JSON.stringify(obj, null, '\t'); - } - - isExist(path: string) { - const exists = this.selectedFields.filter((item) => item.path.includes(path)); - return exists.length > 0; - } - - enableNextButton() { - const modifyData: Modification = { - id: 'next_button', - disabled: this.selectedFields == undefined || this.selectedFields.length === 0 - }; - this.bodyActionsService.handelButton$.next(modifyData); - } - - extractFormFieldsFromModel(): FormSelectableField[] { - return this.attestationModel.attributes.map((attr, index) => { - return { - id: index, - label: attr.text, - value: this.msoMdocPresentationService.fieldConstraint(this.attestationModel.namespace, attr.value) - } - }) - } - - trackByFn(_index: number, data: FormSelectableField) { - return data.id; - } -} diff --git a/src/app/features/selectable-presentation/selectable-presentation-routing.module.ts b/src/app/features/selectable-presentation/selectable-presentation-routing.module.ts deleted file mode 100644 index 0657da4..0000000 --- a/src/app/features/selectable-presentation/selectable-presentation-routing.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { HomeComponent } from './components/home/home.component'; -import { SelectablePresentationFormComponent } from '@features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component'; -const routes: Routes = [ - { - path: '', - component: HomeComponent, - children: [ - { path: 'create', - component: SelectablePresentationFormComponent - } - ] - } -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class SiopCustomRoutingModule { } diff --git a/src/app/features/selectable-presentation/selectable-presentation.module.ts b/src/app/features/selectable-presentation/selectable-presentation.module.ts deleted file mode 100644 index 1fa6cae..0000000 --- a/src/app/features/selectable-presentation/selectable-presentation.module.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatCheckboxModule } from '@angular/material/checkbox'; - - -import { SiopCustomRoutingModule } from './selectable-presentation-routing.module'; -import { SelectablePresentationFormComponent } from '@features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component'; -import { HomeComponent } from './components/home/home.component'; -import { SharedModule } from '@app/shared/shared.module'; -import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; -import { SelectableFormNextAction } from './services/selectable-form-next-action.service'; - - -@NgModule({ - declarations: [ - SelectablePresentationFormComponent, - HomeComponent - ], - imports: [ - CommonModule, - SiopCustomRoutingModule, - WalletLayoutComponent, - ReactiveFormsModule, - FormsModule, - MatExpansionModule, - SharedModule, - MatCheckboxModule, - ], - providers: [ - SelectableFormNextAction - ] -}) -export class SelectablePresentationModule { } diff --git a/src/app/features/selectable-presentation/services/selectable-form-next-action.service.spec.ts b/src/app/features/selectable-presentation/services/selectable-form-next-action.service.spec.ts deleted file mode 100644 index 7a89a77..0000000 --- a/src/app/features/selectable-presentation/services/selectable-form-next-action.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { SelectableFormNextAction } from './selectable-form-next-action.service'; - -describe('HelperCborSelectableService', () => { - let service: SelectableFormNextAction; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [SelectableFormNextAction] - }); - service = TestBed.inject(SelectableFormNextAction); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/features/selectable-presentation/services/selectable-form-next-action.service.ts b/src/app/features/selectable-presentation/services/selectable-form-next-action.service.ts deleted file mode 100644 index 3a32b04..0000000 --- a/src/app/features/selectable-presentation/services/selectable-form-next-action.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@angular/core'; -import {Subject, Subscription} from 'rxjs'; - -@Injectable() -export class SelectableFormNextAction { - - /** - * Next button subject that holds the action to be executed when next button is clicked - */ - private goNextStep$: Subject = new Subject(); - - /** - * An array to hold our subscriptions - */ - private subscriptions$: Array = new Array() - - subscribe(observerOrNext: (value: string) => void) { - this.subscriptions$.push( - this.goNextStep$.subscribe(observerOrNext) - ) - } - - next(goNext: string) { - this.goNextStep$.next(goNext) - } - - /** - * Unsubscribes all subscriptions of goNextStep$ Subject - */ - clear() { - this.subscriptions$.forEach(subscription => subscription.unsubscribe()); - } - -} diff --git a/src/app/features/wallet-redirect/resolver/wallet-redirect-resolver.ts b/src/app/features/wallet-redirect/resolver/wallet-redirect-resolver.ts index 69803aa..8e44bd8 100644 --- a/src/app/features/wallet-redirect/resolver/wallet-redirect-resolver.ts +++ b/src/app/features/wallet-redirect/resolver/wallet-redirect-resolver.ts @@ -7,7 +7,7 @@ import {ConcludedTransaction} from "@core/models/ConcludedTransaction"; import {map} from "rxjs/operators"; import {WalletResponse} from "@core/models/WalletResponse"; import {LocalStorageService} from "@core/services/local-storage.service"; -import {ACTIVE_PRESENTATION_DEFINITION, ACTIVE_TRANSACTION} from "@core/constants/constants"; +import {ACTIVE_PRESENTATION_DEFINITION, ACTIVE_TRANSACTION} from "@core/constants/general"; import {PresentationDefinition} from "@core/models/presentation/PresentationDefinition"; export const WalletRedirectResolver: ResolveFn = diff --git a/src/app/features/home/components/input-scheme/error-state-matcher.ts b/src/app/shared/elements/input-scheme/error-state-matcher.ts similarity index 100% rename from src/app/features/home/components/input-scheme/error-state-matcher.ts rename to src/app/shared/elements/input-scheme/error-state-matcher.ts diff --git a/src/app/shared/elements/input-scheme/input-scheme.component.html b/src/app/shared/elements/input-scheme/input-scheme.component.html new file mode 100644 index 0000000..9d0dab9 --- /dev/null +++ b/src/app/shared/elements/input-scheme/input-scheme.component.html @@ -0,0 +1,26 @@ +

Change default custom scheme

+ + + +
+ Current custom scheme value is:  {{ schemeValue }} +
+ +
+
+ + Scheme + + Please enter a valid scheme + scheme is required + +
+
+ +
+ + + + + diff --git a/src/app/features/home/components/input-scheme/input-scheme.component.scss b/src/app/shared/elements/input-scheme/input-scheme.component.scss similarity index 100% rename from src/app/features/home/components/input-scheme/input-scheme.component.scss rename to src/app/shared/elements/input-scheme/input-scheme.component.scss diff --git a/src/app/features/home/components/input-scheme/input-scheme.component.spec.ts b/src/app/shared/elements/input-scheme/input-scheme.component.spec.ts similarity index 95% rename from src/app/features/home/components/input-scheme/input-scheme.component.spec.ts rename to src/app/shared/elements/input-scheme/input-scheme.component.spec.ts index 95ed5c4..6d0a7cc 100644 --- a/src/app/features/home/components/input-scheme/input-scheme.component.spec.ts +++ b/src/app/shared/elements/input-scheme/input-scheme.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { InputSchemeComponent } from './input-scheme.component'; -import { SharedModule } from '@app/shared/shared.module'; +import { SharedModule } from '@shared/shared.module'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatInputModule } from '@angular/material/input'; diff --git a/src/app/features/home/components/input-scheme/input-scheme.component.ts b/src/app/shared/elements/input-scheme/input-scheme.component.ts similarity index 71% rename from src/app/features/home/components/input-scheme/input-scheme.component.ts rename to src/app/shared/elements/input-scheme/input-scheme.component.ts index 1d8858c..988c39f 100644 --- a/src/app/features/home/components/input-scheme/input-scheme.component.ts +++ b/src/app/shared/elements/input-scheme/input-scheme.component.ts @@ -5,14 +5,16 @@ import { MatIconModule } from '@angular/material/icon'; import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { ErrorStateMatcher } from './error-state-matcher'; -import { SharedModule } from '@app/shared/shared.module'; -import { LocalStorageService } from '@app/core/services/local-storage.service'; -import { DEFAULT_SCHEME, SCHEME } from '@app/core/constants/constants'; +import { SharedModule } from '@shared/shared.module'; +import { LocalStorageService } from '@core/services/local-storage.service'; +import { DEFAULT_SCHEME, SCHEME } from '@core/constants/general'; +import {MatDialogModule} from "@angular/material/dialog"; +import {MatButtonModule} from "@angular/material/button"; @Component({ selector: 'vc-input-scheme', standalone: true, - imports: [CommonModule, MatExpansionModule, FormsModule, ReactiveFormsModule, MatInputModule, SharedModule, MatIconModule], + imports: [CommonModule, MatExpansionModule, FormsModule, ReactiveFormsModule, MatInputModule, SharedModule, MatIconModule, MatDialogModule, MatButtonModule], templateUrl: './input-scheme.component.html', styleUrls: ['./input-scheme.component.scss'] })