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 */}