diff --git a/img/features/itWallet/header/ts_header.png b/img/features/itWallet/header/ts_header.png new file mode 100644 index 00000000000..ed47a8f2d32 Binary files /dev/null and b/img/features/itWallet/header/ts_header.png differ diff --git a/locales/en/index.yml b/locales/en/index.yml index cff60c7472e..d294540a32d 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -684,7 +684,7 @@ authentication: title: How to activate it request_cie: label: Ask for your CIE - activate_cie: + activate_cie: title: Activate CIE credentials activate_spid: title: Ask for your SPID @@ -2607,7 +2607,7 @@ bonus: active: Active failed: Failed redeemed: Consumed - termsAndConditionFooter: 'By pressing “{{ctaText}}” you declare that you are in possession of the requirements and have read and understood the [Guide]({{regulationLink}}) and the [Privacy Policy].' + termsAndConditionFooter: "By pressing “{{ctaText}}” you declare that you are in possession of the requirements and have read and understood the [Guide]({{regulationLink}}) and the [Privacy Policy]." tos: title: Information on the processing of personal data content: !include bonus/bonusVacanze/bonus_tos.md @@ -3211,6 +3211,10 @@ features: issuedDate: "Valida dal" expirationDate: "Valida fino" restrictionConditions: "Codici" + unrecognizedData: + title: "Non riconosci alcuni dati?" + body: "Se ci sono errori nei dati o vuoi capire meglio cosa significano, puoi contattare il Ministero dell’Interno." + cta: "Chiedi informazioni o segnala errori" discovery: banner: title: "Novità: Documenti su IO" @@ -3301,7 +3305,7 @@ features: secondaryAction: Chiudi genericCredentialError: title: Documento non disponibile - body: Si è verificato un errore oppure potresti non avere diritto al documento. + body: Si è verificato un errore oppure potresti non avere diritto al documento. primaryAction: Riprova secondaryAction: Chiudi unsupportedDevice: @@ -3317,6 +3321,8 @@ features: mdl: content: "In questa fase, la versione digitale della Patente non ha lo stesso valore del documento fisico: dovrai presentarla insieme a un documento di identità valido." action: Scopri di più + ehc: + content: "This document is valid only in Italy. The official copy of the document is in PDF." expired: content: Il documento non è più valido. Se sei già in possesso del nuovo documento valido, puoi aggiornare la versione digitale nel Portafoglio action: Aggiorna il documento @@ -3340,7 +3346,7 @@ features: expiring: Expiring actions: removeFromWallet: "Remove from Wallet" - requestAssistance: "Something wrong? Contact {{authSource}}" + requestAssistance: "Something wrong?" showClaimValues: "Show claim values" hideClaimValues: "Hide claim values" dialogs: @@ -3351,6 +3357,7 @@ features: toast: removed: Fatto! Hai rimosso {{credentialName}} flipCard: "Show back" + fiscalCode: Your Fiscal Code trustmark: cta: Mostra certificato di autenticità title: Certificato di autenticità diff --git a/locales/it/index.yml b/locales/it/index.yml index 70a369e492f..a04329792df 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -684,7 +684,7 @@ authentication: title: Come attivarla request_cie: label: Richiedi la tua CIE - activate_cie: + activate_cie: title: Attiva le credenziali CIE activate_spid: title: Attiva SPID @@ -3211,6 +3211,10 @@ features: issuedDate: "Valida dal" expirationDate: "Valida fino" restrictionConditions: "Codici" + unrecognizedData: + title: "Non riconosci alcuni dati?" + body: "Se ci sono errori nei dati o vuoi capire meglio cosa significano, puoi contattare il Ministero dell’Interno." + cta: "Chiedi informazioni o segnala errori" discovery: banner: title: "Novità: Documenti su IO" @@ -3301,7 +3305,7 @@ features: secondaryAction: Chiudi genericCredentialError: title: Documento non disponibile - body: Si è verificato un errore oppure potresti non avere diritto al documento. + body: Si è verificato un errore oppure potresti non avere diritto al documento. primaryAction: Riprova secondaryAction: Chiudi unsupportedDevice: @@ -3317,6 +3321,8 @@ features: mdl: content: "In questa fase, la versione digitale della Patente non ha lo stesso valore del documento fisico: dovrai presentarla insieme a un documento di identità valido." action: Scopri di più + ehc: + content: "Questo documento è valido solo in Italia. La copia ufficiale del documento si trova nel PDF." expired: content: Il documento non è più valido. Se sei già in possesso del nuovo documento valido, puoi aggiornare la versione digitale nel Portafoglio action: Aggiorna il documento @@ -3340,7 +3346,7 @@ features: expiring: In scadenza actions: removeFromWallet: "Rimuovi dal Portafoglio" - requestAssistance: "Qualcosa non torna? Contatta {{authSource}}" + requestAssistance: "Qualcosa non torna?" showClaimValues: "Mostra gli attributi del documento" hideClaimValues: "Nascondi gli attributi del documento" dialogs: @@ -3351,6 +3357,7 @@ features: toast: removed: Fatto! Hai rimosso {{credentialName}} flipCard: "Mostra retro" + fiscalCode: Il tuo Codice Fiscale trustmark: cta: Mostra certificato di autenticità title: Certificato di autenticità diff --git a/ts/features/itwallet/common/components/ItwCredentialCard.tsx b/ts/features/itwallet/common/components/ItwCredentialCard.tsx index ae4ba794026..b13b6751c0a 100644 --- a/ts/features/itwallet/common/components/ItwCredentialCard.tsx +++ b/ts/features/itwallet/common/components/ItwCredentialCard.tsx @@ -1,16 +1,17 @@ import { Badge, - Body, - HSpacer, + HStack, IOColors, + makeFontStyleObject, Tag } from "@pagopa/io-app-design-system"; import React from "react"; -import { ImageSourcePropType, StyleSheet, View } from "react-native"; +import { ImageSourcePropType, StyleSheet, Text, View } from "react-native"; import { AnimatedImage } from "../../../../components/AnimatedImage"; import I18n from "../../../../i18n"; -import { CredentialType } from "../utils/itwMocksUtils"; import { getCredentialNameFromType } from "../utils/itwCredentialUtils"; +import { CredentialType } from "../utils/itwMocksUtils"; +import { getThemeColorByCredentialType } from "../utils/itwStyleUtils"; export type ItwCredentialStatus = "valid" | "pending" | "expiring" | "expired"; @@ -26,7 +27,8 @@ export const ItwCredentialCard = ({ isPreview = false }: ItwCredentialCard) => { const isValid = status === "valid"; - const labelColor: IOColors = isValid ? "bluegreyDark" : "grey-700"; + const theme = getThemeColorByCredentialType(credentialType); + const labelColor = isValid ? theme.textColor : IOColors["grey-700"]; const cardBackgroundSource = credentialCardBackgrounds[credentialType][isValid ? 0 : 1]; @@ -41,23 +43,13 @@ export const ItwCredentialCard = ({ style={styles.cardBackground} /> - - - + + + {getCredentialNameFromType(credentialType, "").toUpperCase()} - - {statusTagProps && ( - <> - - - - )} - + + {statusTagProps && } + {!isValid && } { - const credentialStatus = getCredentialStatus(data); - const claims = parseClaims(data.parsedCredential, { - exclude: [WellKnownClaim.unique_id, WellKnownClaim.link_qr_code] - }); - - return ( - <> - {claims.map((elem, index) => ( - - {index !== 0 && } - - ))} - - - - ); -}; diff --git a/ts/features/itwallet/common/components/ItwQrCodeClaimImage.tsx b/ts/features/itwallet/common/components/ItwQrCodeClaimImage.tsx index 83050d32c69..3e345e92796 100644 --- a/ts/features/itwallet/common/components/ItwQrCodeClaimImage.tsx +++ b/ts/features/itwallet/common/components/ItwQrCodeClaimImage.tsx @@ -1,21 +1,17 @@ import React from "react"; import { StyleSheet, View } from "react-native"; import { QrCodeImage } from "../../../../components/QrCodeImage"; -import { ParsedCredential } from "../utils/itwTypesUtils"; +import { ClaimDisplayFormat } from "../utils/itwClaimsUtils"; type ItwQrCodeClaimImageProps = { - claim?: ParsedCredential[keyof ParsedCredential]; - isHidden?: boolean; + claim: ClaimDisplayFormat; }; /** * This component allows to render the content of a claim in form of a QR Code */ -export const ItwQrCodeClaimImage = ({ - claim, - isHidden -}: ItwQrCodeClaimImageProps) => { - if (claim === undefined || typeof claim.value !== "string" || isHidden) { +export const ItwQrCodeClaimImage = ({ claim }: ItwQrCodeClaimImageProps) => { + if (claim.value === undefined || typeof claim.value !== "string") { return null; } diff --git a/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialCard.test.tsx.snap b/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialCard.test.tsx.snap index fe02531d442..1cdd0ffcc72 100644 --- a/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialCard.test.tsx.snap +++ b/ts/features/itwallet/common/components/__tests__/__snapshots__/ItwCredentialCard.test.tsx.snap @@ -49,6 +49,10 @@ exports[`ItwCredentialCard should match snapshot when status is "expired" 1`] = IDENTITÀ DIGITALE - IDENTITÀ DIGITALE - IDENTITÀ DIGITALE - IDENTITÀ DIGITALE @@ -1107,6 +1026,10 @@ exports[`ItwCredentialCard should render the preview 1`] = ` IDENTITÀ DIGITALE diff --git a/ts/features/itwallet/common/utils/itwClaimsUtils.ts b/ts/features/itwallet/common/utils/itwClaimsUtils.ts index 4f8d131fa75..d8d55a6817b 100644 --- a/ts/features/itwallet/common/utils/itwClaimsUtils.ts +++ b/ts/features/itwallet/common/utils/itwClaimsUtils.ts @@ -40,9 +40,13 @@ export enum WellKnownClaim { expiry_date = "expiry_date", /** * Claim used to display a QR Code for the Disability Card. It must be excluded from the common claims list - * and rendered using a {@link QRCodeImage} + * and rendered using a {@link QRCodeImage} (currently used for the European Disability Card) */ - link_qr_code = "link_qr_code" + link_qr_code = "link_qr_code", + /** + * Claim used to display the attachments of a credential (currently used for the European Health Insurance Card) + */ + content = "content" } /** diff --git a/ts/features/itwallet/common/utils/itwStyleUtils.ts b/ts/features/itwallet/common/utils/itwStyleUtils.ts index 7420806b99a..cfa6206e744 100644 --- a/ts/features/itwallet/common/utils/itwStyleUtils.ts +++ b/ts/features/itwallet/common/utils/itwStyleUtils.ts @@ -1,15 +1,65 @@ -import { IOColors } from "@pagopa/io-app-design-system"; +import { StatusBarStyle } from "react-native"; +import { HeaderSecondLevelHookProps } from "../../../../hooks/useHeaderSecondLevel"; +import { getCredentialNameFromType } from "./itwCredentialUtils"; import { CredentialType } from "./itwMocksUtils"; +export type CredentialTheme = { + backgroundColor: string; + textColor: string; + statusBarStyle: StatusBarStyle; +}; + export const getThemeColorByCredentialType = ( - credentialType: CredentialType -) => { + credentialType: string +): CredentialTheme => { switch (credentialType) { case CredentialType.PID: - return IOColors["blueItalia-600"]; + default: + return { + backgroundColor: "#295699", + textColor: "#032D5C", + statusBarStyle: "light-content" + }; case CredentialType.DRIVING_LICENSE: - return IOColors.antiqueFuchsia; + return { + backgroundColor: "#744C63", + textColor: "#652035", + statusBarStyle: "light-content" + }; + case CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD: + return { + backgroundColor: "#B3DCF9", + textColor: "#032D5C", + statusBarStyle: "dark-content" + }; + case CredentialType.EUROPEAN_DISABILITY_CARD: + return { + backgroundColor: "#315B76", + textColor: "#17406F", + statusBarStyle: "light-content" + }; + } +}; + +export const getHeaderPropsByCredentialType = ( + credentialType: string +): HeaderSecondLevelHookProps => { + const { backgroundColor } = getThemeColorByCredentialType(credentialType); + + switch (credentialType) { + case CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD: + return { + title: getCredentialNameFromType(credentialType), + supportRequest: true, + variant: "neutral", + backgroundColor + }; default: - return IOColors["blueItalia-850"]; + return { + title: getCredentialNameFromType(credentialType), + supportRequest: true, + variant: "contrast", + backgroundColor + }; } }; diff --git a/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx b/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx new file mode 100644 index 00000000000..3a09c50aa94 --- /dev/null +++ b/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx @@ -0,0 +1,50 @@ +import { Divider, H6, IOStyles } from "@pagopa/io-app-design-system"; +import React from "react"; +import { View } from "react-native"; +import I18n from "../../../../i18n"; +import { ItwCredentialClaim } from "../../common/components/ItwCredentialClaim"; +import { ItwReleaserName } from "../../common/components/ItwReleaserName"; +import { parseClaims, WellKnownClaim } from "../../common/utils/itwClaimsUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; + +type ItwCredentialClaimsListProps = { + data: StoredCredential; +}; + +/** + * This component renders the list of claims for a credential. + * It dinamically renders the list of claims passed as claims prop in the order they are passed. + * @param data - the {@link StoredCredential} of the credential. + */ +const ItwCredentialPreviewClaimsList = ({ + data +}: ItwCredentialClaimsListProps) => { + const claims = parseClaims(data.parsedCredential, { + exclude: [WellKnownClaim.unique_id, WellKnownClaim.link_qr_code] + }); + + return ( + <> + +
+ {I18n.t( + "features.itWallet.presentation.credentialDetails.documentDataTitle" + )} +
+
+ {claims.map((elem, index) => ( + + + ))} + + + ); +}; + +const MemoizedItwCredentialPreviewClaimsList = React.memo( + ItwCredentialPreviewClaimsList +); + +export { MemoizedItwCredentialPreviewClaimsList as ItwCredentialPreviewClaimsList }; diff --git a/ts/features/itwallet/issuance/components/__tests__/ItwCredentialPreviewClaimsList.test.tsx b/ts/features/itwallet/issuance/components/__tests__/ItwCredentialPreviewClaimsList.test.tsx new file mode 100644 index 00000000000..2167b62eb4a --- /dev/null +++ b/ts/features/itwallet/issuance/components/__tests__/ItwCredentialPreviewClaimsList.test.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { ItwStoredCredentialsMocks } from "../../../common/utils/itwMocksUtils"; +import { ITW_ROUTES } from "../../../navigation/routes"; +import { ItwCredentialPreviewClaimsList } from "../ItwCredentialPreviewClaimsList"; + +describe("ItwCredentialPreviewClaimsList", () => { + it("should match the snapshot", () => { + const component = renderComponent(); + expect(component).toMatchSnapshot(); + }); +}); + +function renderComponent() { + const globalState = appReducer(undefined, applicationChangeState("active")); + return renderScreenWithNavigationStoreContext( + () => ( + + ), + ITW_ROUTES.ISSUANCE.CREDENTIAL_PREVIEW, + {}, + createStore(appReducer, globalState as any) + ); +} diff --git a/ts/features/itwallet/issuance/components/__tests__/__snapshots__/ItwCredentialPreviewClaimsList.test.tsx.snap b/ts/features/itwallet/issuance/components/__tests__/__snapshots__/ItwCredentialPreviewClaimsList.test.tsx.snap new file mode 100644 index 00000000000..4d220b9dcf4 --- /dev/null +++ b/ts/features/itwallet/issuance/components/__tests__/__snapshots__/ItwCredentialPreviewClaimsList.test.tsx.snap @@ -0,0 +1,2207 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ItwCredentialPreviewClaimsList should match the snapshot 1`] = ` + + + + + + + + + + + + + + + ITW_ISSUANCE_CREDENTIAL_PREVIEW + + + + + + + + + + + + + + + + + + + Document data + + + + + + + + + Place of Birth + + + Rome (IT) + + + + + + + + + + + + + + Institution Number (TEAM) + + + 500001 + + + + + + + + + + + + + + Date of Birth + + + 06/01/1991 + + + + + + + + + + + + + + Province + + + Rome + + + + + + + + + + + + + + Nation + + + IT + + + + + + + + + + + + + + Sex + + + M + + + + + + + + + + + + + + Expiry Date + + + 22/02/2025 + + + + + + + + + + + + + + Document Number (TEAM) + + + 80380000000000000001 + + + + + + + + + + + + + + First Name + + + Casimira + + + + + + + + + + + + + + Fiscal Code + + + RSSMRA80A10H501A + + + + + + + + + + + + + + Family Name + + + Savoia + + + + + + + + + + + + + + Emessa da + + + Ragioneria Generale dello Stato + + + + + + + + + + + + + + + + + + + + + + + + Chi è? + + + + È l’ente riconosciuto dallo Stato a fornirti la versione digitale dei tuoi documenti. IO ti chiede di autorizzarne l'aggiunta sul tuo dispositivo. + + + + + + + + + + + + + + + + + Rilasciata da + + + Istituto Poligrafico e Zecca di Stato + + + + + + + + + + + + + Chi è? + + + + È l’ente riconosciuto dallo Stato a fornirti la versione digitale dei tuoi documenti. IO ti chiede di autorizzarne l'aggiunta sul tuo dispositivo. + + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx index b1632cb3968..712904bf6da 100644 --- a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx +++ b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx @@ -17,7 +17,6 @@ import I18n from "../../../../i18n"; import { identificationRequest } from "../../../../store/actions/identification"; import { useIODispatch } from "../../../../store/hooks"; import { useAvoidHardwareBackButton } from "../../../../utils/useAvoidHardwareBackButton"; -import { ItwCredentialClaimsList } from "../../common/components/ItwCredentialClaimList"; import { ItwGenericErrorContent } from "../../common/components/ItwGenericErrorContent"; import { useItwDisableGestureNavigation } from "../../common/hooks/useItwDisableGestureNavigation"; import { useItwDismissalDialog } from "../../common/hooks/useItwDismissalDialog"; @@ -30,6 +29,7 @@ import { selectIsIssuing } from "../../machine/credential/selectors"; import { ItwCredentialIssuanceMachineContext } from "../../machine/provider"; +import { ItwCredentialPreviewClaimsList } from "../components/ItwCredentialPreviewClaimsList"; export const ItwIssuanceCredentialPreviewScreen = () => { const credentialTypeOption = ItwCredentialIssuanceMachineContext.useSelector( @@ -119,7 +119,7 @@ const ContentView = ({ credentialType, credential }: ContentViewProps) => { })} - +
{ @@ -116,7 +116,7 @@ const ContentView = ({ eid }: ContentViewProps) => {

{I18n.t("features.itWallet.issuance.eidPreview.title")}

- +
{ {/* PLAYGROUNDS */} diff --git a/ts/features/itwallet/presentation/components/ItwCredentialTrustmark.tsx b/ts/features/itwallet/presentation/components/ItwCredentialTrustmark.tsx index 589ee35b926..fee2a5511e7 100644 --- a/ts/features/itwallet/presentation/components/ItwCredentialTrustmark.tsx +++ b/ts/features/itwallet/presentation/components/ItwCredentialTrustmark.tsx @@ -96,8 +96,6 @@ export const ItwCredentialTrustmark = ({ {trustmarkBottomSheet.bottomSheet} - {/* TODO: remove after merging #6154 */} - ); }; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationAdditionalInfoSection.tsx b/ts/features/itwallet/presentation/components/ItwPresentationAdditionalInfoSection.tsx new file mode 100644 index 00000000000..9b40b6137f8 --- /dev/null +++ b/ts/features/itwallet/presentation/components/ItwPresentationAdditionalInfoSection.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { CredentialType } from "../../common/utils/itwMocksUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; +import { ItwPresentationFiscalCode } from "./ItwPresentationFiscalCode"; + +type Props = { + credential: StoredCredential; +}; + +/** + * This component returns the additional information required by a credential details screen, which is not + * part of the credential claims + */ +const ItwPresentationAdditionalInfoSection = ({ credential }: Props) => { + switch (credential.credentialType) { + case CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD: + return ; + default: + return null; + } +}; + +const MemoizedItwPresentationAdditionalInfoSection = React.memo( + ItwPresentationAdditionalInfoSection +); + +export { MemoizedItwPresentationAdditionalInfoSection as ItwPresentationAdditionalInfoSection }; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationAlertsSection.tsx b/ts/features/itwallet/presentation/components/ItwPresentationAlertsSection.tsx index 8e9a6067bfe..c98022b38f4 100644 --- a/ts/features/itwallet/presentation/components/ItwPresentationAlertsSection.tsx +++ b/ts/features/itwallet/presentation/components/ItwPresentationAlertsSection.tsx @@ -27,6 +27,8 @@ export const ItwPresentationAlertsSection = ({ credential }: Props) => { }; const isMdl = credential.credentialType === CredentialType.DRIVING_LICENSE; + const isEhc = + credential.credentialType === CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD; const mdlDisclaimerBottomSheet = useIOBottomSheetAutoresizableModal({ title: I18n.t("features.itWallet.presentation.bottomSheets.mdl.title"), @@ -84,6 +86,18 @@ export const ItwPresentationAlertsSection = ({ credential }: Props) => { {mdlDisclaimerBottomSheet.bottomSheet} )} + {isEhc && ( + <> + + {mdlDisclaimerBottomSheet.bottomSheet} + + )} ); }; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx b/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx index 4e41ace9de2..4a76d94658a 100644 --- a/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx +++ b/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx @@ -1,23 +1,35 @@ -import { H6, IconButton } from "@pagopa/io-app-design-system"; -import React, { useState } from "react"; -import { StyleSheet, View } from "react-native"; +import { + Divider, + H6, + IconButton, + IOStyles +} from "@pagopa/io-app-design-system"; +import { default as React } from "react"; +import { View } from "react-native"; import I18n from "../../../../i18n"; -import { ItwCredentialClaimsList } from "../../common/components/ItwCredentialClaimList"; +import { ItwCredentialClaim } from "../../common/components/ItwCredentialClaim"; import { ItwQrCodeClaimImage } from "../../common/components/ItwQrCodeClaimImage"; +import { ItwReleaserName } from "../../common/components/ItwReleaserName"; +import { + getCredentialStatus, + parseClaims, + WellKnownClaim +} from "../../common/utils/itwClaimsUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; -type Props = { - title: string; - data: StoredCredential; - canHideValues?: boolean; +type ItwPresentationClaimsSectionProps = { + credential: StoredCredential; }; export const ItwPresentationClaimsSection = ({ - title, - canHideValues, - data -}: Props) => { - const [valuesHidden, setValuesHidden] = useState(false); + credential +}: ItwPresentationClaimsSectionProps) => { + const [valuesHidden, setValuesHidden] = React.useState(false); + const credentialStatus = getCredentialStatus(credential); + + const claims = parseClaims(credential.parsedCredential, { + exclude: [WellKnownClaim.unique_id, WellKnownClaim.content] + }); const renderHideValuesToggle = () => ( - -
{title}
- {canHideValues && renderHideValuesToggle()} -
- - + { + // If do not have claims, we should not render the title and the toggle + claims.length > 0 && ( + <> + +
+ {I18n.t( + "features.itWallet.presentation.credentialDetails.documentDataTitle" + )} +
+ {renderHideValuesToggle()} +
+ + ) + } + {claims.map((claim, index) => { + if (claim.id === WellKnownClaim.link_qr_code) { + // Since the `link_qr_code` claim difficult to distinguish from a generic image claim, we need to manually + // check for the claim and render it accordingly + return ; + } + + return ( + + {index !== 0 && } + + ); + })} + +
); }; - -const styles = StyleSheet.create({ - header: { - justifyContent: "space-between", - flexDirection: "row" - } -}); diff --git a/ts/features/itwallet/presentation/components/ItwPresentationCredentialCard.tsx b/ts/features/itwallet/presentation/components/ItwPresentationCredentialCard.tsx index c53db909e53..140496b04ac 100644 --- a/ts/features/itwallet/presentation/components/ItwPresentationCredentialCard.tsx +++ b/ts/features/itwallet/presentation/components/ItwPresentationCredentialCard.tsx @@ -8,10 +8,17 @@ import { StyleSheet, View } from "react-native"; import I18n from "../../../../i18n"; import { ItwCredentialCard } from "../../common/components/ItwCredentialCard"; import { ItwSkeumorphicCard } from "../../common/components/ItwSkeumorphicCard"; +import { getCredentialStatus } from "../../common/utils/itwClaimsUtils"; import { CredentialType } from "../../common/utils/itwMocksUtils"; import { getThemeColorByCredentialType } from "../../common/utils/itwStyleUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; -import { getCredentialStatus } from "../../common/utils/itwClaimsUtils"; + +/** + * Credentials that should display a skeumorphic card + */ +const credentialsWithSkeumorphicCard: ReadonlyArray = [ + CredentialType.DRIVING_LICENSE +]; type Props = { credential: StoredCredential; @@ -23,21 +30,23 @@ type Props = { */ const ItwPresentationCredentialCard = ({ credential }: Props) => { const [isFlipped, setIsFlipped] = React.useState(false); - const themeColor = getThemeColorByCredentialType( + + const { backgroundColor } = getThemeColorByCredentialType( credential.credentialType as CredentialType ); - const hasSkeumorphicCard = - credential.credentialType === CredentialType.DRIVING_LICENSE; - const credentialStatus = getCredentialStatus(credential); + const hasSkeumorphicCard = credentialsWithSkeumorphicCard.includes( + credential.credentialType + ); + if (hasSkeumorphicCard) { return ( - + - + { } return ( - + - + ); }; -type WrapperProps = { - children: React.ReactNode; +type CardContainerProps = { backgroundColor: string; }; -const Wrapper = ({ children, backgroundColor }: WrapperProps) => ( +const CardContainer = ({ + children, + backgroundColor +}: React.PropsWithChildren) => ( {children} diff --git a/ts/features/itwallet/presentation/components/ItwPresentationDetailFooter.tsx b/ts/features/itwallet/presentation/components/ItwPresentationDetailsFooter.tsx similarity index 70% rename from ts/features/itwallet/presentation/components/ItwPresentationDetailFooter.tsx rename to ts/features/itwallet/presentation/components/ItwPresentationDetailsFooter.tsx index 47788dd5ffd..6205bc7082e 100644 --- a/ts/features/itwallet/presentation/components/ItwPresentationDetailFooter.tsx +++ b/ts/features/itwallet/presentation/components/ItwPresentationDetailsFooter.tsx @@ -1,29 +1,40 @@ -import { ListItemAction, useIOToast } from "@pagopa/io-app-design-system"; +import { + ContentWrapper, + ListItemAction, + useIOToast, + VSpacer +} from "@pagopa/io-app-design-system"; import React from "react"; -import { Alert, Linking, View } from "react-native"; +import { Alert } from "react-native"; +import { useStartSupportRequest } from "../../../../hooks/useStartSupportRequest"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch } from "../../../../store/hooks"; -import { itwCredentialNameByCredentialType } from "../../common/utils/itwCredentialUtils"; +import { getCredentialNameFromType } from "../../common/utils/itwCredentialUtils"; import { CredentialType } from "../../common/utils/itwMocksUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; import { itwCredentialsRemove } from "../../credentials/store/actions"; -type Props = { +type ItwPresentationDetailFooterProps = { credential: StoredCredential; }; -export const ItwPresentationDetailFooter = ({ credential }: Props) => { +const ItwPresentationDetailsFooter = ({ + credential +}: ItwPresentationDetailFooterProps) => { const dispatch = useIODispatch(); const navigation = useIONavigation(); const toast = useIOToast(); + const startSupportRequest = useStartSupportRequest({ + faqCategories: [] + }); + const handleRemoveCredential = () => { dispatch(itwCredentialsRemove(credential)); toast.success( I18n.t("features.itWallet.presentation.credentialDetails.toast.removed", { - credentialName: - itwCredentialNameByCredentialType[credential.credentialType] + credentialName: getCredentialNameFromType(credential.credentialType) }) ); navigation.pop(); @@ -52,24 +63,19 @@ export const ItwPresentationDetailFooter = ({ credential }: Props) => { ] ); - const { federation_entity } = credential.issuerConf; - return ( - + + - Linking.openURL(`mailto:${federation_entity.contacts?.[0]}`) - } + onPress={() => startSupportRequest()} /> {credential.credentialType !== CredentialType.PID ? ( { onPress={showRemoveCredentialDialog} /> ) : null} - + ); }; + +const MemoizedItwPresentationDetailsFooter = React.memo( + ItwPresentationDetailsFooter +); + +export { MemoizedItwPresentationDetailsFooter as ItwPresentationDetailsFooter }; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationDetailsHeader.tsx b/ts/features/itwallet/presentation/components/ItwPresentationDetailsHeader.tsx new file mode 100644 index 00000000000..916443643e3 --- /dev/null +++ b/ts/features/itwallet/presentation/components/ItwPresentationDetailsHeader.tsx @@ -0,0 +1,107 @@ +import { + ContentWrapper, + IOVisualCostants, + makeFontStyleObject +} from "@pagopa/io-app-design-system"; +import React from "react"; +import { ImageSourcePropType, StyleSheet, Text, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AnimatedImage } from "../../../../components/AnimatedImage"; +import FocusAwareStatusBar from "../../../../components/ui/FocusAwareStatusBar"; +import { getCredentialNameFromType } from "../../common/utils/itwCredentialUtils"; +import { CredentialType } from "../../common/utils/itwMocksUtils"; +import { getThemeColorByCredentialType } from "../../common/utils/itwStyleUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; +import { ItwPresentationCredentialCard } from "./ItwPresentationCredentialCard"; + +type ItwPresentationDetailsHeaderProps = { credential: StoredCredential }; + +/** + * Credentials that should display a card + */ +const credentialsWithCard: ReadonlyArray = [ + CredentialType.PID, + CredentialType.DRIVING_LICENSE, + CredentialType.EUROPEAN_DISABILITY_CARD +]; + +/** + * Images for the header + */ +const credentialsHeader: Record = { + [CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD]: require("../../../../../img/features/itWallet/header/ts_header.png") +}; + +/** + * This component renders the header for the presentation details screen of a credential + * If the credential needs to show the card, it will render the card, otherwise it will render the header with the title + */ +const ItwPresentationDetailsHeader = ({ + credential +}: ItwPresentationDetailsHeaderProps) => { + const safeAreaInsets = useSafeAreaInsets(); + + const { backgroundColor, textColor, statusBarStyle } = + getThemeColorByCredentialType(credential.credentialType as CredentialType); + + const headerHeight = safeAreaInsets.top + IOVisualCostants.headerHeight; + + const headerContent = React.useMemo(() => { + if (credentialsWithCard.includes(credential.credentialType)) { + return ; + } + + return ( + + + + + {getCredentialNameFromType(credential.credentialType)} + + + + ); + }, [credential, backgroundColor, textColor, headerHeight]); + + return ( + + + {headerContent} + + ); +}; + +const styles = StyleSheet.create({ + headerImage: { + width: "100%", + position: "absolute" + }, + headerLabel: { + ...makeFontStyleObject("Semibold", false, "ReadexPro"), + fontSize: 26 + } +}); + +const MemoizedItwPresentationDetailsHeader = React.memo( + ItwPresentationDetailsHeader +); + +export { MemoizedItwPresentationDetailsHeader as ItwPresentationDetailsHeader }; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationDetailsScreenBase.tsx b/ts/features/itwallet/presentation/components/ItwPresentationDetailsScreenBase.tsx new file mode 100644 index 00000000000..8f3a7f74f1e --- /dev/null +++ b/ts/features/itwallet/presentation/components/ItwPresentationDetailsScreenBase.tsx @@ -0,0 +1,82 @@ +import { + IOSpacingScale, + IOVisualCostants, + buttonSolidHeight +} from "@pagopa/io-app-design-system"; +import React from "react"; +import Animated, { + useAnimatedRef, + useAnimatedScrollHandler, + useSharedValue +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; +import { getHeaderPropsByCredentialType } from "../../common/utils/itwStyleUtils"; +import { StoredCredential } from "../../common/utils/itwTypesUtils"; + +type ItwPresentationDetailsScreenBaseProps = { + credential: StoredCredential; + children?: React.ReactNode; +}; + +const scrollTriggerOffsetValue: number = 88; +const contentEndMargin: IOSpacingScale = 32; + +const ItwPresentationDetailsScreenBase = ({ + credential, + children +}: ItwPresentationDetailsScreenBaseProps) => { + const animatedScrollViewRef = useAnimatedRef(); + const safeAreaInsets = useSafeAreaInsets(); + const scrollTranslationY = useSharedValue(0); + + const bottomMargin: number = React.useMemo( + () => + safeAreaInsets.bottom === 0 + ? IOVisualCostants.appMarginDefault + : safeAreaInsets.bottom, + [safeAreaInsets] + ); + + const safeBottomAreaHeight: number = React.useMemo( + () => bottomMargin + buttonSolidHeight + contentEndMargin, + [bottomMargin] + ); + + const headerProps = getHeaderPropsByCredentialType(credential.credentialType); + + useHeaderSecondLevel({ + transparent: true, + scrollValues: { + triggerOffset: scrollTriggerOffsetValue, + contentOffsetY: scrollTranslationY + }, + supportRequest: true, + enableDiscreteTransition: true, + animatedRef: animatedScrollViewRef, + ...headerProps + }); + + const scrollHandler = useAnimatedScrollHandler(({ contentOffset }) => { + // eslint-disable-next-line functional/immutable-data + scrollTranslationY.value = contentOffset.y; + }); + + return ( + + {children} + + ); +}; + +export { ItwPresentationDetailsScreenBase }; diff --git a/ts/features/itwallet/presentation/components/ItwPresentationFiscalCode.tsx b/ts/features/itwallet/presentation/components/ItwPresentationFiscalCode.tsx new file mode 100644 index 00000000000..eb6196f26c1 --- /dev/null +++ b/ts/features/itwallet/presentation/components/ItwPresentationFiscalCode.tsx @@ -0,0 +1,58 @@ +import { + IOColors, + makeFontStyleObject, + useIOTheme +} from "@pagopa/io-app-design-system"; +import React from "react"; +import { StyleSheet, Text, View } from "react-native"; +import Barcode from "react-native-barcode-builder"; +import I18n from "../../../../i18n"; +import { useIOSelector } from "../../../../store/hooks"; +import { selectFiscalCodeFromEid } from "../../credentials/store/selectors"; + +const ItwPresentationFiscalCode = () => { + const fiscalCode = useIOSelector(selectFiscalCodeFromEid); + const theme = useIOTheme(); + + return ( + + + {I18n.t("features.itWallet.presentation.credentialDetails.fiscalCode")} + + + {fiscalCode} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + paddingVertical: 12 + }, + label: { + ...makeFontStyleObject("Regular", false, "TitilliumSansPro"), + fontSize: 14, + lineHeight: 21 + }, + value: { + ...makeFontStyleObject("Medium", false, "DMMono"), + fontSize: 16, + lineHeight: 24 + } +}); + +const MemoizedItwPresentationFiscalCode = React.memo(ItwPresentationFiscalCode); + +export { MemoizedItwPresentationFiscalCode as ItwPresentationFiscalCode }; diff --git a/ts/features/itwallet/presentation/components/__tests__/ItwPresentationClaimsSection.test.tsx b/ts/features/itwallet/presentation/components/__tests__/ItwPresentationClaimsSection.test.tsx index 4e223c43c93..5bd7d70d733 100644 --- a/ts/features/itwallet/presentation/components/__tests__/ItwPresentationClaimsSection.test.tsx +++ b/ts/features/itwallet/presentation/components/__tests__/ItwPresentationClaimsSection.test.tsx @@ -32,11 +32,7 @@ function renderComponent() { const globalState = appReducer(undefined, applicationChangeState("active")); return renderScreenWithNavigationStoreContext( () => ( - + ), ITW_ROUTES.PRESENTATION.CREDENTIAL_DETAIL, {}, diff --git a/ts/features/itwallet/presentation/components/__tests__/ItwPresentationDetailFooter.test.tsx b/ts/features/itwallet/presentation/components/__tests__/ItwPresentationDetailFooter.test.tsx index 16d02f38622..a28e51ca0a0 100644 --- a/ts/features/itwallet/presentation/components/__tests__/ItwPresentationDetailFooter.test.tsx +++ b/ts/features/itwallet/presentation/components/__tests__/ItwPresentationDetailFooter.test.tsx @@ -10,7 +10,7 @@ import { } from "../../../common/utils/itwMocksUtils"; import { ItwCredentialIssuanceMachineContext } from "../../../machine/provider"; import { ITW_ROUTES } from "../../../navigation/routes"; -import { ItwPresentationDetailFooter } from "../ItwPresentationDetailFooter"; +import { ItwPresentationDetailsFooter } from "../ItwPresentationDetailsFooter"; describe("ItwPresentationAlertsSection", () => { test.each([ @@ -38,7 +38,7 @@ const renderComponent = (credentialType: CredentialType) => { return renderScreenWithNavigationStoreContext( () => ( - - Personal data + Document data - + - - + Place of Birth + + - Place of Birth - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + + } + > - - + Institution Number (TEAM) + + - Institution Number (TEAM) - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + } + /> + - - - Date of Birth - - + Date of Birth + + - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + } + /> + - - + Province + + - Province - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + } + /> + - - + Nation + + - Nation - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + + } + > - - + Sex + + - Sex - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + } + /> + - - + Expiry Date + + - Expiry Date - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + + } + > - - + Document Number (TEAM) + + - Document Number (TEAM) - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + + } + > - - + First Name + + - First Name - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + + } + > - - + Fiscal Code + + - Fiscal Code - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + } + /> + - - + Family Name + + - Family Name - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + - - + + } + > - - + evidence + + - evidence - - - ****** - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + ****** + @@ -2562,7 +2538,7 @@ exports[`ItwPresentationClaimsSection should match the snapshot when claims are } weight="Semibold" > - Personal data + Document data - + - - + Place of Birth + + - Place of Birth - - - Rome (IT) - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + Rome (IT) + - - + + } + > - + - - + Institution Number (TEAM) + + - Institution Number (TEAM) - - - 500001 - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + 500001 + - - + + } + > - - + Date of Birth + + - Date of Birth - - - 06/01/1991 - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + 06/01/1991 + - - + } + /> + + - - + - Province - - - Rome - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + Rome + - - + + } + > - - + Nation + + - Nation - - - IT - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + IT + - - + + } + > - - + Sex + + - Sex - - - M - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + M + - - + + } + > - - + Expiry Date + + - Expiry Date - - - 22/02/2025 - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + 22/02/2025 + + + - - - Valid - - + Valid + - - + + } + > - - + Document Number (TEAM) + + - Document Number (TEAM) - - - 80380000000000000001 - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + 80380000000000000001 + - - + + } + > - - + First Name + + - First Name - - - Casimira - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + Casimira + - - + + } + > - - + Fiscal Code + + - Fiscal Code - - - RSSMRA80A10H501A - - + "lineHeight": 25, + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + RSSMRA80A10H501A + - - + + } + > - - + Family Name + + - Family Name - - - Savoia - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + Savoia + - - + + } + > - - + Emessa da + + - Emessa da - - - Ragioneria Generale dello Stato - - + }, + { + "color": "#0E0F13", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "600", + }, + ] + } + weight="Semibold" + > + Ragioneria Generale dello Stato + + + - - - - - - - + fillRule={0} + propList={ + [ + "fill", + "fillRule", + ] + } + /> + + - + + - - + + - + - - - Chi è? - - + Chi è? + + - + - È l’ente riconosciuto dallo Stato a fornirti la versione digitale dei tuoi documenti. IO ti chiede di autorizzarne l'aggiunta sul tuo dispositivo. - - - + "color": "#475A6D", + "fontFamily": "Titillium Sans Pro", + "fontStyle": "normal", + "fontWeight": "400", + }, + ] + } + weight="Regular" + > + È l’ente riconosciuto dallo Stato a fornirti la versione digitale dei tuoi documenti. IO ti chiede di autorizzarne l'aggiunta sul tuo dispositivo. + + - - - + + + { itwCredentialByTypeSelector(credentialType) ); - return pipe( - credentialOption, - O.fold( - () => , - credential => - ) - ); -}; - -type ContentProps = { credential: StoredCredential }; - -/** - * This component renders the entire credential detail. - */ -const ContentView = ({ credential }: ContentProps) => { - const { screenEndMargin } = useScreenEndMargin(); - const themeColor = getThemeColorByCredentialType( - credential.credentialType as CredentialType - ); - - useHeaderSecondLevel({ - title: "", - supportRequest: true, - variant: "contrast", - backgroundColor: themeColor - }); - useDebugInfo({ - parsedCredential: getHumanReadableParsedCredential( - credential.parsedCredential + parsedCredential: pipe( + credentialOption, + O.map(credential => + getHumanReadableParsedCredential(credential.parsedCredential) + ), + O.toUndefined ) }); + if (O.isNone(credentialOption)) { + // This is unlikely to happen, but we want to handle the case where the credential is not found + // because of inconsistencies in the state, and assert that the credential is O.some + return ; + } + + const credential = credentialOption.value; + return ( - <> - - - + + + - - - - - - - + + + + + + - - + + + ); };