From d15d93f3f28cf6641b9472837e610fcfbad10860 Mon Sep 17 00:00:00 2001 From: Alessandro Mazzon Date: Thu, 17 Oct 2024 17:44:26 +0200 Subject: [PATCH 1/3] feat(IT Wallet): [SIW-1584] Add IPZS privacy screen (#6270) ## Short description This PR adds a new screen regarding the IPZS privacy policy that must be reviewed before proceeding and logging in via SPID/CIE. ## List of changes proposed in this pull request - Added a new component used to display a `WebView` inside the `ItwIpzsPrivacyScreen` - Added a new machine state that handles the new screen inside the discovery flow - Removed the privacy policy acceptance text inside the `ItwIdentificationModeSelectionScreen` - Added the mixpanel tracking event inside the new screen ## How to test - Start the new documents flow from the payment section - Navigate to this screen after the onboarding/discovery screen (The first screen inside the WIA flow) - ~~Read the policy and scroll to the bottom to enable the continue button~~ - Scroll down the policy (not mandatory now) and press the continue button to navigate to the next screen **NOTE**: I cannot test this PR on a real Android device since I don't have one. Can someone test on Android (physical device)? Thanks! DEMO: https://github.com/user-attachments/assets/ba9781d3-dffa-4132-9274-cc43c6c7a991 --------- Co-authored-by: ITW Development Co-authored-by: Mario Perrotta Co-authored-by: LazyAfternoons --- .env.local | 2 + .env.production | 2 + locales/en/index.yml | 6 +- locales/it/index.yml | 6 +- ts/config.ts | 5 ++ .../components/ItwPrivacyWebViewComponent.tsx | 62 +++++++++++++++++++ .../screens/ItwIpzsPrivacyScreen.tsx | 60 ++++++++++++++++++ .../__tests__/ItwIpzsPrivacyScreen.test.tsx | 39 ++++++++++++ .../ItwIdentificationModeSelectionScreen.tsx | 6 -- .../machine/eid/__tests__/machine.test.ts | 20 ++++-- ts/features/itwallet/machine/eid/actions.ts | 6 ++ ts/features/itwallet/machine/eid/events.ts | 14 ++++- ts/features/itwallet/machine/eid/machine.ts | 21 ++++++- .../itwallet/navigation/ItwParamsList.ts | 1 + .../itwallet/navigation/ItwStackNavigator.tsx | 5 ++ ts/features/itwallet/navigation/routes.ts | 3 +- 16 files changed, 241 insertions(+), 17 deletions(-) create mode 100644 ts/features/itwallet/discovery/components/ItwPrivacyWebViewComponent.tsx create mode 100644 ts/features/itwallet/discovery/screens/ItwIpzsPrivacyScreen.tsx create mode 100644 ts/features/itwallet/discovery/screens/__tests__/ItwIpzsPrivacyScreen.test.tsx diff --git a/.env.local b/.env.local index 57dd5a0975e..a21c5e7226c 100644 --- a/.env.local +++ b/.env.local @@ -100,3 +100,5 @@ ITW_ISSUANCE_REDIRECT_URI_CIE="iowalletcie://cb" ITW_BYPASS_IDENTITY_MATCH=YES # Use the test environment for the IDP hint for both CIE and SPID ITW_IDP_HINT_TEST=YES +# IPZS Privacy Policy URL +ITW_IPZS_PRIVACY_URL='https://io.italia.it/informativa-ipzs' diff --git a/.env.production b/.env.production index 31a328f1413..5af2d48b6d1 100644 --- a/.env.production +++ b/.env.production @@ -100,3 +100,5 @@ ITW_ISSUANCE_REDIRECT_URI_CIE="iowalletcie://cb" ITW_BYPASS_IDENTITY_MATCH=NO # Use the test environment for the IDP hint for both CIE and SPID ITW_IDP_HINT_TEST=NO +# IPZS Privacy Policy URL +ITW_IPZS_PRIVACY_URL='https://io.italia.it/informativa-ipzs' diff --git a/locales/en/index.yml b/locales/en/index.yml index 1838936f988..ee4b5604c5b 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3215,6 +3215,11 @@ features: mdl: Patente di Guida dc: Carta Europea della Disabilità ts: Tessera Sanitaria - Tessera europea di assicurazione malattia + ipzsPrivacy: + title: I tuoi Documenti su IO sono al sicuro + warning: Premendo **Continua** dichiari di aver letto e compreso l’**Informativa Privacy**. + button: + label: Continua wallet: active: Attivo inactive: Non attivo @@ -3287,7 +3292,6 @@ features: cieId: title: CieID subtitle: Usa credenziali e app CieID - privacy: Identificandoti dichiari di aver letto e compreso l’[Informativa Privacy](https://io.italia.it/informativa-ipzs-sperimentazione) di **Istituto Poligrafico e Zecca dello Stato**. nfc: title: Attiva l'NFC per continuare description: Per consentire a IO di leggere la tua CIE, attiva l'NFC dalle Impostazioni del tuo dispositivo. diff --git a/locales/it/index.yml b/locales/it/index.yml index 76b01c44534..65fe02df8b9 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3215,6 +3215,11 @@ features: mdl: Patente di Guida dc: Carta Europea della Disabilità ts: Tessera Sanitaria - Tessera europea di assicurazione malattia + ipzsPrivacy: + title: I tuoi Documenti su IO sono al sicuro + warning: Premendo **Continua** dichiari di aver letto e compreso l’**Informativa Privacy**. + button: + label: Continua wallet: active: Attivo inactive: Non attivo @@ -3287,7 +3292,6 @@ features: cieId: title: CieID subtitle: Usa credenziali e app CieID - privacy: Identificandoti dichiari di aver letto e compreso l’[Informativa Privacy](https://io.italia.it/informativa-ipzs-sperimentazione) di **Istituto Poligrafico e Zecca dello Stato**. nfc: title: Attiva l'NFC per continuare description: Per consentire a IO di leggere la tua CIE, attiva l'NFC dalle Impostazioni del tuo dispositivo. diff --git a/ts/config.ts b/ts/config.ts index 21af74a22cc..3da3b40e938 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -255,3 +255,8 @@ export const itwEaaVerifierBaseUrl = Config.ITW_EAA_VERIFIER_BASE_URL; export const itwBypassIdentityMatch = Config.ITW_BYPASS_IDENTITY_MATCH === "YES"; export const itwIdpHintTest = Config.ITW_IDP_HINT_TEST === "YES"; +export const itwIpzsPrivacyUrl: string = pipe( + Config.ITW_IPZS_PRIVACY_URL, + t.string.decode, + E.getOrElse(() => "https://io.italia.it/informativa-ipzs") +); diff --git a/ts/features/itwallet/discovery/components/ItwPrivacyWebViewComponent.tsx b/ts/features/itwallet/discovery/components/ItwPrivacyWebViewComponent.tsx new file mode 100644 index 00000000000..3fd2391fbc3 --- /dev/null +++ b/ts/features/itwallet/discovery/components/ItwPrivacyWebViewComponent.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { + ContentWrapper, + IOStyles, + VSpacer +} from "@pagopa/io-app-design-system"; +import WebView from "react-native-webview"; +import { View } from "react-native"; +import { WebViewSource } from "react-native-webview/lib/WebViewTypes"; +import { AVOID_ZOOM_JS, closeInjectedScript } from "../../../../utils/webview"; +import I18n from "../../../../i18n"; +import ItwMarkdown from "../../common/components/ItwMarkdown"; +import { FooterActions } from "../../../../components/ui/FooterActions"; + +type Props = { + source: WebViewSource; + onAcceptTos: () => void; + onLoadEnd: () => void; + onError: () => void; +}; + +const ItwPrivacyWebViewComponent = ({ + source, + onAcceptTos, + onLoadEnd, + onError +}: Props) => ( + + + + + + + {I18n.t("features.itWallet.ipzsPrivacy.warning")} + + + + +); + +export default ItwPrivacyWebViewComponent; diff --git a/ts/features/itwallet/discovery/screens/ItwIpzsPrivacyScreen.tsx b/ts/features/itwallet/discovery/screens/ItwIpzsPrivacyScreen.tsx new file mode 100644 index 00000000000..32406b92f24 --- /dev/null +++ b/ts/features/itwallet/discovery/screens/ItwIpzsPrivacyScreen.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import { View } from "react-native"; +import { H2, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; +import I18n from "../../../../i18n"; +import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; +import ItwPrivacyWebViewComponent from "../components/ItwPrivacyWebViewComponent"; +import { ItwEidIssuanceMachineContext } from "../../machine/provider"; +import { trackOpenItwTosAccepted } from "../../analytics"; +import { itwIpzsPrivacyUrl } from "../../../../config"; + +const ItwIpzsPrivacyScreen = () => { + const [isLoading, setIsLoading] = useState(true); + const machineRef = ItwEidIssuanceMachineContext.useActorRef(); + + const handleContinuePress = () => { + trackOpenItwTosAccepted(); + machineRef.send({ type: "accept-ipzs-privacy" }); + }; + + const onLoadEnd = () => { + setIsLoading(false); + }; + + const onError = () => { + onLoadEnd(); + machineRef.send({ type: "error", scope: "ipzs-privacy" }); + }; + + useHeaderSecondLevel({ + title: "", + canGoBack: true, + supportRequest: true + }); + + return ( + + +

+ {I18n.t("features.itWallet.ipzsPrivacy.title")} +

+ +
+ +
+ ); +}; + +export default ItwIpzsPrivacyScreen; diff --git a/ts/features/itwallet/discovery/screens/__tests__/ItwIpzsPrivacyScreen.test.tsx b/ts/features/itwallet/discovery/screens/__tests__/ItwIpzsPrivacyScreen.test.tsx new file mode 100644 index 00000000000..b4a14986ebb --- /dev/null +++ b/ts/features/itwallet/discovery/screens/__tests__/ItwIpzsPrivacyScreen.test.tsx @@ -0,0 +1,39 @@ +import * as 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 { itwEidIssuanceMachine } from "../../../machine/eid/machine"; +import { ItwEidIssuanceMachineContext } from "../../../machine/provider"; +import { ITW_ROUTES } from "../../../navigation/routes"; +import ItwIpzsPrivacyScreen from "../ItwIpzsPrivacyScreen"; + +describe("Test ItwIpzsPrivacy screen", () => { + it("it should render the screen correctly", () => { + const component = renderComponent(); + expect(component).toBeTruthy(); + }); +}); + +const renderComponent = () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const logic = itwEidIssuanceMachine.provide({ + actions: { + onInit: jest.fn(), + navigateToTosScreen: () => undefined + } + }); + + return renderScreenWithNavigationStoreContext( + () => ( + + + + ), + ITW_ROUTES.DISCOVERY.IPZS_PRIVACY, + {}, + createStore(appReducer, globalState as any) + ); +}; diff --git a/ts/features/itwallet/identification/screens/ItwIdentificationModeSelectionScreen.tsx b/ts/features/itwallet/identification/screens/ItwIdentificationModeSelectionScreen.tsx index 549a841cc15..a76680d4df4 100644 --- a/ts/features/itwallet/identification/screens/ItwIdentificationModeSelectionScreen.tsx +++ b/ts/features/itwallet/identification/screens/ItwIdentificationModeSelectionScreen.tsx @@ -2,7 +2,6 @@ import { ContentWrapper, ListItemHeader, ModuleNavigation, - VSpacer, VStack } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; @@ -13,7 +12,6 @@ import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { cieFlowForDevServerEnabled } from "../../../cieLogin/utils"; import { ItwEidIssuanceMachineContext } from "../../machine/provider"; -import ItwMarkdown from "../../common/components/ItwMarkdown"; import { itwIsCieSupportedSelector } from "../store/selectors"; import { trackItWalletIDMethod, @@ -91,10 +89,6 @@ export const ItwIdentificationModeSelectionScreen = () => { onPress={handleCieIdPress} /> - - - {I18n.t("features.itWallet.identification.mode.privacy")} - ); diff --git a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts index d7260ef9fff..afaa8bcf279 100644 --- a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts +++ b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts @@ -20,6 +20,7 @@ const T_WIA: string = "abcdefg"; describe("itwEidIssuanceMachine", () => { const navigateToTosScreen = jest.fn(); + const navigateToIpzsPrivacyScreen = jest.fn(); const navigateToIdentificationModeScreen = jest.fn(); const navigateToIdpSelectionScreen = jest.fn(); const navigateToEidPreviewScreen = jest.fn(); @@ -54,6 +55,7 @@ describe("itwEidIssuanceMachine", () => { const mockedMachine = itwEidIssuanceMachine.provide({ actions: { navigateToTosScreen, + navigateToIpzsPrivacyScreen, navigateToIdentificationModeScreen, navigateToIdpSelectionScreen, navigateToEidPreviewScreen, @@ -167,10 +169,16 @@ describe("itwEidIssuanceMachine", () => { // Wallet instance creation and attestation obtainment success + // Navigate to ipzs privacy screen + expect(actor.getSnapshot().value).toStrictEqual("IpzsPrivacyAcceptance"); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + + // Accept IPZS privacy + actor.send({ type: "accept-ipzs-privacy" }); + // Navigate to identification mode selection expect(actor.getSnapshot().value).toStrictEqual({ UserIdentification: "ModeSelection" }); - expect(actor.getSnapshot().tags).toStrictEqual(new Set()); /** * Choose SPID as identification mode @@ -551,12 +559,16 @@ describe("itwEidIssuanceMachine", () => { actor.send({ type: "accept-tos" }); - expect(actor.getSnapshot().value).toStrictEqual({ - UserIdentification: "ModeSelection" - }); + expect(actor.getSnapshot().value).toStrictEqual("IpzsPrivacyAcceptance"); expect(actor.getSnapshot().tags).toStrictEqual(new Set([])); expect(createWalletInstance).toHaveBeenCalledTimes(0); expect(getWalletAttestation).toHaveBeenCalledTimes(0); + + // Accept IPZS privacy + actor.send({ type: "accept-ipzs-privacy" }); + expect(actor.getSnapshot().value).toStrictEqual({ + UserIdentification: "ModeSelection" + }); }); it("Should allow the user to add a new credential once eID issuance is complete", () => { diff --git a/ts/features/itwallet/machine/eid/actions.ts b/ts/features/itwallet/machine/eid/actions.ts index a3487ab4899..20f00d1b072 100644 --- a/ts/features/itwallet/machine/eid/actions.ts +++ b/ts/features/itwallet/machine/eid/actions.ts @@ -34,6 +34,12 @@ export const createEidIssuanceActionsImplementation = ( }); }, + navigateToIpzsPrivacyScreen: () => { + navigation.navigate(ITW_ROUTES.MAIN, { + screen: ITW_ROUTES.DISCOVERY.IPZS_PRIVACY + }); + }, + navigateToIdentificationModeScreen: () => { navigation.navigate(ITW_ROUTES.MAIN, { screen: ITW_ROUTES.IDENTIFICATION.MODE_SELECTION diff --git a/ts/features/itwallet/machine/eid/events.ts b/ts/features/itwallet/machine/eid/events.ts index 82876e88a94..973ca373558 100644 --- a/ts/features/itwallet/machine/eid/events.ts +++ b/ts/features/itwallet/machine/eid/events.ts @@ -15,6 +15,10 @@ export type AcceptTos = { type: "accept-tos"; }; +export type AcceptIpzsPrivacy = { + type: "accept-ipzs-privacy"; +}; + export type AddToWallet = { type: "add-to-wallet"; }; @@ -72,10 +76,17 @@ export type RevokeWalletInstance = { type: "revoke-wallet-instance"; }; +export type Error = { + type: "error"; + // Add a custom error code to the error event to distinguish between different errors. Add a new error code for each different error if needed. + scope: "ipzs-privacy"; +}; + export type EidIssuanceEvents = | Reset | Start | AcceptTos + | AcceptIpzsPrivacy | SelectIdentificationMode | SelectSpidIdp | CiePinEntered @@ -89,4 +100,5 @@ export type EidIssuanceEvents = | NfcEnabled | Abort | RevokeWalletInstance - | ErrorActorEvent; + | ErrorActorEvent + | Error; diff --git a/ts/features/itwallet/machine/eid/machine.ts b/ts/features/itwallet/machine/eid/machine.ts index 1807fb4833c..500a1d24f3d 100644 --- a/ts/features/itwallet/machine/eid/machine.ts +++ b/ts/features/itwallet/machine/eid/machine.ts @@ -28,6 +28,7 @@ export const itwEidIssuanceMachine = setup({ }, actions: { navigateToTosScreen: notImplemented, + navigateToIpzsPrivacyScreen: notImplemented, navigateToIdentificationModeScreen: notImplemented, navigateToIdpSelectionScreen: notImplemented, navigateToEidPreviewScreen: notImplemented, @@ -107,7 +108,7 @@ export const itwEidIssuanceMachine = setup({ target: "WalletInstanceAttestationObtainment" }, { - target: "UserIdentification" + target: "IpzsPrivacyAcceptance" } ] } @@ -173,7 +174,7 @@ export const itwEidIssuanceMachine = setup({ })), { type: "storeWalletInstanceAttestation" } ], - target: "UserIdentification" + target: "IpzsPrivacyAcceptance" }, onError: [ { @@ -187,6 +188,20 @@ export const itwEidIssuanceMachine = setup({ ] } }, + IpzsPrivacyAcceptance: { + description: + "This state handles the acceptance of the IPZS privacy policy", + entry: "navigateToIpzsPrivacyScreen", + on: { + "accept-ipzs-privacy": { + target: "UserIdentification" + }, + error: { + target: "#itwEidIssuanceMachine.Failure" + }, + back: "#itwEidIssuanceMachine.TosAcceptance" + } + }, UserIdentification: { description: "User identification flow. Once we get the user token we can continue to the eID issuance", @@ -215,7 +230,7 @@ export const itwEidIssuanceMachine = setup({ target: "#itwEidIssuanceMachine.UserIdentification.Completed" } ], - back: "#itwEidIssuanceMachine.TosAcceptance" + back: "#itwEidIssuanceMachine.IpzsPrivacyAcceptance" } }, Spid: { diff --git a/ts/features/itwallet/navigation/ItwParamsList.ts b/ts/features/itwallet/navigation/ItwParamsList.ts index a5d07b311ac..7949241a6fc 100644 --- a/ts/features/itwallet/navigation/ItwParamsList.ts +++ b/ts/features/itwallet/navigation/ItwParamsList.ts @@ -8,6 +8,7 @@ export type ItwParamsList = { [ITW_ROUTES.ONBOARDING]: undefined; // DISCOVERY [ITW_ROUTES.DISCOVERY.INFO]: undefined; + [ITW_ROUTES.DISCOVERY.IPZS_PRIVACY]: undefined; // IDENTIFICATION [ITW_ROUTES.IDENTIFICATION.MODE_SELECTION]: undefined; [ITW_ROUTES.IDENTIFICATION.IDP_SELECTION]: undefined; diff --git a/ts/features/itwallet/navigation/ItwStackNavigator.tsx b/ts/features/itwallet/navigation/ItwStackNavigator.tsx index c2283b20cae..854c727c1ff 100644 --- a/ts/features/itwallet/navigation/ItwStackNavigator.tsx +++ b/ts/features/itwallet/navigation/ItwStackNavigator.tsx @@ -33,6 +33,7 @@ import ItwPlayground from "../playgrounds/screens/ItwPlayground"; import { ItwPresentationCredentialAttachmentScreen } from "../presentation/screens/ItwPresentationCredentialAttachmentScreen"; import { ItwPresentationCredentialDetailScreen } from "../presentation/screens/ItwPresentationCredentialDetailScreen"; import { ItwIssuanceCredentialAsyncContinuationScreen } from "../issuance/screens/ItwIssuanceCredentialAsyncContinuationScreen"; +import ItwIpzsPrivacyScreen from "../discovery/screens/ItwIpzsPrivacyScreen"; import { ItwParamsList } from "./ItwParamsList"; import { ITW_ROUTES } from "./routes"; @@ -74,6 +75,10 @@ const InnerNavigator = () => { name={ITW_ROUTES.DISCOVERY.INFO} component={ItwDiscoveryInfoScreen} /> + {/* IDENTIFICATION */} Date: Thu, 17 Oct 2024 18:14:36 +0200 Subject: [PATCH 2/3] chore: [PE-737] CGN capitalize text apostrophe case (#6293) ## Short description This PR updates the `capitalize` function to correctly handle words with apostrophes, ensuring that words like _Dall'Ara_ remain _Dall'Ara_ and not _Dall'ara_. ## List of changes proposed in this pull request - Added new test cases to ensure the function works as expected with apostrophes. - Updated the `capitalize` function to correctly handle words with apostrophes. - Used RegExp.exec() to match and preserve leading and trailing spaces. ## How to test 1. Ensure your development environment is set up and the project is running. 2. Run the test suite to verify the changes 3. Check that all tests pass, including the new test cases for words with apostrophes and leading/trailing spaces. Or 1. Modify the CgnOwnershipInformation file with a hardcoded word containing an apostrophe. 2. Verify that the CgnOwnershipInformation component correctly capitalizes words with apostrophes when rendered. ##Preview ![Screenshot 2024-10-17 at 09 41 56](https://github.com/user-attachments/assets/637139cb-5dce-4d57-bd4f-10cd0490ed49) --------- Co-authored-by: Alessandro --- .../detail/CgnOwnershipInformation.tsx | 12 ++--- .../bonus/cgn/screens/CgnDetailScreen.tsx | 8 ++-- ts/utils/__tests__/string.test.ts | 29 +++++++++++- ts/utils/strings.ts | 44 +++++++++++++++++++ 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/ts/features/bonus/cgn/components/detail/CgnOwnershipInformation.tsx b/ts/features/bonus/cgn/components/detail/CgnOwnershipInformation.tsx index 0b8e4642b2c..5732b6a2fca 100644 --- a/ts/features/bonus/cgn/components/detail/CgnOwnershipInformation.tsx +++ b/ts/features/bonus/cgn/components/detail/CgnOwnershipInformation.tsx @@ -1,14 +1,14 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as React from "react"; import { Divider, ListItemHeader, ListItemInfo } from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as React from "react"; import I18n from "../../../../../i18n"; -import { profileSelector } from "../../../../../store/reducers/profile"; import { useIOSelector } from "../../../../../store/hooks"; -import { capitalize } from "../../../../../utils/strings"; +import { profileSelector } from "../../../../../store/reducers/profile"; +import { capitalizeTextName } from "../../../../../utils/strings"; /** * Renders the CGN ownership block for detail screen, including Owner's Fiscal Code (The current user logged in) @@ -23,12 +23,12 @@ const CgnOwnershipInformation = (): React.ReactElement => { & ReturnType; @@ -198,9 +198,9 @@ const CgnDetailScreen = (props: Props): React.ReactElement => { }} > {pot.isSome(currentProfile) - ? `${capitalize(currentProfile.value.name)} ${capitalize( - currentProfile.value.family_name - )}` + ? `${capitalizeTextName( + currentProfile.value.name + )} ${capitalizeTextName(currentProfile.value.family_name)}` : ""} } diff --git a/ts/utils/__tests__/string.test.ts b/ts/utils/__tests__/string.test.ts index d910798c418..b27a7a5c287 100644 --- a/ts/utils/__tests__/string.test.ts +++ b/ts/utils/__tests__/string.test.ts @@ -4,7 +4,8 @@ import { capitalize, isStringNullyOrEmpty, maybeNotNullyString, - formatBytesWithUnit + formatBytesWithUnit, + capitalizeTextName } from "../strings"; describe("capitalize", () => { @@ -116,3 +117,29 @@ describe("formatBytesWithUnit", () => { expect(formatBytesWithUnit(-1234)).toEqual("0 B"); }); }); + +describe("capitalizeTextName", () => { + it("should return a string where each word has the first char in uppercase", () => { + expect(capitalizeTextName("capitalize")).toEqual("Capitalize"); + }); + + it("should return a string where each word has the first char in uppercase even after an apostrophe", () => { + expect(capitalizeTextName("Capit'Alize")).toEqual("Capit'Alize"); + }); + + it("should return a string where each word has the first char in uppercase even after an apostrophe-2", () => { + expect(capitalizeTextName("capit'alize")).toEqual("Capit'Alize"); + }); + + it("should return a string where each word has the first char in uppercase even after an apostrophe-3", () => { + expect(capitalizeTextName("Capit'alize")).toEqual("Capit'Alize"); + }); + + it("should return a string where each word has the first char in uppercase even after an apostrophe-4", () => { + expect(capitalizeTextName("capit'Alize")).toEqual("Capit'Alize"); + }); + + it("should return a string where each word has the first char in uppercase even after an apostrophe-5", () => { + expect(capitalizeTextName("CAPIT'ALIZE")).toEqual("Capit'Alize"); + }); +}); diff --git a/ts/utils/strings.ts b/ts/utils/strings.ts index 82a4a11b29e..dc7bde8fef0 100644 --- a/ts/utils/strings.ts +++ b/ts/utils/strings.ts @@ -182,3 +182,47 @@ export const formatBytesWithUnit = (bytes: number) => { const unit = units[Math.min(i, units.length - 1)]; return `${value} ${unit}`; }; + +/** + * Capitalizes the first letter of each word in the given text, preserving leading and trailing spaces. + * Words are separated by the specified separator. + * Handles words with apostrophes by capitalizing the first letter of each sub-token. + * + * @param {string} text + * @param {string} [separator=" "] + * @returns {string} + * + * @example + * capitalizeTextName(" hello world "); // returns " Hello World " + * + * @example + * capitalizeTextName("d'angelo"); //returns "D'Angelo" + */ + +export const capitalizeTextName = ( + text: string, + separator: string = " " +): string => { + // Match leading and trailing spaces + const leadingSpacesMatch = /^\s*/.exec(text); + const trailingSpacesMatch = /\s*$/.exec(text); + + const leadingSpaces = leadingSpacesMatch ? leadingSpacesMatch[0] : ""; + const trailingSpaces = trailingSpacesMatch ? trailingSpacesMatch[0] : ""; + + // Capitalize the words between the separators + const capitalizedText = text + .trim() // Remove leading/trailing spaces for processing + .split(separator) + .map(token => + // Handle words with apostrophes + token + .split("'") + .map(subToken => _.upperFirst(subToken.toLowerCase())) + .join("'") + ) + .join(separator); + + // Re-add the leading and trailing spaces + return `${leadingSpaces}${capitalizedText}${trailingSpaces}`; +}; From 24a9b414d12d85449991e516f6037d3622d0e8d5 Mon Sep 17 00:00:00 2001 From: Gianluca Spada Date: Thu, 17 Oct 2024 20:03:53 +0200 Subject: [PATCH 3/3] feat(IT Wallet): [SIW-1742] Display credentials authentic source (#6294) > [!WARNING] > Depends on https://github.com/pagopa/io-react-native-wallet/pull/149 ## Short description This PR adds the authentic source name in the credential detail and preview. ## List of changes proposed in this pull request - Renamed `ItwReleaserName` in `ItwIssuanceMetadata` and refactored it - Added authentic source from EC - Added missing Mixpanel events - Updated `io-react-native-wallet` ## How to test See the credential detail and preview. --------- Co-authored-by: LazyAfternoons --- locales/en/index.yml | 5 +- locales/it/index.yml | 5 +- package.json | 2 +- .../common/components/ItwIssuanceMetadata.tsx | 160 ++++++++++++++++++ .../common/components/ItwReleaserName.tsx | 81 --------- .../ItwCredentialPreviewClaimsList.tsx | 4 +- .../ItwPresentationClaimsSection.tsx | 4 +- ...ItwPresentationClaimsSection.test.tsx.snap | 4 +- yarn.lock | 8 +- 9 files changed, 179 insertions(+), 94 deletions(-) create mode 100644 ts/features/itwallet/common/components/ItwIssuanceMetadata.tsx delete mode 100644 ts/features/itwallet/common/components/ItwReleaserName.tsx diff --git a/locales/en/index.yml b/locales/en/index.yml index ee4b5604c5b..a4753c8974f 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3255,11 +3255,11 @@ features: placeOfBirth: "Luogo di nascita" expirationDate: "Scadenza" securityLevel: "Livello di sicurezza" - issuedBy: "Credenziale emessa da" info: "Ulteriori info su questi dati" issuedByNew: "Emessa da" releasedBy: Emissione versione digitale attachments: "Attachments" + authenticSource: Origine dei dati mdl: category: "Licenza {{category}}" issuedDate: "Valida dal" @@ -3334,6 +3334,9 @@ features: about: title: "Chi è?" subtitle: "È l'ente riconosciuto dallo Stato a fornirti la versione digitale dei tuoi documenti." + authSource: + title: "Chi è?" + subtitle: "È l'ente che detiene i dati contenuti all'interno del tuo documento." actions: primary: Aggiungi al portafoglio secondary: Annulla diff --git a/locales/it/index.yml b/locales/it/index.yml index 65fe02df8b9..737990793fa 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3255,11 +3255,11 @@ features: placeOfBirth: "Luogo di nascita" expirationDate: "Scadenza" securityLevel: "Livello di sicurezza" - issuedBy: "Credenziale emessa da" info: "Ulteriori info su questi dati" issuedByNew: "Emessa da" releasedBy: Emissione versione digitale attachments: "Allegati" + authenticSource: Origine dei dati mdl: category: "Licenza {{category}}" issuedDate: "Valida dal" @@ -3334,6 +3334,9 @@ features: about: title: "Chi è?" subtitle: "È l'ente riconosciuto dallo Stato a fornirti la versione digitale dei tuoi documenti." + authSource: + title: "Chi è?" + subtitle: "È l'ente che detiene i dati contenuti all'interno del tuo documento." actions: primary: Aggiungi al portafoglio secondary: Annulla diff --git a/package.json b/package.json index 2c920879abb..23057755366 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "@pagopa/io-react-native-integrity": "^0.3.0", "@pagopa/io-react-native-jwt": "^1.2.0", "@pagopa/io-react-native-login-utils": "1.0.6", - "@pagopa/io-react-native-wallet": "^0.20.0", + "@pagopa/io-react-native-wallet": "^0.21.0", "@pagopa/io-react-native-zendesk": "^0.3.29", "@pagopa/react-native-cie": "^1.3.0", "@pagopa/ts-commons": "^10.15.0", diff --git a/ts/features/itwallet/common/components/ItwIssuanceMetadata.tsx b/ts/features/itwallet/common/components/ItwIssuanceMetadata.tsx new file mode 100644 index 00000000000..1ea4fc79baa --- /dev/null +++ b/ts/features/itwallet/common/components/ItwIssuanceMetadata.tsx @@ -0,0 +1,160 @@ +import { Divider, ListItemInfo } from "@pagopa/io-app-design-system"; +import React, { useMemo } from "react"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import I18n from "../../../../i18n"; +import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet"; +import { StoredCredential } from "../utils/itwTypesUtils"; +import { + CREDENTIALS_MAP, + trackWalletCredentialShowAuthSource, + trackWalletCredentialShowIssuer +} from "../../analytics"; + +type ItwIssuanceMetadataProps = { + credential: StoredCredential; + isPreview?: boolean; +}; + +type ItwMetadataIssuanceListItemProps = { + label: string; + value: string; + bottomSheet: { + contentTitle: string; + contentBody: string; + onPress: () => void; + }; + isPreview?: boolean; +}; + +const ItwMetadataIssuanceListItem = ({ + label, + value, + bottomSheet: bottomSheetProps, + isPreview +}: ItwMetadataIssuanceListItemProps) => { + const bottomSheet = useItwInfoBottomSheet({ + title: value, + content: [ + { + title: bottomSheetProps.contentTitle, + body: bottomSheetProps.contentBody + } + ] + }); + + const endElement: ListItemInfo["endElement"] = useMemo(() => { + if (isPreview) { + return; + } + + return { + type: "iconButton", + componentProps: { + icon: "info", + accessibilityLabel: `Info ${label}`, + onPress: () => { + bottomSheetProps.onPress(); + bottomSheet.present(); + } + } + }; + }, [isPreview, bottomSheet, bottomSheetProps, label]); + + return ( + <> + + {bottomSheet.bottomSheet} + + ); +}; + +const getAuthSource = (credential: StoredCredential) => + pipe( + credential.issuerConf.openid_credential_issuer + .credential_configurations_supported?.[credential.credentialType], + O.fromNullable, + O.map(config => config.authentic_source), + O.toUndefined + ); + +/** + * Renders additional issuance-related metadata, i.e. releaser and auth source. + * They are not part of the claims list, thus they're rendered separately. + * @param credential - the credential with the issuer configuration + * @param isPreview - whether the component is rendered in preview mode which hides the info button. + * @returns the list items with the metadata. + */ +export const ItwIssuanceMetadata = ({ + credential, + isPreview +}: ItwIssuanceMetadataProps) => { + const releaserName = + credential.issuerConf.federation_entity.organization_name; + const authSource = getAuthSource(credential); + + const releaserNameBottomSheet: ItwMetadataIssuanceListItemProps["bottomSheet"] = + useMemo( + () => ({ + contentTitle: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.about.title" + ), + contentBody: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.about.subtitle" + ), + onPress: () => + trackWalletCredentialShowIssuer( + CREDENTIALS_MAP[credential.credentialType] + ) + }), + [credential.credentialType] + ); + + const authSourceBottomSheet: ItwMetadataIssuanceListItemProps["bottomSheet"] = + useMemo( + () => ({ + contentTitle: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.authSource.title" + ), + contentBody: I18n.t( + "features.itWallet.issuance.credentialPreview.bottomSheet.authSource.subtitle" + ), + onPress: () => + trackWalletCredentialShowAuthSource( + CREDENTIALS_MAP[credential.credentialType] + ) + }), + [credential.credentialType] + ); + + return ( + <> + {authSource && ( + + )} + {authSource && releaserName && } + {releaserName && ( + + )} + + ); +}; diff --git a/ts/features/itwallet/common/components/ItwReleaserName.tsx b/ts/features/itwallet/common/components/ItwReleaserName.tsx deleted file mode 100644 index 1ddb3c064da..00000000000 --- a/ts/features/itwallet/common/components/ItwReleaserName.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { ListItemInfo } from "@pagopa/io-app-design-system"; -import React, { useCallback, useMemo } from "react"; -import I18n from "../../../../i18n"; -import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet"; -import { StoredCredential } from "../utils/itwTypesUtils"; -import { - CREDENTIALS_MAP, - trackWalletCredentialShowIssuer -} from "../../analytics"; - -type Props = { - credential: StoredCredential; - isPreview?: boolean; -}; - -/** - * 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. - * @param isPreview - whether the component is rendered in preview mode which hides the info button. - * @returns the list item with the releaser name. - */ -export const ItwReleaserName = ({ credential, isPreview }: 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" - ) - } - ] - }); - - const onPressWithMixpanelEvent = useCallback(() => { - trackWalletCredentialShowIssuer(CREDENTIALS_MAP[credential.credentialType]); - releasedByBottomSheet.present(); - }, [credential.credentialType, releasedByBottomSheet]); - - const endElement: ListItemInfo["endElement"] = useMemo(() => { - if (isPreview) { - return; - } - - return { - type: "iconButton", - componentProps: { - icon: "info", - accessibilityLabel: "Info", - onPress: onPressWithMixpanelEvent - } - }; - }, [isPreview, onPressWithMixpanelEvent]); - - if (!releaserName) { - return null; - } - - return ( - <> - - {releasedByBottomSheet.bottomSheet} - - ); -}; diff --git a/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx b/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx index 674dc54c2fa..9429a0366c3 100644 --- a/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx +++ b/ts/features/itwallet/issuance/components/ItwCredentialPreviewClaimsList.tsx @@ -2,7 +2,7 @@ import { Divider } from "@pagopa/io-app-design-system"; import React from "react"; import { View } from "react-native"; import { ItwCredentialClaim } from "../../common/components/ItwCredentialClaim"; -import { ItwReleaserName } from "../../common/components/ItwReleaserName"; +import { ItwIssuanceMetadata } from "../../common/components/ItwIssuanceMetadata"; import { parseClaims, WellKnownClaim } from "../../common/utils/itwClaimsUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; @@ -35,7 +35,7 @@ const ItwCredentialPreviewClaimsList = ({ {releaserVisible && ( <> - + )} diff --git a/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx b/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx index da9e3e5bb6d..e5c9e53bb96 100644 --- a/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx +++ b/ts/features/itwallet/presentation/components/ItwPresentationClaimsSection.tsx @@ -9,7 +9,7 @@ import { View } from "react-native"; import I18n from "../../../../i18n"; import { ItwCredentialClaim } from "../../common/components/ItwCredentialClaim"; import { ItwQrCodeClaimImage } from "../../common/components/ItwQrCodeClaimImage"; -import { ItwReleaserName } from "../../common/components/ItwReleaserName"; +import { ItwIssuanceMetadata } from "../../common/components/ItwIssuanceMetadata"; import { getCredentialStatus, parseClaims, @@ -81,7 +81,7 @@ export const ItwPresentationClaimsSection = ({ ); })} {claims.length > 0 && } - + ); }; diff --git a/ts/features/itwallet/presentation/components/__tests__/__snapshots__/ItwPresentationClaimsSection.test.tsx.snap b/ts/features/itwallet/presentation/components/__tests__/__snapshots__/ItwPresentationClaimsSection.test.tsx.snap index 08e08b3e5ce..9adfa7e5729 100644 --- a/ts/features/itwallet/presentation/components/__tests__/__snapshots__/ItwPresentationClaimsSection.test.tsx.snap +++ b/ts/features/itwallet/presentation/components/__tests__/__snapshots__/ItwPresentationClaimsSection.test.tsx.snap @@ -1887,7 +1887,7 @@ exports[`ItwPresentationClaimsSection should match the snapshot when claims are } >