From b207270adeb6b14bee81ee1e5a02344ff1b98aca Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Thu, 9 Jan 2025 17:02:05 +0100 Subject: [PATCH 1/3] chore(IT Wallet): [SIW-1933] Handle Trustmark QR code generation failures (#6558) ## Short description This PR adds the handling of Trustmark generation failures in the Trustmark QR Code screen ## List of changes proposed in this pull request - Added failure alert if QR Code generation fails - Added the ability to retry the QR Code generation with an exponential backoff timer - Updated tests ## How to test From the details screen of a credential, tap on the Trustmark button, check that everything works as expected. > [!TIP] > You can make the Trustmark generation fail by mocking the `getCredentialTrustmarkActor` function and make it throw Ensure that when the Trustmark generation fails, the screens shows an error alert with a "Retry" button. If you try to press the "Retry" button repeatedly, you should get an error toast telling you to wait a few seconds before trying to get the trustmark again. ## Preview --- locales/de/index.yml | 17 ++-- locales/en/index.yml | 25 ++++-- locales/it/index.yml | 23 ++++- ts/components/QrCodeImage.tsx | 6 +- .../components/ItwCredentialTrustmark.tsx | 8 +- .../ItwTrustmarkExpirationTimer.tsx | 52 +++++++++++ .../components/ItwTrustmarkQrCode.tsx | 79 +++++++++++++++++ .../machine/__tests__/machine.test.ts | 19 +++- .../itwallet/trustmark/machine/actions.ts | 84 +++++++++++++++--- .../itwallet/trustmark/machine/actors.ts | 16 +--- .../itwallet/trustmark/machine/context.ts | 13 +++ .../itwallet/trustmark/machine/events.ts | 7 ++ .../itwallet/trustmark/machine/failure.ts | 37 ++++++++ .../itwallet/trustmark/machine/machine.ts | 78 +++++++++++----- .../itwallet/trustmark/machine/provider.tsx | 4 +- .../itwallet/trustmark/machine/selectors.ts | 2 + .../screens/ItwCredentialTrustmarkScreen.tsx | 88 ++----------------- .../utils/__tests__/index.ts} | 9 +- .../utils/index.ts} | 9 +- ts/store/reducers/debug.ts | 14 ++- 20 files changed, 427 insertions(+), 163 deletions(-) create mode 100644 ts/features/itwallet/trustmark/components/ItwTrustmarkExpirationTimer.tsx create mode 100644 ts/features/itwallet/trustmark/components/ItwTrustmarkQrCode.tsx create mode 100644 ts/features/itwallet/trustmark/machine/events.ts create mode 100644 ts/features/itwallet/trustmark/machine/failure.ts rename ts/features/itwallet/{common/utils/__tests__/itwTrustmarkUtils.test.ts => trustmark/utils/__tests__/index.ts} (89%) rename ts/features/itwallet/{common/utils/itwTrustmarkUtils.ts => trustmark/utils/index.ts} (85%) diff --git a/locales/de/index.yml b/locales/de/index.yml index 63cabb837a2..a1edb6614a5 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -278,8 +278,8 @@ inbox: enableButton: "Posteingang aktivieren" disableButton: "Posteingang deaktivieren" enableCallToActionDescription: "Aktiviere den Posteingang, um Mitteilungen anzusehen" -settings: - informativeBanner: +settings: + informativeBanner: content: "Suchst du dein Profil? Wir haben es verschoben, es befindet sich jetzt in der oberen rechten Ecke!" action: "Geh zu den Einstellungen" profile: @@ -443,7 +443,7 @@ profile: body: "Wenn du dies bestätigst, können wir dir nicht gezielt helfen, da uns möglicherweise die notwendigen Daten zur Lösung des Problems fehlen." whyBottomSheet: title: "Warum wir Daten erfassen" - body: "Wir sammeln keine Daten für Marketingzwecke. \n\nWir verwenden Tools, die es uns ermöglichen, Daten darüber aufzuzeichnen, wie du die App nutzt, um **dir einen besseren Service zu bieten** und **dich bei Bedarf zu unterstützen**." + body: "Wir sammeln keine Daten für Marketingzwecke. \n\nWir verwenden Tools, die es uns ermöglichen, Daten darüber aufzuzeichnen, wie du die App nutzt, um **dir einen besseren Service zu bieten** und **dich bei Bedarf zu unterstützen**." securityBottomSheet: title: "Was ist eine Pseudonymisierung?" body: "Sie ist die Verarbeitung personenbezogener Daten in einer Weise, dass die personenbezogener Daten ohne Hinzuziehung zusätzlicher Informationen nicht mehr einer bestimmten betroffenen Person zugeordnet werden können, sofern diese zusätzlichen Informationen gesondert aufbewahrt werden und technischen und organisatorischen Maßnahmen unterliegen, die gewährleisten, dass diese personenbezogener Daten nicht einer identifizierten oder identifizierbaren natürlichen Person zugewiesen werden können." @@ -2711,9 +2711,9 @@ features: delete: successful: "Die Quittung wurde ausgeblendet" failed: "Es ist ein Fehler aufgetreten, bitte versuche es erneut" - hideBanner: + hideBanner: title: "Möchtest du diese Quittung aus deiner Liste ausblenden?" - content: "Dieser Vorgang ist unwiderruflich. Die Quittung wird nicht mehr in deiner Quittungsliste angezeigt." + content: "Dieser Vorgang ist unwiderruflich. Die Quittung wird nicht mehr in deiner Quittungsliste angezeigt." accept: "Ja, ausblenden" details: totalFeeUnknown: "Der Gesamtbetrag enthält keine Provisionskosten: du findest diese in dem Dokument, das du von {{pspName}} erhalten hast." @@ -2731,7 +2731,7 @@ features: empty: title: "Keine Quittung gefunden" subtitle: "Wenn du eine Quittung für eine pagoPA-Zahlungsmitteilung suchst, die du in der Vergangenheit bezahlt hast, wende dich an den Gläubiger." - emptyPayer: + emptyPayer: title: "Hier findest du Quittungen für Zahlungen, die mit der App getätigt wurden." details: payPal: @@ -3023,13 +3023,12 @@ features: content: "Dies ist ein notwendiger Sicherheitsschritt, um das Dokument '{{credentialName}}' in IO weiter zu verwenden." primaryAction: "Start" ctas: + trustmark: "Echtheitszertifikat anzeigen" openPdf: "Dokument anzeigen" shareButton: "Speichern oder freigeben" fiscalCode: "Deine Steuernummer" trustmark: - cta: "Echtheitszertifikat anzeigen" description: "Zeige den QR-Code vor, um die Echtheit des Dokuments zu bestätigen, wenn du dazu aufgefordert wirst." - expiration: "Der QR-Code erneuert sich in" qrCode: "QR-Code zur Authentizität von Dokumenten" walletRevocation: cta: "Dokumente in IO deaktivieren" @@ -3053,7 +3052,7 @@ features: cta: "Mehr erfahren" closeButton: "Schließen" closeButtonAlt: "Verstanden" - revokedByWalletProvider: + revokedByWalletProvider: title: "Dokumente in IO wurde deaktiviert" content: "Um die Voraussetzungen für die weitere Nutzung der Funktionen auf deinem Gerät zu prüfen, tippe auf 'Mehr erfahren'." newWalletInstanceCreated: diff --git a/locales/en/index.yml b/locales/en/index.yml index 828dbc29f7c..26f4ccddb42 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -57,6 +57,15 @@ date: meridian: - "am" - "pm" + time: + minutes: + zero: "0 minutes" + one: "1 minute" + other: "{{count}} minutes" + seconds: + zero: "0 seconds" + one: "1 second" + other: "{{count}} seconds" global: why: "Perché?" you: "YOU" @@ -3240,7 +3249,7 @@ features: empty: title: Nessuna ricevuta trovata subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore. - emptyPayer: + emptyPayer: title: Qui vedrai le ricevute dei pagamenti fatti in app details: payPal: @@ -3536,7 +3545,8 @@ features: content: È un passaggio di sicurezza necessario per continuare ad usare la tua {{credentialName}} su IO. primaryAction: Inizia ctas: - openPdf: "Show document" + trustmark: Mostra certificato di autenticità + openPdf: Show document shareButton: Save or share fiscalCode: Your Fiscal Code statusAttestationUnknown: @@ -3544,10 +3554,15 @@ features: content: Chiudi e riapri l'app per riprovare. primaryAction: Ho capito trustmark: - cta: Mostra certificato di autenticità description: Mostra il QR Code per attestare l’autenticità del documento quando ti viene richiesto. - expiration: Il QR Code si rinnova tra qrCode: QR code autenticità credenziale + timer: + expiresIn: Il QR Code si rinnova tra **{{time}}** + expired: Il QR Code è scaduto + failure: + description: Abbiamo avuto un problema nel generare il nuovo QR Code. + action: Riprova + toast: "Puoi riprovare tra {{time}}" walletRevocation: cta: Disattiva Documenti su IO confirmScreen: @@ -3570,7 +3585,7 @@ features: cta: Scopri di più closeButton: Chiudi closeButtonAlt: Ho capito - revokedByWalletProvider: + revokedByWalletProvider: title: Documenti su IO è stata disattivata content: Per verificare i requisiti richiesti per continuare a usare la funzionalità sul tuo dispositivo, premi "Scopri di più". newWalletInstanceCreated: diff --git a/locales/it/index.yml b/locales/it/index.yml index 297a8225a7d..1305f0d0adb 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -57,6 +57,15 @@ date: meridian: - "am" - "pm" + time: + minutes: + zero: "0 minuti" + one: "1 minuto" + other: "{{count}} minuti" + seconds: + zero: "0 secondi" + one: "1 secondo" + other: "{{count}} secondi" global: why: "Perché?" you: "TU" @@ -3240,7 +3249,7 @@ features: empty: title: Nessuna ricevuta trovata subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore. - emptyPayer: + emptyPayer: title: Qui vedrai le ricevute dei pagamenti fatti in app details: payPal: @@ -3536,6 +3545,7 @@ features: content: È un passaggio di sicurezza necessario per continuare ad usare la tua {{credentialName}} su IO. primaryAction: Inizia ctas: + trustmark: Mostra certificato di autenticità openPdf: "Mostra documento" shareButton: Salva o condividi fiscalCode: Il tuo Codice Fiscale @@ -3544,10 +3554,15 @@ features: content: Chiudi e riapri l'app per riprovare. primaryAction: Ho capito trustmark: - cta: Mostra certificato di autenticità description: Mostra il QR Code per attestare l’autenticità del documento quando ti viene richiesto. - expiration: Il QR Code si rinnova tra qrCode: QR code autenticità credenziale + timer: + expiresIn: Il QR Code si rinnova tra **{{time}}** + expired: Il QR Code è scaduto + failure: + description: Abbiamo avuto un problema nel generare il nuovo QR Code. + action: Riprova + toast: "Puoi riprovare tra {{time}}" walletRevocation: cta: Disattiva Documenti su IO confirmScreen: @@ -3570,7 +3585,7 @@ features: cta: Scopri di più closeButton: Chiudi closeButtonAlt: Ho capito - revokedByWalletProvider: + revokedByWalletProvider: title: Documenti su IO è stata disattivata content: Per verificare i requisiti richiesti per continuare a usare la funzionalità sul tuo dispositivo, premi "Scopri di più". newWalletInstanceCreated: diff --git a/ts/components/QrCodeImage.tsx b/ts/components/QrCodeImage.tsx index aed11f8fe8f..c9c5c00bf0c 100644 --- a/ts/components/QrCodeImage.tsx +++ b/ts/components/QrCodeImage.tsx @@ -15,6 +15,8 @@ export type QrCodeImageProps = { correctionLevel?: QRCodeProps["ecl"]; // Accessibility accessibilityLabel?: string; + // Callback to handle the error if the QR Code generation fails + onError?: (error: Error) => void; }; const defaultAccessibilityLabel = "QR Code"; @@ -27,7 +29,8 @@ const QrCodeImage = ({ size = 200, backgroundColor, correctionLevel = "H", - accessibilityLabel = defaultAccessibilityLabel + accessibilityLabel = defaultAccessibilityLabel, + onError }: QrCodeImageProps) => { const { width } = useWindowDimensions(); const realSize = React.useMemo(() => { @@ -49,6 +52,7 @@ const QrCodeImage = ({ size={realSize} ecl={correctionLevel} backgroundColor={backgroundColor} + onError={onError} /> ) : ( diff --git a/ts/features/itwallet/trustmark/components/ItwCredentialTrustmark.tsx b/ts/features/itwallet/trustmark/components/ItwCredentialTrustmark.tsx index 10452e35a72..7f2a4497b32 100644 --- a/ts/features/itwallet/trustmark/components/ItwCredentialTrustmark.tsx +++ b/ts/features/itwallet/trustmark/components/ItwCredentialTrustmark.tsx @@ -300,7 +300,9 @@ export const ItwCredentialTrustmark = ({ onPress={onPressWithTrackEvent} testID={testID} accessible={true} - accessibilityLabel={I18n.t("features.itWallet.trustmark.cta")} + accessibilityLabel={I18n.t( + "features.itWallet.presentation.ctas.trustmark" + )} accessibilityRole="button" onPressIn={onPressIn} onPressOut={onPressOut} @@ -321,7 +323,9 @@ export const ItwCredentialTrustmark = ({ - {I18n.t("features.itWallet.trustmark.cta").toUpperCase()} + {I18n.t( + "features.itWallet.presentation.ctas.trustmark" + ).toUpperCase()} {!enableIridescence && ( { + const expirationSeconds = ItwTrustmarkMachineContext.useSelector( + selectExpirationSeconds + ); + const failure = ItwTrustmarkMachineContext.useSelector(selectFailure); + + useDebugInfo({ expirationSeconds, failure }); + + const content = useMemo(() => { + if (failure) { + return {I18n.t("features.itWallet.trustmark.timer.expired")}; + } + + if (expirationSeconds === undefined) { + return ( + + ); + } + + return ( + + ); + }, [failure, expirationSeconds]); + + return ( + + + + {content} + + ); +}; + +export { ItwTrustmarkExpirationTimer }; diff --git a/ts/features/itwallet/trustmark/components/ItwTrustmarkQrCode.tsx b/ts/features/itwallet/trustmark/components/ItwTrustmarkQrCode.tsx new file mode 100644 index 00000000000..33e98771c83 --- /dev/null +++ b/ts/features/itwallet/trustmark/components/ItwTrustmarkQrCode.tsx @@ -0,0 +1,79 @@ +import { + Body, + ButtonLink, + Icon, + IOColors, + IOVisualCostants +} from "@pagopa/io-app-design-system"; +import React, { useCallback, useMemo } from "react"; +import { StyleSheet, View } from "react-native"; +import { QrCodeImage } from "../../../../components/QrCodeImage"; +import { useDebugInfo } from "../../../../hooks/useDebugInfo"; +import I18n from "../../../../i18n"; +import { ItwTrustmarkMachineContext } from "../machine/provider"; +import { selectFailure, selectTrustmarkUrl } from "../machine/selectors"; + +/** + * Component that renders the QR code of the trustmark + */ +const ItwTrustmarkQrCode = () => { + const machineRef = ItwTrustmarkMachineContext.useActorRef(); + const trustmarkUrl = + ItwTrustmarkMachineContext.useSelector(selectTrustmarkUrl); + const failure = ItwTrustmarkMachineContext.useSelector(selectFailure); + + useDebugInfo({ trustmarkUrl, failure }); + + const handleOnRetry = useCallback(() => { + machineRef.send({ type: "retry" }); + }, [machineRef]); + + const content = useMemo(() => { + if (failure) { + return ( + + + + {I18n.t("features.itWallet.trustmark.failure.description")} + + + + + + ); + } + + return ( + + ); + }, [failure, trustmarkUrl, handleOnRetry]); + + return {content}; +}; + +const styles = StyleSheet.create({ + container: { + marginHorizontal: -IOVisualCostants.appMarginDefault, + alignItems: "center" + }, + alert: { + backgroundColor: IOColors["grey-50"], + width: "92%", + alignItems: "center", + justifyContent: "center", + aspectRatio: 1, + padding: 16, + borderRadius: 16, + gap: 8 + } +}); + +export { ItwTrustmarkQrCode }; diff --git a/ts/features/itwallet/trustmark/machine/__tests__/machine.test.ts b/ts/features/itwallet/trustmark/machine/__tests__/machine.test.ts index 4be224e66e5..3a57dbe5be1 100644 --- a/ts/features/itwallet/trustmark/machine/__tests__/machine.test.ts +++ b/ts/features/itwallet/trustmark/machine/__tests__/machine.test.ts @@ -15,7 +15,9 @@ import { import { itwTrustmarkMachine } from "../machine"; const onInit = jest.fn(); +const storeWalletInstanceAttestation = jest.fn(); const handleSessionExpired = jest.fn(); +const showRetryFailureToast = jest.fn(); const getWalletAttestationActor = jest.fn(); const getCredentialTrustmarkActor = jest.fn(); @@ -26,7 +28,9 @@ const hasValidWalletInstanceAttestation = jest.fn(); const mockedMachine = itwTrustmarkMachine.provide({ actions: { onInit: assign(onInit), - handleSessionExpired + storeWalletInstanceAttestation, + handleSessionExpired, + showRetryFailureToast }, actors: { getWalletAttestationActor: fromPromise( @@ -117,7 +121,9 @@ describe("itwTrustmarkMachine", () => { credential: ItwStoredCredentialsMocks.mdl, trustmarkUrl: "T_URL", expirationDate: new Date(1732099618000 + 10 * 1000), - expirationSeconds: 10 + expirationSeconds: 10, + attempts: undefined, + nextAttemptAt: undefined }); expect(actor.getSnapshot().tags).toStrictEqual(new Set()); @@ -134,7 +140,9 @@ describe("itwTrustmarkMachine", () => { credential: ItwStoredCredentialsMocks.mdl, trustmarkUrl: undefined, expirationDate: undefined, - expirationSeconds: undefined + expirationSeconds: undefined, + attempts: undefined, + nextAttemptAt: undefined }); expect(actor.getSnapshot().tags).toStrictEqual(new Set([ItwTags.Loading])); @@ -317,7 +325,10 @@ describe("itwTrustmarkMachine", () => { expect(actor.getSnapshot().context).toStrictEqual({ credentialType: "MDL", walletInstanceAttestation: "T_WIA", - credential: ItwStoredCredentialsMocks.mdl + credential: ItwStoredCredentialsMocks.mdl, + attempts: 1, + failure: expect.anything(), + nextAttemptAt: new Date(1732099618000 + 1 * 1000) }); expect(actor.getSnapshot().tags).toStrictEqual(new Set()); }); diff --git a/ts/features/itwallet/trustmark/machine/actions.ts b/ts/features/itwallet/trustmark/machine/actions.ts index 145a4ba00fd..a99add0561f 100644 --- a/ts/features/itwallet/trustmark/machine/actions.ts +++ b/ts/features/itwallet/trustmark/machine/actions.ts @@ -1,29 +1,52 @@ +import { useIOToast } from "@pagopa/io-app-design-system"; +import { differenceInSeconds } from "date-fns"; import * as O from "fp-ts/lib/Option"; -import { AnyEventObject, assign } from "xstate"; +import { ActionArgs, assign } from "xstate"; +import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { checkCurrentSession } from "../../../../store/actions/authentication"; import { useIOStore } from "../../../../store/hooks"; +import { assert } from "../../../../utils/assert"; import { itwCredentialByTypeSelector } from "../../credentials/store/selectors"; +import { itwWalletInstanceAttestationStore } from "../../walletInstance/store/actions"; import { itwWalletInstanceAttestationSelector } from "../../walletInstance/store/selectors"; import { Context } from "./context"; +import { TrustmarkEvents } from "./events"; export const createItwTrustmarkActionsImplementation = ( store: ReturnType, - navigation: ReturnType + navigation: ReturnType, + toast: ReturnType ) => { /** * Initializes the trustmark machine */ - const onInit = assign( - ({ context }) => ({ - walletInstanceAttestation: itwWalletInstanceAttestationSelector( - store.getState() - ), - credential: O.toUndefined( - itwCredentialByTypeSelector(context.credentialType)(store.getState()) - ) - }) - ); + const onInit = assign< + Context, + TrustmarkEvents, + unknown, + TrustmarkEvents, + any + >(({ context }) => ({ + walletInstanceAttestation: itwWalletInstanceAttestationSelector( + store.getState() + ), + credential: O.toUndefined( + itwCredentialByTypeSelector(context.credentialType)(store.getState()) + ) + })); + + const storeWalletInstanceAttestation = ({ + context + }: ActionArgs) => { + assert( + context.walletInstanceAttestation, + "walletInstanceAttestation is undefined" + ); + store.dispatch( + itwWalletInstanceAttestationStore(context.walletInstanceAttestation) + ); + }; /** * Handles the session expired event by dispatching the session expired action and navigating back to the credential details screen @@ -33,5 +56,40 @@ export const createItwTrustmarkActionsImplementation = ( navigation.pop(); }; - return { onInit, handleSessionExpired }; + /** + * Shows a failure toast + */ + const showRetryFailureToast = ({ + context + }: ActionArgs) => { + const timeDiffInSeconds = differenceInSeconds( + context.nextAttemptAt || new Date(), + new Date() + ); + + const time = + timeDiffInSeconds > 60 + ? Math.ceil(timeDiffInSeconds / 60) + : timeDiffInSeconds; + + const timeUnit = timeDiffInSeconds > 60 ? "minutes" : "seconds"; + + const timeString = I18n.t(`date.time.${timeUnit}`, { + defaultValue: I18n.t(`date.time.${timeUnit}.other`, { count: time }), + count: time + }); + + toast.error( + I18n.t("features.itWallet.trustmark.failure.toast", { + time: timeString + }) + ); + }; + + return { + onInit, + storeWalletInstanceAttestation, + handleSessionExpired, + showRetryFailureToast + }; }; diff --git a/ts/features/itwallet/trustmark/machine/actors.ts b/ts/features/itwallet/trustmark/machine/actors.ts index f317f8b9905..f037fbb5bc9 100644 --- a/ts/features/itwallet/trustmark/machine/actors.ts +++ b/ts/features/itwallet/trustmark/machine/actors.ts @@ -5,10 +5,9 @@ import { useIOStore } from "../../../../store/hooks"; import { sessionTokenSelector } from "../../../../store/reducers/authentication"; import { assert } from "../../../../utils/assert"; import * as itwAttestationUtils from "../../common/utils/itwAttestationUtils"; -import { getCredentialTrustmark } from "../../common/utils/itwTrustmarkUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors"; -import { itwWalletInstanceAttestationStore } from "../../walletInstance/store/actions"; +import * as itwTrustmarkUtils from "../utils"; export type GetWalletAttestationActorOutput = Awaited< ReturnType @@ -20,7 +19,7 @@ export type GetCredentialTrustmarkUrlActorInput = { }; export type GetCredentialTrustmarkUrlActorOutput = Awaited< - ReturnType + ReturnType >; /** @@ -45,17 +44,10 @@ export const createItwTrustmarkActorsImplementation = ( /** * Get the wallet instance attestation */ - const wia = await itwAttestationUtils.getAttestation( + return await itwAttestationUtils.getAttestation( integrityKeyTag.value, sessionToken ); - - /** - * Store the wallet instance attestation in the store to persist it across sessions - */ - store.dispatch(itwWalletInstanceAttestationStore(wia)); - - return wia; }); const getCredentialTrustmarkActor = fromPromise< @@ -69,7 +61,7 @@ export const createItwTrustmarkActorsImplementation = ( assert(input.credential, "credential is undefined"); // Generate trustmark url to be presented - return await getCredentialTrustmark( + return await itwTrustmarkUtils.getCredentialTrustmark( input.walletInstanceAttestation, input.credential, itwEaaVerifierBaseUrl diff --git a/ts/features/itwallet/trustmark/machine/context.ts b/ts/features/itwallet/trustmark/machine/context.ts index e9c4e4fe865..9da11ab7a94 100644 --- a/ts/features/itwallet/trustmark/machine/context.ts +++ b/ts/features/itwallet/trustmark/machine/context.ts @@ -1,4 +1,5 @@ import { StoredCredential } from "../../common/utils/itwTypesUtils"; +import { TrustmarkFailure } from "./failure"; export type Context = { /** @@ -25,4 +26,16 @@ export type Context = { * The trustmark url */ trustmarkUrl?: string; + /** + * The failure of the trustmark machine + */ + failure?: TrustmarkFailure; + /** + * The number of attempts made by the user to get the trustmark + */ + attempts?: number; + /** + * Time after which the user can attempt again to get the trustmark + */ + nextAttemptAt?: Date; }; diff --git a/ts/features/itwallet/trustmark/machine/events.ts b/ts/features/itwallet/trustmark/machine/events.ts new file mode 100644 index 00000000000..dd60cf52169 --- /dev/null +++ b/ts/features/itwallet/trustmark/machine/events.ts @@ -0,0 +1,7 @@ +import { ErrorActorEvent } from "xstate"; + +export type Retry = { + type: "retry"; +}; + +export type TrustmarkEvents = Retry | ErrorActorEvent; diff --git a/ts/features/itwallet/trustmark/machine/failure.ts b/ts/features/itwallet/trustmark/machine/failure.ts new file mode 100644 index 00000000000..6143a4b5f8d --- /dev/null +++ b/ts/features/itwallet/trustmark/machine/failure.ts @@ -0,0 +1,37 @@ +import { Errors } from "@pagopa/io-react-native-wallet"; +import { TrustmarkEvents } from "./events"; + +const { isWalletProviderResponseError } = Errors; + +export enum TrustmarkFailureType { + UNEXPECTED = "UNEXPECTED", + WALLET_PROVIDER_GENERIC = "WALLET_PROVIDER_GENERIC" +} + +export type TrustmarkFailure = { + type: TrustmarkFailureType; + reason: unknown; +}; + +export const mapEventToFailure = (event: TrustmarkEvents): TrustmarkFailure => { + if (!("error" in event)) { + return { + type: TrustmarkFailureType.UNEXPECTED, + reason: event + }; + } + + const { error } = event; + + if (isWalletProviderResponseError(error)) { + return { + type: TrustmarkFailureType.WALLET_PROVIDER_GENERIC, + reason: String(error) + }; + } + + return { + type: TrustmarkFailureType.UNEXPECTED, + reason: String(error) + }; +}; diff --git a/ts/features/itwallet/trustmark/machine/machine.ts b/ts/features/itwallet/trustmark/machine/machine.ts index 89db76fbb6b..b6b16cfea75 100644 --- a/ts/features/itwallet/trustmark/machine/machine.ts +++ b/ts/features/itwallet/trustmark/machine/machine.ts @@ -1,5 +1,5 @@ -import { differenceInSeconds, isPast } from "date-fns"; -import { assign, fromPromise, setup } from "xstate"; +import { addSeconds, differenceInSeconds, isPast } from "date-fns"; +import { assign, fromPromise, not, setup } from "xstate"; import { ItwTags } from "../../machine/tags"; import { GetCredentialTrustmarkUrlActorInput, @@ -7,20 +7,28 @@ import { GetWalletAttestationActorOutput } from "./actors"; import { Context } from "./context"; +import { TrustmarkEvents } from "./events"; +import { mapEventToFailure } from "./failure"; import { Input } from "./input"; const notImplemented = () => { throw new Error("Not implemented"); }; +/** + * Amount in seconds to wait before retrying + */ +const backoffTimeAmounts = [1, 10, 60, 180]; + export const itwTrustmarkMachine = setup({ types: { context: {} as Context, input: {} as Input, - events: {} as { type: "" } + events: {} as TrustmarkEvents }, actions: { onInit: notImplemented, + storeWalletInstanceAttestation: notImplemented, handleSessionExpired: notImplemented, updateExpirationSeconds: assign(({ context }) => ({ expirationSeconds: context.expirationDate @@ -31,7 +39,23 @@ export const itwTrustmarkMachine = setup({ trustmarkUrl: undefined, expirationDate: undefined, expirationSeconds: undefined - }) + }), + incrementAttempts: assign(({ context }) => { + const attempts = context.attempts ? context.attempts + 1 : 1; + const backoffTime = backoffTimeAmounts[attempts - 1] || 180; + return { + attempts, + nextAttemptAt: addSeconds(new Date(), backoffTime) + }; + }), + resetAttempts: assign({ + attempts: undefined, + nextAttemptAt: undefined + }), + setFailure: assign({ + failure: ({ event }) => mapEventToFailure(event) + }), + showRetryFailureToast: notImplemented }, actors: { getWalletAttestationActor: @@ -45,7 +69,9 @@ export const itwTrustmarkMachine = setup({ isTrustmarkExpired: ({ context }) => context.expirationDate ? isPast(context.expirationDate) : true, isSessionExpired: notImplemented, - hasValidWalletInstanceAttestation: notImplemented + hasValidWalletInstanceAttestation: notImplemented, + hasBackoffTimePassed: ({ context }) => + context.nextAttemptAt ? isPast(context.nextAttemptAt) : true } }).createMachine({ id: "itwTrustmarkMachine", @@ -57,21 +83,19 @@ export const itwTrustmarkMachine = setup({ states: { CheckingWalletInstanceAttestation: { tags: [ItwTags.Loading], - description: - "This is a state with the only purpose of checking the WIA and decide weather to get a new one or not", + description: "Checks the WIA and decide weather to get a new one or not", always: [ { - guard: "hasValidWalletInstanceAttestation", - target: "RefreshingTrustmark" + guard: not("hasValidWalletInstanceAttestation"), + target: "ObtainingWalletInstanceAttestation" }, { - target: "ObtainingWalletInstanceAttestation" + target: "RefreshingTrustmark" } ] }, ObtainingWalletInstanceAttestation: { - description: - "This state obtains the wallet instance attestation and stores it in the context for later use in the issuance flow.", + description: "Obtains the WIA and stores it in the context", tags: [ItwTags.Loading], invoke: { src: "getWalletAttestationActor", @@ -80,7 +104,8 @@ export const itwTrustmarkMachine = setup({ actions: [ assign(({ event }) => ({ walletInstanceAttestation: event.output - })) + })), + "storeWalletInstanceAttestation" ] }, onError: [ @@ -89,14 +114,14 @@ export const itwTrustmarkMachine = setup({ actions: "handleSessionExpired" }, { - target: "Failure" + target: "Failure", + actions: "setFailure" } ] } }, RefreshingTrustmark: { - description: - "This state obtains the trustmark url and stores it in the context for later use in the displaying flow.", + description: "Obtains the Trustmark and stores it to the context", tags: [ItwTags.Loading], invoke: { src: "getCredentialTrustmarkActor", @@ -115,14 +140,15 @@ export const itwTrustmarkMachine = setup({ ] }, onError: { - target: "Failure" + target: "Failure", + actions: "setFailure" } } }, DisplayingTrustmark: { - description: - "This state displays the trustmark QR Code and checks if it has expired or not.", + description: "Displays the QR Code and checks if it has expired", initial: "Idle", + entry: "resetAttempts", states: { Idle: { after: { @@ -147,7 +173,19 @@ export const itwTrustmarkMachine = setup({ } }, Failure: { - description: "This state is reached when an error occurs" + description: "This state is reached when an error occurs", + entry: "incrementAttempts", + on: { + retry: [ + { + guard: not("hasBackoffTimePassed"), + actions: "showRetryFailureToast" + }, + { + target: "RefreshingTrustmark" + } + ] + } } } }); diff --git a/ts/features/itwallet/trustmark/machine/provider.tsx b/ts/features/itwallet/trustmark/machine/provider.tsx index 506da7e0fb2..4bc644c9e48 100644 --- a/ts/features/itwallet/trustmark/machine/provider.tsx +++ b/ts/features/itwallet/trustmark/machine/provider.tsx @@ -1,3 +1,4 @@ +import { useIOToast } from "@pagopa/io-app-design-system"; import { createActorContext } from "@xstate/react"; import React from "react"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; @@ -20,9 +21,10 @@ export const ItwTrustmarkMachineProvider = ({ }: Props) => { const store = useIOStore(); const navigation = useIONavigation(); + const toast = useIOToast(); const trustmarkMachine = itwTrustmarkMachine.provide({ - actions: createItwTrustmarkActionsImplementation(store, navigation), + actions: createItwTrustmarkActionsImplementation(store, navigation, toast), actors: createItwTrustmarkActorsImplementation(store), guards: createItwTrustmarkGuardsImplementation() }); diff --git a/ts/features/itwallet/trustmark/machine/selectors.ts b/ts/features/itwallet/trustmark/machine/selectors.ts index 81256086d41..1045bd9eb23 100644 --- a/ts/features/itwallet/trustmark/machine/selectors.ts +++ b/ts/features/itwallet/trustmark/machine/selectors.ts @@ -8,3 +8,5 @@ export const selectTrustmarkUrl = ({ context }: MachineSnapshot) => export const selectExpirationSeconds = ({ context }: MachineSnapshot) => context.expirationSeconds; + +export const selectFailure = ({ context }: MachineSnapshot) => context.failure; diff --git a/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx b/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx index d29cf008aeb..acf0693a136 100644 --- a/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx +++ b/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx @@ -1,32 +1,14 @@ -import { - Body, - ContentWrapper, - HSpacer, - Icon, - IOStyles, - IOVisualCostants, - VStack -} from "@pagopa/io-app-design-system"; -import { format } from "date-fns"; +import { Body, ContentWrapper, VStack } from "@pagopa/io-app-design-system"; import React from "react"; -import { StyleSheet, View } from "react-native"; -import Placeholder from "rn-placeholder"; -import { QrCodeImage } from "../../../../components/QrCodeImage"; import { IOScrollViewWithLargeHeader } from "../../../../components/ui/IOScrollViewWithLargeHeader"; import I18n from "../../../../i18n"; import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; import { useMaxBrightness } from "../../../../utils/brightness"; import { getCredentialNameFromType } from "../../common/utils/itwCredentialUtils"; import { ItwParamsList } from "../../navigation/ItwParamsList"; -import { - ItwTrustmarkMachineContext, - ItwTrustmarkMachineProvider -} from "../machine/provider"; -import { - selectExpirationSeconds, - selectTrustmarkUrl -} from "../machine/selectors"; -import { useDebugInfo } from "../../../../hooks/useDebugInfo"; +import { ItwTrustmarkExpirationTimer } from "../components/ItwTrustmarkExpirationTimer"; +import { ItwTrustmarkQrCode } from "../components/ItwTrustmarkQrCode"; +import { ItwTrustmarkMachineProvider } from "../machine/provider"; export type ItwCredentialTrustmarkScreenNavigationParams = { credentialType: string; @@ -53,69 +35,11 @@ export const ItwCredentialTrustmarkScreen = (params: ScreenProps) => { {I18n.t("features.itWallet.trustmark.description")} - - + + ); }; - -/** - * Component that renders the QR code of the trustmark - */ -const TrustmarkQrCode = () => { - const trustmarkUrl = - ItwTrustmarkMachineContext.useSelector(selectTrustmarkUrl); - - useDebugInfo({ trustmarkUrl }); - - return ( - - - - ); -}; - -/** - * Timer that shows the remaining time before the trustmark expires - */ -const TrustmarkExpirationTimer = () => { - const expirationSeconds = ItwTrustmarkMachineContext.useSelector( - selectExpirationSeconds - ); - - // Format the expiration time to mm:ss and show a placeholder if the expiration time is undefined - const formattedExpirationTime = - expirationSeconds !== undefined ? ( - - {format(new Date(expirationSeconds * 1000), "mm:ss")} - - ) : ( - - ); - - return ( - - - - - {I18n.t("features.itWallet.trustmark.expiration") + " "} - {formattedExpirationTime} - - - ); -}; - -const styles = StyleSheet.create({ - qrCodeContainer: { - marginHorizontal: -IOVisualCostants.appMarginDefault, - alignItems: "center" - } -}); diff --git a/ts/features/itwallet/common/utils/__tests__/itwTrustmarkUtils.test.ts b/ts/features/itwallet/trustmark/utils/__tests__/index.ts similarity index 89% rename from ts/features/itwallet/common/utils/__tests__/itwTrustmarkUtils.test.ts rename to ts/features/itwallet/trustmark/utils/__tests__/index.ts index 362fac508b8..b34a351c68a 100644 --- a/ts/features/itwallet/common/utils/__tests__/itwTrustmarkUtils.test.ts +++ b/ts/features/itwallet/trustmark/utils/__tests__/index.ts @@ -1,9 +1,6 @@ import { Credential } from "@pagopa/io-react-native-wallet"; -import { ItwStoredCredentialsMocks } from "../itwMocksUtils"; -import { - getCredentialDocumentNumber, - getCredentialTrustmark -} from "../itwTrustmarkUtils"; +import { ItwStoredCredentialsMocks } from "../../../common/utils/itwMocksUtils"; +import { getCredentialDocumentNumber, getCredentialTrustmark } from ".."; jest.mock("@pagopa/io-react-native-wallet", () => ({ ...jest.requireActual("@pagopa/io-react-native-wallet"), @@ -14,7 +11,7 @@ jest.mock("@pagopa/io-react-native-wallet", () => ({ } })); -describe("itwTrustmarkUtils", () => { +describe("ITW trustmark utils", () => { describe("getCredentialDocumentNumber", () => { it.each([ ["MDL", ItwStoredCredentialsMocks.mdl, "RM8375131N"], diff --git a/ts/features/itwallet/common/utils/itwTrustmarkUtils.ts b/ts/features/itwallet/trustmark/utils/index.ts similarity index 85% rename from ts/features/itwallet/common/utils/itwTrustmarkUtils.ts rename to ts/features/itwallet/trustmark/utils/index.ts index 6cfb2af490f..a7305131825 100644 --- a/ts/features/itwallet/common/utils/itwTrustmarkUtils.ts +++ b/ts/features/itwallet/trustmark/utils/index.ts @@ -2,9 +2,12 @@ import { createCryptoContextFor, Credential } from "@pagopa/io-react-native-wallet"; -import { WellKnownClaim } from "./itwClaimsUtils"; -import { ParsedCredential, StoredCredential } from "./itwTypesUtils"; -import { WIA_KEYTAG } from "./itwCryptoContextUtils"; +import { + ParsedCredential, + StoredCredential +} from "../../common/utils/itwTypesUtils"; +import { WellKnownClaim } from "../../common/utils/itwClaimsUtils"; +import { WIA_KEYTAG } from "../../common/utils/itwCryptoContextUtils"; /** * Returns the document number for a credential, if applicable diff --git a/ts/store/reducers/debug.ts b/ts/store/reducers/debug.ts index 99c957ee9a4..4455bec0f58 100644 --- a/ts/store/reducers/debug.ts +++ b/ts/store/reducers/debug.ts @@ -1,6 +1,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import _ from "lodash"; import { PersistConfig, PersistPartial, persistReducer } from "redux-persist"; +import { createSelector } from "reselect"; import { getType } from "typesafe-actions"; import { resetDebugData, @@ -74,4 +75,15 @@ export const debugPersistor = persistReducer( // Selector export const isDebugModeEnabledSelector = (state: GlobalState) => state.debug.isDebugModeEnabled; -export const debugDataSelector = (state: GlobalState) => state.debug.debugData; + +/** + * Selector that returns the debug data without the undefined values + * avoiding to display empty values in the DebugInfoOverlay + */ +export const debugDataSelector = createSelector( + (state: GlobalState) => state.debug.debugData, + debugData => + Object.fromEntries( + Object.entries(debugData).filter(([_, value]) => value !== undefined) + ) +); From 4c8f237bb0d1faab02b91ea243d770f7286cec53 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jan 2025 22:21:34 +0000 Subject: [PATCH 2/3] chore(release): 2.80.0-rc.10 --- CHANGELOG.md | 8 ++++++++ android/app/build.gradle | 4 ++-- ios/ItaliaApp.xcodeproj/project.pbxproj | 6 +++--- ios/ItaliaApp/Info.plist | 2 +- ios/ItaliaAppTests/Info.plist | 2 +- package.json | 2 +- publiccode.yml | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8c2d6aa80..8263441bb4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.80.0-rc.10](https://github.com/pagopa/io-app/compare/2.80.0-rc.9...2.80.0-rc.10) (2025-01-09) + + +### Chores + +* **IT Wallet:** [[SIW-1933](https://pagopa.atlassian.net/browse/SIW-1933)] Handle Trustmark QR code generation failures ([#6558](https://github.com/pagopa/io-app/issues/6558)) ([b207270](https://github.com/pagopa/io-app/commit/b207270adeb6b14bee81ee1e5a02344ff1b98aca)) +* **IT Wallet:** [[SIW-1949](https://pagopa.atlassian.net/browse/SIW-1949)] Display email address in the same line in issuance failure screen ([#6589](https://github.com/pagopa/io-app/issues/6589)) ([22ca3e6](https://github.com/pagopa/io-app/commit/22ca3e692394a3d5395646f6f299293e4f62f480)) + ## [2.80.0-rc.9](https://github.com/pagopa/io-app/compare/2.80.0-rc.8...2.80.0-rc.9) (2025-01-08) diff --git a/android/app/build.gradle b/android/app/build.gradle index 93cf5a4cc30..aeb98f1ddc1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -119,8 +119,8 @@ android { applicationId "it.pagopa.io.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 100154904 - versionName "2.80.0.9" + versionCode 100154905 + versionName "2.80.0.10" multiDexEnabled true // The resConfigs attribute will remove all not required localized resources while building the application, // including the localized resources from libraries. diff --git a/ios/ItaliaApp.xcodeproj/project.pbxproj b/ios/ItaliaApp.xcodeproj/project.pbxproj index 364169d6160..31839947f75 100644 --- a/ios/ItaliaApp.xcodeproj/project.pbxproj +++ b/ios/ItaliaApp.xcodeproj/project.pbxproj @@ -802,7 +802,7 @@ CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; @@ -839,7 +839,7 @@ CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; @@ -1081,7 +1081,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = M2X5YQ4BJ7; diff --git a/ios/ItaliaApp/Info.plist b/ios/ItaliaApp/Info.plist index d4030bbad2f..30c89ade17c 100644 --- a/ios/ItaliaApp/Info.plist +++ b/ios/ItaliaApp/Info.plist @@ -36,7 +36,7 @@ CFBundleVersion - 9 + 10 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ItaliaAppTests/Info.plist b/ios/ItaliaAppTests/Info.plist index 99363cb379d..24041aea6ba 100644 --- a/ios/ItaliaAppTests/Info.plist +++ b/ios/ItaliaAppTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9 + 10 \ No newline at end of file diff --git a/package.json b/package.json index 32f3afd514c..4f2bd6e7a6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "italia-app", - "version": "2.80.0-rc.9", + "version": "2.80.0-rc.10", "private": true, "scripts": { "start": "react-native start", diff --git a/publiccode.yml b/publiccode.yml index 41452c4466a..36d32de520b 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -9,7 +9,7 @@ releaseDate: "2024-11-21" url: "https://github.com/pagopa/io-app" applicationSuite: IO landingURL: "https://io.italia.it/" -softwareVersion: 2.80.0-rc.9 +softwareVersion: 2.80.0-rc.10 developmentStatus: beta softwareType: standalone/mobile roadmap: "https://io.italia.it/" From 7e5ca4a918ec36b82e77837987da34db647bffdf Mon Sep 17 00:00:00 2001 From: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:12:11 +0100 Subject: [PATCH 3/3] fix: [IOBP-1100] German FAQ strings (#6579) ## Short description This pull request corrects the display strings for the German FAQ. ## List of changes proposed in this pull request - Update `io-service-metadata` version - Add german key to `localeToLocalizedMessageKey` constant - Added `fallbackForLocalizedMessageKeys` function to provide a fallback for German translations when they are not available - Updated locale handling to use the new fallback mechanism ## How to test > [!IMPORTANT] > Align `io-dev-api-server` with this [PR](https://github.com/pagopa/io-dev-api-server/pull/453) - Select German language, reload the app - Open the app - Tap on FAQ - Ensure that categories and subcategories are in german ## Preview https://github.com/user-attachments/assets/333858a3-d03a-496e-8869-e28f10d2843c --- scripts/generate-api-models.sh | 2 +- .../ItwIssuanceCredentialFailureScreen.tsx | 10 +++++--- .../WalletPaymentFeedbackBanner.tsx | 12 ++++++--- .../home/components/PaymentsAlertStatus.tsx | 25 +++++++++++++------ ts/hooks/useStatusAlertProps.ts | 12 ++++++--- ts/i18n.ts | 3 ++- ts/store/reducers/__mock__/backendStatus.ts | 2 ++ ts/utils/locale.ts | 11 ++++++-- ts/utils/payment.ts | 4 ++- 9 files changed, 58 insertions(+), 23 deletions(-) diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index 401d50fca5f..8c4e56d8735 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -1,7 +1,7 @@ #!/bin/bash IO_BACKEND_VERSION=v16.4.0-RELEASE -IO_SERVICES_METADATA_VERSION=1.0.51 +IO_SERVICES_METADATA_VERSION=1.0.54 declare -a apis=( # Backend APIs diff --git a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx index 7969c57556e..1fdeb2f9620 100644 --- a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx +++ b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialFailureScreen.tsx @@ -28,7 +28,10 @@ import { ItwCredentialIssuanceMachineContext } from "../../machine/provider"; import { useCredentialEventsTracking } from "../hooks/useCredentialEventsTracking"; import { useIOSelector } from "../../../../store/hooks"; import { itwDeferredIssuanceScreenContentSelector } from "../../../../store/reducers/backendStatus/remoteConfig"; -import { getFullLocale } from "../../../../utils/locale"; +import { + fallbackForLocalizedMessageKeys, + getFullLocale +} from "../../../../utils/locale"; import { serializeFailureReason } from "../../common/utils/itwStoreUtils"; export const ItwIssuanceCredentialFailureScreen = () => { @@ -65,6 +68,7 @@ const ContentView = ({ failure }: ContentViewProps) => { selectIssuerConfigurationOption ); const locale = getFullLocale(); + const localeFallback = fallbackForLocalizedMessageKeys(locale); const deferredIssuanceScreenContent = useIOSelector( itwDeferredIssuanceScreenContentSelector ); @@ -118,10 +122,10 @@ const ContentView = ({ failure }: ContentViewProps) => { case CredentialIssuanceFailureType.ASYNC_ISSUANCE: return { title: - deferredIssuanceScreenContent?.title?.[locale] ?? + deferredIssuanceScreenContent?.title?.[localeFallback] ?? I18n.t("features.itWallet.issuance.asyncCredentialError.title"), subtitle: - deferredIssuanceScreenContent?.description?.[locale] ?? + deferredIssuanceScreenContent?.description?.[localeFallback] ?? I18n.t("features.itWallet.issuance.asyncCredentialError.body"), pictogram: "pending", action: { diff --git a/ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx b/ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx index ff326c5a0a4..1e14462016f 100644 --- a/ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx +++ b/ts/features/payments/checkout/components/WalletPaymentFeedbackBanner.tsx @@ -8,7 +8,10 @@ import { isPaymentsFeedbackBannerEnabledSelector, paymentsFeedbackBannerConfigSelector } from "../../../../store/reducers/backendStatus/remoteConfig"; -import { getFullLocale } from "../../../../utils/locale"; +import { + fallbackForLocalizedMessageKeys, + getFullLocale +} from "../../../../utils/locale"; const WalletPaymentFeebackBanner = () => { const bannerViewRef = React.useRef(null); @@ -19,6 +22,7 @@ const WalletPaymentFeebackBanner = () => { paymentsFeedbackBannerConfigSelector ); const locale = getFullLocale(); + const localeFallback = fallbackForLocalizedMessageKeys(locale); const handleBannerPress = () => { if (!feedbackBannerConfig?.action) { @@ -42,9 +46,9 @@ const WalletPaymentFeebackBanner = () => { pictogramName="feedback" size="big" viewRef={bannerViewRef} - title={feedbackBannerConfig.title?.[locale]} - content={feedbackBannerConfig.description[locale]} - action={feedbackBannerConfig.action?.label[locale]} + title={feedbackBannerConfig.title?.[localeFallback]} + content={feedbackBannerConfig.description[localeFallback]} + action={feedbackBannerConfig.action?.label[localeFallback]} onPress={handleBannerPress} /> diff --git a/ts/features/payments/home/components/PaymentsAlertStatus.tsx b/ts/features/payments/home/components/PaymentsAlertStatus.tsx index c0da4e2739d..9d20e7557fd 100644 --- a/ts/features/payments/home/components/PaymentsAlertStatus.tsx +++ b/ts/features/payments/home/components/PaymentsAlertStatus.tsx @@ -1,12 +1,19 @@ -import * as React from "react"; -import { constVoid } from "fp-ts/lib/function"; import { Alert } from "@pagopa/io-app-design-system"; -import Animated, { FadeIn, FadeOut, Layout } from "react-native-reanimated"; +import { constVoid } from "fp-ts/lib/function"; +import * as React from "react"; import { GestureResponderEvent } from "react-native"; +import Animated, { + FadeIn, + FadeOut, + LinearTransition +} from "react-native-reanimated"; import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { sectionStatusByKeySelector } from "../../../../store/reducers/backendStatus/sectionStatus"; -import { getFullLocale } from "../../../../utils/locale"; +import { + fallbackForLocalizedMessageKeys, + getFullLocale +} from "../../../../utils/locale"; import { openWebUrl } from "../../../../utils/url"; import { getAlertVariant } from "../../common/utils"; @@ -22,7 +29,9 @@ export const PaymentsAlertStatus = () => { const handleOnPressAlertStatusInfo = (_: GestureResponderEvent) => { if (alertInfo && alertInfo.web_url && alertInfo.web_url[getFullLocale()]) { - openWebUrl(alertInfo.web_url[getFullLocale()]); + openWebUrl( + alertInfo.web_url[fallbackForLocalizedMessageKeys(getFullLocale())] + ); } }; @@ -30,10 +39,12 @@ export const PaymentsAlertStatus = () => { { if (!currentStatusMessage || currentStatusMessage.length === 0) { @@ -48,15 +52,15 @@ export const useStatusAlertProps = ( () => ({}), url => ({ action: I18n.t("global.sectionStatus.moreInfo"), - onPress: () => openWebUrl(url[locale]) + onPress: () => openWebUrl(url[localeFallback]) }) ) ); return { - content: firstAlert.message[locale], + content: firstAlert.message[localeFallback], variant: statusVariantMap[firstAlert.level], ...statusAction }; - }, [currentStatusMessage, locale]); + }, [currentStatusMessage, localeFallback]); }; diff --git a/ts/i18n.ts b/ts/i18n.ts index 2a98f8083c0..b8cb85a6f3e 100644 --- a/ts/i18n.ts +++ b/ts/i18n.ts @@ -19,7 +19,8 @@ export const localeToLocalizedMessageKey = new Map< LocalizedMessageKeys >([ ["it", "it-IT"], - ["en", "en-EN"] + ["en", "en-EN"], + ["de", "de-DE"] ]); export const localeToPreferredLanguageMapping = new Map< diff --git a/ts/store/reducers/__mock__/backendStatus.ts b/ts/store/reducers/__mock__/backendStatus.ts index 614d15132e7..efebdaace6c 100644 --- a/ts/store/reducers/__mock__/backendStatus.ts +++ b/ts/store/reducers/__mock__/backendStatus.ts @@ -301,6 +301,7 @@ export const baseRawBackendStatus: BackendStatus = { android: "0.0.0.0", ios: "0.0.0.0" }, + ipatente_cta_visible: true, feedback_banner_visible: true, disabled_identification_methods: [], disabled_credentials: [] @@ -393,6 +394,7 @@ export const baseBackendConfig: Config = { android: "0.0.0.0", ios: "0.0.0.0" }, + ipatente_cta_visible: true, feedback_banner_visible: true, disabled_credentials: [], disabled_identification_methods: [] diff --git a/ts/utils/locale.ts b/ts/utils/locale.ts index c65473dd294..a1f71cdb25f 100644 --- a/ts/utils/locale.ts +++ b/ts/utils/locale.ts @@ -1,6 +1,7 @@ -import * as O from "fp-ts/lib/Option"; import * as AR from "fp-ts/lib/Array"; import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import { PreferredLanguageEnum } from "../../definitions/backend/PreferredLanguage"; import { Locales } from "../../locales/locales"; import I18n, { availableTranslations, @@ -9,7 +10,6 @@ import I18n, { localeToPreferredLanguageMapping, LocalizedMessageKeys } from "../i18n"; -import { PreferredLanguageEnum } from "../../definitions/backend/PreferredLanguage"; /** * Helpers for handling locales */ @@ -84,3 +84,10 @@ export const fromPreferredLanguageToLocale = ( O.fromNullable, O.getOrElseW(() => localeFallback.locale) ); + +// This function is for the case where the localized message key is not available. When german translation is not available, we use italian as a fallback. +// This function will be removed when the german translation will be available for all the keys. +export const fallbackForLocalizedMessageKeys = ( + locale: LocalizedMessageKeys +): Exclude => + locale === "de-DE" ? "it-IT" : locale; diff --git a/ts/utils/payment.ts b/ts/utils/payment.ts index 6a407f8d2c4..88a67be4d55 100644 --- a/ts/utils/payment.ts +++ b/ts/utils/payment.ts @@ -421,7 +421,9 @@ export const getPaymentOutcomeCodeDescription = ( outcomeCodes[maybeOutcomeCodeKey.right] as OutcomeCode, O.fromNullable, O.chainNullableK(oc => oc.description), - O.map(description => description[getFullLocale()]) + O.map( + description => description[getFullLocale() as keyof typeof description] + ) ); } return O.none;