diff --git a/locales/en/index.yml b/locales/en/index.yml index a6a39984b05..91493fae18c 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3222,6 +3222,20 @@ features: unsupportedDevice: text: Starting from 02.07.2024 your device may no longer be compatible with IO. moreInfo: More info + presentation: + credentialDetails: + personalDataTitle: "Personal data" + documentDataTitle: "Document data" + lastUpdated: "Last updated on {{lastUpdateTime}}" + hiddenClaim: "Hidden" + status: + valid: Valid + expired: Expired + actions: + removeFromWallet: "Remove from Wallet" + requestAssistance: "Something wrong? Contact {{authSource}}" + showClaimValues: "Show claim values" + hideClaimValues: "Hide claim values" webView: error: missingParams: Not all information necessary to access this page are available diff --git a/locales/it/index.yml b/locales/it/index.yml index 9ca0b7b04b9..1839cba45c9 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3222,6 +3222,20 @@ features: unsupportedDevice: text: A partire dal 02.07.2024 il tuo dispositivo potrebbe non essere più compatibile con IO. moreInfo: Scopri perchè + presentation: + credentialDetails: + personalDataTitle: "Dati personali" + documentDataTitle: "Dati documento" + lastUpdated: "Dati aggiornati al {{lastUpdateTime}}" + hiddenClaim: "Nascosto" + status: + valid: Valida + expired: Scaduta + actions: + removeFromWallet: "Rimuovi dal Portafoglio" + requestAssistance: "Qualcosa non torna? Contatta {{authSource}}" + showClaimValues: "Mostra gli attributi del documento" + hideClaimValues: "Nascondi gli attributi del documento" webView: error: missingParams: Non sono presenti le informazioni necessarie per accedere a questa pagina diff --git a/ts/features/itwallet/common/components/ItwCredentialClaim.tsx b/ts/features/itwallet/common/components/ItwCredentialClaim.tsx index 1090590e64a..1219a85ccec 100644 --- a/ts/features/itwallet/common/components/ItwCredentialClaim.tsx +++ b/ts/features/itwallet/common/components/ItwCredentialClaim.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Divider, ListItemInfo } from "@pagopa/io-app-design-system"; import * as E from "fp-ts/Either"; import { pipe } from "fp-ts/lib/function"; @@ -7,18 +7,24 @@ import { DateFromString } from "@pagopa/ts-commons/lib/dates"; import { ClaimDisplayFormat, ClaimValue, + DateClaimConfig, DrivingPrivilegesClaim, DrivingPrivilegesClaimType, EvidenceClaim, ImageClaim, PlaceOfBirthClaim, PlaceOfBirthClaimType, - PlainTextClaim + PlainTextClaim, + dateClaimsConfig, + previewDateClaimsConfig } from "../utils/itwClaimsUtils"; import I18n from "../../../../i18n"; import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet"; import { localeDateFormat } from "../../../../utils/locale"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; +import { getExpireStatus } from "../../../../utils/dates"; + +const HIDDEN_CLAIM = "******"; /** * Component which renders a place of birth type claim. @@ -57,28 +63,63 @@ const PlainTextClaimItem = ({ ); /** - * Component which renders a date type claim. + * Component which renders a date type claim with an optional icon and expiration badge. * @param label - the label of the claim * @param claim - the value of the claim */ -const DateClaimItem = ({ label, claim }: { label: string; claim: Date }) => { +const DateClaimItem = ({ + label, + claim, + iconVisible, + expirationBadgeVisible +}: { + label: string; + claim: Date; +} & DateClaimConfig) => { const value = localeDateFormat( claim, I18n.t("global.dateFormats.shortFormat") ); + + const endElement: ListItemInfo["endElement"] = useMemo(() => { + if (!expirationBadgeVisible) { + return; + } + const isExpired = getExpireStatus(claim) === "EXPIRED"; + return { + type: "badge", + componentProps: { + variant: isExpired ? "error" : "success", + text: I18n.t( + `features.itWallet.presentation.credentialDetails.status.${ + isExpired ? "expired" : "valid" + }` + ) + } + }; + }, [expirationBadgeVisible, claim]); + return ( @@ -239,17 +280,35 @@ const DrivingPrivilegesClaimItem = ({ * It renders a different component based on the type of the claim. * @param claim - the claim to render */ -export const ItwCredentialClaim = ({ claim }: { claim: ClaimDisplayFormat }) => +export const ItwCredentialClaim = ({ + claim, + hidden, + isPreview +}: { + claim: ClaimDisplayFormat; + hidden?: boolean; + isPreview?: boolean; +}) => pipe( claim.value, ClaimValue.decode, E.fold( () => , - decoded => { + _decoded => { + const decoded = hidden ? HIDDEN_CLAIM : _decoded; if (PlaceOfBirthClaim.is(decoded)) { return ; } else if (DateFromString.is(decoded)) { - return ; + const dateClaimProps = isPreview + ? previewDateClaimsConfig + : dateClaimsConfig[claim.id]; + return ( + + ); } else if (EvidenceClaim.is(decoded)) { return ( { - const claims = parseClaims(sortClaims(displayData.order, parsedCredential)); - - /** - * Renders the releaser name with an info button that opens the bottom sheet. - * This is not part of the claims list because it's not a claim. - * Thus it's rendered separately. - * @param releaserName - the releaser name. - * @returns the list item with the releaser name. - */ - const RenderReleaserName = () => { - const releaserName = issuerConf.federation_entity.organization_name; - const label = I18n.t( - "features.itWallet.verifiableCredentials.claims.releasedBy" - ); - const releasedByBottomSheet = useItwInfoBottomSheet({ - title: - releaserName ?? - I18n.t("features.itWallet.generic.placeholders.organizationName"), - content: [ - { - title: I18n.t( - "features.itWallet.issuance.credentialPreview.bottomSheet.about.title" - ), - body: I18n.t( - "features.itWallet.issuance.credentialPreview.bottomSheet.about.subtitle" - ) - }, - { - title: I18n.t( - "features.itWallet.issuance.credentialPreview.bottomSheet.data.title" - ), - body: I18n.t( - "features.itWallet.issuance.credentialPreview.bottomSheet.data.subtitle" - ) - } - ] - }); + const { parsedCredential, displayData } = data; - return ( - <> - {releaserName ? ( - <> - releasedByBottomSheet.present() - } - }} - label={label} - value={releaserName} - accessibilityLabel={`${label} ${releaserName}`} - /> - {releasedByBottomSheet.bottomSheet} - - ) : null} - - ); - }; - - /** - * Renders the PID assurance level with an info button that currenlt navigates to a not available screen. - * If the credential is not a PID credential, it returns null. - * This is not part of the claims list because it's not a claim. - * Thus it's rendered separately. - * @returns the list item with the PID assurance level. - */ - const RenderPidAssuranceLevel = () => { - if (credentialType === CredentialType.PID) { - const { sdJwt } = SdJwt.decode(credential, SdJwt.SdJwt4VC); - const assuranceLevel = mapAssuranceLevel( - sdJwt.payload.verified_claims.verification.assurance_level - ); - return ( - Alert.alert("Not available"), - accessibilityLabel: "" - } - }} - /> - ); - } else { - return null; - } - }; + const claims = parseClaims(sortClaims(displayData.order, parsedCredential)); return ( <> {claims.map((elem, index) => ( - + ))} - - + + ); }; diff --git a/ts/features/itwallet/common/components/ItwCredentialClaimsSection.tsx b/ts/features/itwallet/common/components/ItwCredentialClaimsSection.tsx new file mode 100644 index 00000000000..f8ad18e2c4d --- /dev/null +++ b/ts/features/itwallet/common/components/ItwCredentialClaimsSection.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { StyleSheet, View } from "react-native"; +import { IconButton, H6 } from "@pagopa/io-app-design-system"; +import I18n from "../../../../i18n"; +import { ClaimDisplayFormat } from "../utils/itwClaimsUtils"; +import { ItwCredentialClaim } from "./ItwCredentialClaim"; + +type Props = { + title: string; + claims: ReadonlyArray; + canHideValues?: boolean; +}; + +export const ItwCredentialClaimsSection = ({ + title, + canHideValues, + claims +}: Props) => { + const [valuesHidden, setValuesHidden] = useState(false); + + const renderHideValuesToggle = () => ( + setValuesHidden(x => !x)} + accessibilityLabel={I18n.t( + valuesHidden + ? "features.itWallet.presentation.credentialDetails.actions.showClaimValues" + : "features.itWallet.presentation.credentialDetails.actions.hideClaimValues" + )} + /> + ); + + return ( + + +
{title}
+ {canHideValues && renderHideValuesToggle()} +
+ + {claims.map(c => ( + +
+ ); +}; + +const styles = StyleSheet.create({ + header: { + justifyContent: "space-between", + flexDirection: "row" + } +}); diff --git a/ts/features/itwallet/common/components/ItwPidAssuranceLevel.tsx b/ts/features/itwallet/common/components/ItwPidAssuranceLevel.tsx new file mode 100644 index 00000000000..347d91415dd --- /dev/null +++ b/ts/features/itwallet/common/components/ItwPidAssuranceLevel.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { Alert } from "react-native"; +import { ListItemInfo } from "@pagopa/io-app-design-system"; +import { SdJwt } from "@pagopa/io-react-native-wallet"; +import I18n from "../../../../i18n"; +import { CredentialType, mapAssuranceLevel } from "../utils/itwMocksUtils"; +import { StoredCredential } from "../utils/itwTypesUtils"; + +type Props = { + credential: StoredCredential; +}; + +/** + * Renders the PID assurance level with an info button that currenlt navigates to a not available screen. + * If the credential is not a PID credential, it returns null. + * This is not part of the claims list because it's not a claim. + * Thus it's rendered separately. + * @returns the list item with the PID assurance level. + */ +export const ItwPidAssuranceLevel = ({ + credential: storedCredential +}: Props) => { + const { credential, credentialType } = storedCredential; + + if (credentialType !== CredentialType.PID) { + return null; + } + + const { sdJwt } = SdJwt.decode(credential, SdJwt.SdJwt4VC); + const assuranceLevel = mapAssuranceLevel( + sdJwt.payload.verified_claims.verification.assurance_level + ); + return ( + Alert.alert("Not available"), + accessibilityLabel: "" + } + }} + /> + ); +}; diff --git a/ts/features/itwallet/common/components/ItwReleaserName.tsx b/ts/features/itwallet/common/components/ItwReleaserName.tsx new file mode 100644 index 00000000000..a9051b1cbbb --- /dev/null +++ b/ts/features/itwallet/common/components/ItwReleaserName.tsx @@ -0,0 +1,70 @@ +import { ListItemInfo } from "@pagopa/io-app-design-system"; +import React from "react"; +import I18n from "../../../../i18n"; +import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet"; +import { StoredCredential } from "../utils/itwTypesUtils"; + +type Props = { + credential: StoredCredential; +}; + +/** + * Renders the releaser name with an info button that opens the bottom sheet. + * This is not part of the claims list because it's not a claim. + * Thus it's rendered separately. + * @param releaserName - the releaser name. + * @returns the list item with the releaser name. + */ +export const ItwReleaserName = ({ credential }: Props) => { + const releaserName = + credential.issuerConf.federation_entity.organization_name; + const label = I18n.t( + "features.itWallet.verifiableCredentials.claims.releasedBy" + ); + const releasedByBottomSheet = useItwInfoBottomSheet({ + title: + releaserName ?? + I18n.t("features.itWallet.generic.placeholders.organizationName"), + content: [ + { + title: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.about.title" + ), + body: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.about.subtitle" + ) + }, + { + title: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.data.title" + ), + body: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.data.subtitle" + ) + } + ] + }); + + if (!releaserName) { + return null; + } + + return ( + <> + releasedByBottomSheet.present() + } + }} + label={label} + value={releaserName} + accessibilityLabel={`${label} ${releaserName}`} + /> + {releasedByBottomSheet.bottomSheet} + + ); +}; diff --git a/ts/features/itwallet/common/components/__tests__/ItwCredentialClaimsSection.test.tsx b/ts/features/itwallet/common/components/__tests__/ItwCredentialClaimsSection.test.tsx new file mode 100644 index 00000000000..95e34cd65e3 --- /dev/null +++ b/ts/features/itwallet/common/components/__tests__/ItwCredentialClaimsSection.test.tsx @@ -0,0 +1,41 @@ +import { render, fireEvent } from "@testing-library/react-native"; +import * as React from "react"; +import { ItwCredentialClaimsSection } from "../ItwCredentialClaimsSection"; +import { ClaimDisplayFormat } from "../../utils/itwClaimsUtils"; + +describe("ItwCredentialClaimsSection", () => { + const claims: Array = [ + { id: "name", label: "Nome", value: "Luigi" }, + { id: "surname", label: "Cognome", value: "Rossi" }, + { id: "age", label: "Età", value: 20 } + ]; + + it("should match the snapshot when claims are visible", () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + + it("should match the snapshot when claims are hidden", () => { + const component = render( + + ); + const toggleButton = component.queryByTestId("toggle-claim-visibility"); + + if (!toggleButton) { + fail("Toggle button not found"); + } + + fireEvent(toggleButton, "onPress"); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialClaimsSection.test.tsx.snap b/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialClaimsSection.test.tsx.snap new file mode 100644 index 00000000000..3999ad21b8d --- /dev/null +++ b/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialClaimsSection.test.tsx.snap @@ -0,0 +1,949 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ItwCredentialClaimsSection should match the snapshot when claims are hidden 1`] = ` + + + + Personal data + + + + + + + + + + + + + + + + + + Nome + + + ****** + + + + + + + + + + + + Cognome + + + ****** + + + + + + + + + + + + Età + + + Attributo non riconosciuto + + + + + + + + +`; + +exports[`ItwCredentialClaimsSection should match the snapshot when claims are visible 1`] = ` + + + + Personal data + + + + + + + + + + + + + + + + + + Nome + + + Luigi + + + + + + + + + + + + Cognome + + + Rossi + + + + + + + + + + + + Età + + + Attributo non riconosciuto + + + + + + + + +`; diff --git a/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts b/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts new file mode 100644 index 00000000000..736722e4326 --- /dev/null +++ b/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts @@ -0,0 +1,109 @@ +import { setLocale } from "../../../../../i18n"; +import { groupCredentialClaims } from "../itwClaimsUtils"; +import { ItwCredentialsMocks } from "../itwMocksUtils"; + +describe("groupCredentialClaims", () => { + beforeAll(() => setLocale("it")); + + it("Groups claims in the appropriate sections for eID", () => { + expect(groupCredentialClaims(ItwCredentialsMocks.eid)).toEqual({ + personalData: [ + { label: "Nome", value: "Casimira", id: "given_name" }, + { label: "Cognome", value: "Savoia", id: "family_name" }, + { label: "Data di Nascita", value: "1991-01-06", id: "birthdate" }, + { + label: "Luogo di Nascita", + value: { country: "IT", locality: "Rome" }, + id: "place_of_birth" + }, + { + label: "tax_id_number", + value: "TAMMRA80A41H501I", + id: "tax_id_number" + }, + { label: "Codice Fiscale", id: "tax_id_code" } + ], + noSection: [ + { label: "Identificativo univoco", value: "idANPR", id: "unique_id" }, + { + label: "evidence", + value: [ + { + type: "electronic_record", + record: { + type: "https://eudi.wallet.cie.gov.it", + source: { + organization_name: "Ministero dell'Interno", + organization_id: "urn:eudi:it:organization_id:ipa_code:m_it", + country_code: "IT" + } + } + } + ], + id: "evidence" + } + ] + }); + }); + + it("Groups claims in the appropriate sections for mDL", () => { + expect(groupCredentialClaims(ItwCredentialsMocks.mdl)).toEqual({ + personalData: [ + { label: "Nome", value: "Casimira", id: "given_name" }, + { label: "Cognome", value: "Savoia", id: "family_name" }, + { label: "Data di nascita", value: "1991-01-06", id: "birthdate" }, + { label: "Foto", value: expect.any(String), id: "portrait" } + ], + documentData: [ + { label: "Data di rilascio", value: "2023-11-14", id: "issue_date" }, + { label: "Data di scadenza", value: "2024-02-22", id: "expiry_date" }, + { + label: "Numero di documento", + value: "XX1234567", + id: "document_number" + } + ], + noSection: [ + { label: "Paese di rilascio", value: "IT", id: "issuing_country" }, + { + label: "Autorità di rilascio", + value: "Istituto Poligrafico e Zecca dello Stato", + id: "issuing_authority" + }, + { + label: "Segno distintivo UN", + value: "I", + id: "un_distinguishing_sign" + }, + { + label: "evidence", + value: [ + { + type: "electronic_record", + record: { + type: "https://eudi.wallet.pdnd.gov.it", + source: { + organization_name: "Motorizzazione Civile", + organization_id: "urn:eudi:it:organization_id:ipa_code:m_inf", + country_code: "IT" + } + } + } + ], + id: "evidence" + } + ], + licenseData: [ + { + label: "Categorie di veicoli", + value: { + issue_date: "2023-11-14", + vehicle_category_code: "A", + expiry_date: "2024-02-22" + }, + id: "driving_privileges" + } + ] + }); + }); +}); diff --git a/ts/features/itwallet/common/utils/itwClaimsUtils.ts b/ts/features/itwallet/common/utils/itwClaimsUtils.ts index fc38777d57b..ffb46e01c98 100644 --- a/ts/features/itwallet/common/utils/itwClaimsUtils.ts +++ b/ts/features/itwallet/common/utils/itwClaimsUtils.ts @@ -10,7 +10,7 @@ import * as O from "fp-ts/lib/Option"; import * as E from "fp-ts/lib/Either"; import { Locales } from "../../../../../locales/locales"; import I18n from "../../../../i18n"; -import { ParsedCredential } from "./itwTypesUtils"; +import { ParsedCredential, StoredCredential } from "./itwTypesUtils"; import { CredentialCatalogDisplay } from "./itwMocksUtils"; /** @@ -52,6 +52,7 @@ export const getEvidenceOrganizationName = (credential: ParsedCredential) => * Type for each claim to be displayed. */ export type ClaimDisplayFormat = { + id: string; label: string; value: unknown; }; @@ -77,7 +78,7 @@ export const parseClaims = ( ? attribute.name : attribute.name[getClaimsFullLocale()] || key; - return { label: attributeName, value: attribute.value }; + return { label: attributeName, value: attribute.value, id: key }; }); /** @@ -228,3 +229,72 @@ export const ClaimValue = t.union([ // Otherwise fallback to string PlainTextClaim ]); + +type ClaimSection = + | "personalData" + | "documentData" + | "licenseData" + | "noSection"; + +export type DateClaimConfig = Partial<{ + iconVisible: boolean; + expirationBadgeVisible: boolean; +}>; + +/** + * Hardcoded claims sections: currently it's not possible to determine how to group claims from the credential. + * The order of the claims doesn't matter here, the credential's `displayData` order wins. + * Claims that are present here but not in the credential are safely ignored. + */ +const sectionsByClaim: Record = { + // Personal data claims + given_name: "personalData", + family_name: "personalData", + birthdate: "personalData", + place_of_birth: "personalData", + tax_id_code: "personalData", + tax_id_number: "personalData", + portrait: "personalData", + sex: "personalData", + + // Document data claims + issue_date: "documentData", + expiry_date: "documentData", + expiration_date: "documentData", + document_number: "documentData", + + // Driving license claims + driving_privileges: "licenseData" +}; + +export const dateClaimsConfig: Record = { + issue_date: { iconVisible: true }, + expiry_date: { iconVisible: true, expirationBadgeVisible: true }, + expiration_date: { iconVisible: true, expirationBadgeVisible: true } +}; + +export const previewDateClaimsConfig: DateClaimConfig = { + iconVisible: false, + expirationBadgeVisible: false +}; + +/** + * Groups claims in a credential according to {@link sectionsByClaim}. + * Claims are assigned to the designated section in the order specified by the credential's `displayData`. + * Claims without a section are assigned to the key `noSection` so they can be rendered separately. + * @param credential + * @returns + */ +export const groupCredentialClaims = (credential: StoredCredential) => { + const claims = parseClaims( + sortClaims(credential.displayData.order, credential.parsedCredential) + ); + + return claims.reduce((acc, claim) => { + const section = sectionsByClaim[claim.id] || "noSection"; + return { + ...acc, + [section]: (acc[section] || []).concat(claim) + }; + }, {} as Record>); +}; diff --git a/ts/features/itwallet/common/utils/itwStyleUtils.ts b/ts/features/itwallet/common/utils/itwStyleUtils.ts new file mode 100644 index 00000000000..7420806b99a --- /dev/null +++ b/ts/features/itwallet/common/utils/itwStyleUtils.ts @@ -0,0 +1,15 @@ +import { IOColors } from "@pagopa/io-app-design-system"; +import { CredentialType } from "./itwMocksUtils"; + +export const getThemeColorByCredentialType = ( + credentialType: CredentialType +) => { + switch (credentialType) { + case CredentialType.PID: + return IOColors["blueItalia-600"]; + case CredentialType.DRIVING_LICENSE: + return IOColors.antiqueFuchsia; + default: + return IOColors["blueItalia-850"]; + } +}; diff --git a/ts/features/itwallet/issuance/components/ItwCredentialPreviewScreenContent.tsx b/ts/features/itwallet/issuance/components/ItwCredentialPreviewScreenContent.tsx index 465f1a172cc..d3e1d508dae 100644 --- a/ts/features/itwallet/issuance/components/ItwCredentialPreviewScreenContent.tsx +++ b/ts/features/itwallet/issuance/components/ItwCredentialPreviewScreenContent.tsx @@ -55,7 +55,7 @@ export const ItwCredentialPreviewScreenContent = ({ - + ( component={ItwIssuanceEidResultScreen} options={{ headerShown: false }} /> + + {/* CREDENTIAL PRESENTATION */} + ); diff --git a/ts/features/itwallet/navigation/routes.ts b/ts/features/itwallet/navigation/routes.ts index 8845a41bcb2..b1e0fd1a475 100644 --- a/ts/features/itwallet/navigation/routes.ts +++ b/ts/features/itwallet/navigation/routes.ts @@ -12,5 +12,8 @@ export const ITW_ROUTES = { EID_PREVIEW: "ITW_ISSUANCE_EID_PREVIEW", CREDENTIAL_PREVIEW: "ITW_ISSUANCE_CREDENTIAL_PREVIEW", RESULT: "ITW_ISSUANCE_RESULT" + } as const, + PRESENTATION: { + EID_DETAIL: "ITW_PRESENTATION_EID_DETAIL" } as const }; diff --git a/ts/features/itwallet/presentation/components/ItwClaimsSections.tsx b/ts/features/itwallet/presentation/components/ItwClaimsSections.tsx new file mode 100644 index 00000000000..39567996009 --- /dev/null +++ b/ts/features/itwallet/presentation/components/ItwClaimsSections.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { VSpacer } from "@pagopa/io-app-design-system"; +import { pipe, constNull } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import * as RA from "fp-ts/lib/ReadonlyArray"; +import I18n from "../../../../i18n"; +import { groupCredentialClaims } from "../../common/utils/itwClaimsUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; +import { ItwCredentialClaimsSection } from "../../common/components/ItwCredentialClaimsSection"; +import { ItwCredentialClaim } from "../../common/components/ItwCredentialClaim"; + +type Props = { + credential: StoredCredential; +}; + +/** + * Renders claims within sections defined by the hardcoded configuration in app. + */ +export const ItwClaimsSections = ({ credential }: Props) => { + const groupedClaims = groupCredentialClaims(credential); + + return ( + <> + {pipe( + groupedClaims.personalData, + O.fromNullable, + O.filter(RA.isNonEmpty), + O.fold(constNull, claims => ( + <> + + + + )) + )} + {pipe( + groupedClaims.documentData, + O.fromNullable, + O.filter(RA.isNonEmpty), + O.fold(constNull, claims => ( + <> + + + + )) + )} + {/* Fallback for claims that could not be assigned to any section */} + {pipe( + groupedClaims.noSection, + O.fromNullable, + O.filter(RA.isNonEmpty), + O.fold(constNull, claims => ( + <> + + {claims.map(c => ( + + ))} + + )) + )} + + ); +}; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationDetailFooter.tsx b/ts/features/itwallet/presentation/components/ItwPresentationDetailFooter.tsx new file mode 100644 index 00000000000..5e0cc83654f --- /dev/null +++ b/ts/features/itwallet/presentation/components/ItwPresentationDetailFooter.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { Alert, View } from "react-native"; +import { + ListItemAction, + VSpacer, + Chip, + IOStyles +} from "@pagopa/io-app-design-system"; +import { format } from "../../../../utils/dates"; +import I18n from "../../../../i18n"; + +type Props = { + lastUpdateTime: Date; +}; + +export const ItwPresentationDetailFooter = ({ lastUpdateTime }: Props) => ( + + Alert.alert("Assistance")} + /> + Alert.alert("Remove")} + /> + + + {I18n.t("features.itWallet.presentation.credentialDetails.lastUpdated", { + lastUpdateTime: format(lastUpdateTime, "DD MMMM YYYY, HH:mm") + })} + + +); diff --git a/ts/features/itwallet/presentation/screens/ItwPresentationEidDetailScreen.tsx b/ts/features/itwallet/presentation/screens/ItwPresentationEidDetailScreen.tsx new file mode 100644 index 00000000000..010dace9966 --- /dev/null +++ b/ts/features/itwallet/presentation/screens/ItwPresentationEidDetailScreen.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import * as O from "fp-ts/Option"; +import { pipe } from "fp-ts/lib/function"; +import { + ContentWrapper, + Divider, + IOVisualCostants, + VSpacer +} from "@pagopa/io-app-design-system"; +import { ScrollView, StyleSheet, View } from "react-native"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import FocusAwareStatusBar from "../../../../components/ui/FocusAwareStatusBar"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { ItwPidAssuranceLevel } from "../../common/components/ItwPidAssuranceLevel"; +import { ItwReleaserName } from "../../common/components/ItwReleaserName"; +import { + ItWalletError, + getItwGenericMappedError +} from "../../common/utils/itwErrorsUtils"; +import { + CredentialType, + ItwCredentialsMocks +} from "../../common/utils/itwMocksUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; +import { ItwCredentialCard } from "../../common/components/ItwCredentialCard"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; +import { useScreenEndMargin } from "../../../../hooks/useScreenEndMargin"; +import { getThemeColorByCredentialType } from "../../common/utils/itwStyleUtils"; +import { ItwClaimsSections } from "../components/ItwClaimsSections"; +import { ItwPresentationDetailFooter } from "../components/ItwPresentationDetailFooter"; + +// TODO: use the real credential update time +const today = new Date(); +const credentialCardData: ReadonlyArray = []; + +/** + * This component renders the entire credential detail. + */ +const ContentView = ({ eid }: { eid: StoredCredential }) => { + const { screenEndMargin } = useScreenEndMargin(); + const themeColor = getThemeColorByCredentialType( + eid.credentialType as CredentialType + ); + + useHeaderSecondLevel({ + title: "", + supportRequest: true, + variant: "contrast", + backgroundColor: themeColor + }); + + return ( + <> + + + + + + + + + + + + + + + + + + ); +}; + +export const ItwPresentationEidDetailScreen = () => { + const navigation = useIONavigation(); + const eidOption = O.some(ItwCredentialsMocks.eid); + + /** + * Error view component which currently displays a generic error. + * @param error - optional ItWalletError to be displayed. + */ + const ErrorView = ({ error: _ }: { error?: ItWalletError }) => { + const mappedError = getItwGenericMappedError(() => navigation.goBack()); + return ; + }; + + return pipe( + eidOption, + O.fold( + () => , + eid => + ) + ); +}; + +const styles = StyleSheet.create({ + cardContainer: { + position: "relative", + paddingHorizontal: IOVisualCostants.appMarginDefault + }, + cardBackdrop: { + height: "200%", // Twice the card in order to avoid the white background when the scrollview bounces + position: "absolute", + top: "-130%", // Offset by the card height + a 30% + right: 0, + left: 0, + zIndex: -1 + } +}); diff --git a/ts/hooks/useHeaderSecondLevel.tsx b/ts/hooks/useHeaderSecondLevel.tsx index ed45fc13bab..0496add26e2 100644 --- a/ts/hooks/useHeaderSecondLevel.tsx +++ b/ts/hooks/useHeaderSecondLevel.tsx @@ -23,6 +23,8 @@ type CommonProps = { headerShown?: boolean; transparent?: boolean; scrollValues?: ScrollValues; + variant?: "neutral" | "contrast"; + backgroundColor?: string; }; type NoAdditionalActions = { @@ -75,7 +77,9 @@ export const useHeaderSecondLevel = ({ secondAction, thirdAction, transparent = false, - scrollValues + scrollValues, + variant, + backgroundColor }: HeaderSecondLevelHookProps) => { const startSupportRequest = useStartSupportRequest({ faqCategories, @@ -154,6 +158,8 @@ export const useHeaderSecondLevel = ({ ), @@ -165,6 +171,8 @@ export const useHeaderSecondLevel = ({ headerShown, navigation, transparent, - scrollValues + scrollValues, + variant, + backgroundColor ]); }; diff --git a/ts/screens/profile/playgrounds/ItwPlayground.tsx b/ts/screens/profile/playgrounds/ItwPlayground.tsx index 87e66f72f14..e8054985144 100644 --- a/ts/screens/profile/playgrounds/ItwPlayground.tsx +++ b/ts/screens/profile/playgrounds/ItwPlayground.tsx @@ -56,6 +56,12 @@ const ItwPlayground = () => { }); }; + const navigateToCredentialDetail = () => { + navigation.navigate(ITW_ROUTES.MAIN, { + screen: ITW_ROUTES.PRESENTATION.EID_DETAIL + }); + }; + const navigateToCredentialPreview = () => { navigation.navigate(ITW_ROUTES.MAIN, { screen: ITW_ROUTES.ISSUANCE.CREDENTIAL_PREVIEW @@ -105,6 +111,14 @@ const ItwPlayground = () => { onPress={navigateToCredentialPreview} /> + {/* Credential detail playground */} + +

{"IT Wallet markdown preview"}

{/* Markdown ITW Playground */}