diff --git a/ts/features/itwallet/analytics/enum.ts b/ts/features/itwallet/analytics/enum.ts index 62c2f0f739d..5abbfc07d8b 100644 --- a/ts/features/itwallet/analytics/enum.ts +++ b/ts/features/itwallet/analytics/enum.ts @@ -109,7 +109,8 @@ export enum ITW_ERRORS_EVENTS { ITW_USER_WITHOUT_L3_REQUIREMENTS = "ITW_USER_WITHOUT_L3_REQUIREMENTS", ITW_CIEID_CIE_NOT_REGISTERED = "ITW_CIEID_CIE_NOT_REGISTERED", ITW_CREDENTIAL_REISSUING_FAILED = "ITW_CREDENTIAL_REISSUING_FAILED", - ITW_ADD_CREDENTIAL_NOT_TRUSTED_ISSUER = "ITW_ADD_CREDENTIAL_NOT_TRUSTED_ISSUER" + ITW_ADD_CREDENTIAL_NOT_TRUSTED_ISSUER = "ITW_ADD_CREDENTIAL_NOT_TRUSTED_ISSUER", + ITW_REISSUING_EID_MANDATORY = "ITW_REISSUING_EID_MANDATORY" } export enum ITW_EXIT_EVENTS { diff --git a/ts/features/itwallet/analytics/index.ts b/ts/features/itwallet/analytics/index.ts index be7f68364d2..f60e3a8c5d1 100644 --- a/ts/features/itwallet/analytics/index.ts +++ b/ts/features/itwallet/analytics/index.ts @@ -272,6 +272,17 @@ type ItwCredentialInfoDetails = { credential_screen_type: "detail" | "preview"; }; +/** + * Actions that can trigger the eID reissuing flow. + * This type represents the user action that was performed immediately before + * the eID reissuing process is initiated. + * Add new values here when implementing additional flows that should start + * the reissuing procedure. + */ +export enum ItwEidReissuingTrigger { + ADD_CREDENTIAL = "add_credential" +} + /** * Actions that trigger the requirement for L3 upgrade. * This type represents the user action that was performed immediately before @@ -1149,6 +1160,15 @@ export const trackItwAddCredentialNotTrustedIssuer = ( ); }; +export const trackItwEidReissuingMandatory = ( + action: ItwEidReissuingTrigger +) => { + void mixpanelTrack( + ITW_ERRORS_EVENTS.ITW_REISSUING_EID_MANDATORY, + buildEventProperties("KO", "screen_view", { action }) + ); +}; + // #endregion ERRORS // #region PROFILE PROPERTIES diff --git a/ts/features/itwallet/common/components/ItwEidInfoBottomSheetContent.tsx b/ts/features/itwallet/common/components/ItwEidInfoBottomSheetContent.tsx index d23cfc1a9fc..fbbf9cddaa9 100644 --- a/ts/features/itwallet/common/components/ItwEidInfoBottomSheetContent.tsx +++ b/ts/features/itwallet/common/components/ItwEidInfoBottomSheetContent.tsx @@ -93,7 +93,7 @@ const ItwEidInfoBottomSheetContent = ({ "features.itWallet.presentation.bottomSheets.eidInfo.contentTop" )} /> - + {claims.map((claim, index) => ( diff --git a/ts/features/itwallet/common/components/ItwEidLifecycleAlert.tsx b/ts/features/itwallet/common/components/ItwEidLifecycleAlert.tsx index e1384d25d9b..e0f0afc356f 100644 --- a/ts/features/itwallet/common/components/ItwEidLifecycleAlert.tsx +++ b/ts/features/itwallet/common/components/ItwEidLifecycleAlert.tsx @@ -19,6 +19,7 @@ import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { ITW_ROUTES } from "../../navigation/routes"; import { itwLifecycleIsITWalletValidSelector } from "../../lifecycle/store/selectors"; import { offlineAccessReasonSelector } from "../../../ingress/store/selectors"; +import { useItwEidLifecycleAlertTracking } from "../hooks/useItwEidLifecycleAlertTracking"; const defaultLifecycleStatus: Array = [ "valid", @@ -32,6 +33,8 @@ type Props = { */ lifecycleStatus?: Array; navigation: ReturnType; + skipViewTracking?: boolean; + currentScreenName?: string; }; /** @@ -39,14 +42,27 @@ type Props = { */ export const ItwEidLifecycleAlert = ({ lifecycleStatus = defaultLifecycleStatus, - navigation + navigation, + skipViewTracking, + currentScreenName }: Props) => { const eidOption = useIOSelector(itwCredentialsEidSelector); const isItw = useIOSelector(itwLifecycleIsITWalletValidSelector); const maybeEidStatus = useIOSelector(itwCredentialsEidStatusSelector); const offlineAccessReason = useIOSelector(offlineAccessReasonSelector); + const isOffline = offlineAccessReason !== undefined; + + const { trackAlertTap } = useItwEidLifecycleAlertTracking({ + isItw, + maybeEidStatus, + navigation, + skipViewTracking, + currentScreenName, + isOffline + }); const startEidReissuing = () => { + trackAlertTap(); navigation.navigate(ITW_ROUTES.MAIN, { screen: ITW_ROUTES.IDENTIFICATION.MODE_SELECTION, params: { diff --git a/ts/features/itwallet/common/hooks/useItwEidLifecycleAlertTracking.ts b/ts/features/itwallet/common/hooks/useItwEidLifecycleAlertTracking.ts new file mode 100644 index 00000000000..29b5e48d141 --- /dev/null +++ b/ts/features/itwallet/common/hooks/useItwEidLifecycleAlertTracking.ts @@ -0,0 +1,104 @@ +import { useCallback, useMemo, useRef, useEffect } from "react"; +import { + trackITWalletBannerVisualized, + trackItWalletBannerTap +} from "../../analytics"; +import { ITW_SCREENVIEW_EVENTS } from "../../analytics/enum"; +import { ItwJwtCredentialStatus } from "../utils/itwTypesUtils"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; + +type Props = { + isItw: boolean; + maybeEidStatus: ItwJwtCredentialStatus | undefined; + navigation: ReturnType; + currentScreenName?: string; + isOffline?: boolean; + skipViewTracking?: boolean; +}; + +/** + * Hook for tracking eID lifecycle alerts. + * + * This hook handles two types of analytics events: + * 1. Banner visualized event: triggered the first time the alert becomes visible + * when the screen is focused. If the screen loses focus and regains it, + * the event can be retracked depending on the focus behavior. + * 2. Banner tap event: triggered when the user tap the alert. + * + * Tracking rules: + * - If `skipViewTracking` is true, only the visualized event is skipped. + * - If the eID status is valid, no visualized event is sent. + * - If `isItw` is true, no tracking is sent at all. + * + * @param isItw Whether IT Wallet is active disables tracking entirely + * @param maybeEidStatus The current eID status + * @param navigation Navigation object to listen for focus/blur events + * @param skipViewTracking Flag to disable only the view tracking (visualized) + * @param currentScreenName Optional screen name to include in tracking + * @param isOffline Whether the app is in offline mode + * @returns trackAlertTap callback to track tap interactions on the alert + */ +export const useItwEidLifecycleAlertTracking = ({ + isItw, + maybeEidStatus, + navigation, + skipViewTracking = false, + currentScreenName, + isOffline = false +}: Props) => { + const hasTrackedRef = useRef(false); + const isEidInvalid = + maybeEidStatus === "jwtExpiring" || maybeEidStatus === "jwtExpired"; + + const shouldTrackVisualization = !skipViewTracking && isEidInvalid && !isItw; + + const trackingProperties = useMemo( + () => ({ + banner_id: + maybeEidStatus === "jwtExpiring" + ? "itwExpiringIdBanner" + : "itwExpiredIdBanner", + banner_page: currentScreenName ?? "not_available", + banner_landing: isOffline + ? "not_available" + : ITW_SCREENVIEW_EVENTS.ITW_ID_METHOD + }), + [maybeEidStatus, currentScreenName, isOffline] + ); + + useEffect(() => { + if (!shouldTrackVisualization) { + return; + } + const onFocus = () => { + if (!hasTrackedRef.current) { + trackITWalletBannerVisualized(trackingProperties); + // eslint-disable-next-line functional/immutable-data + hasTrackedRef.current = true; + } + }; + + const onBlur = () => { + // eslint-disable-next-line functional/immutable-data + hasTrackedRef.current = false; + }; + + // We use navigation listeners for "focus" and "blur" here instead of "useFocusEffect" + // because this hook may be used inside a BottomSheet. + const unsubscribeFocus = navigation.addListener("focus", onFocus); + const unsubscribeBlur = navigation.addListener("blur", onBlur); + + return () => { + unsubscribeFocus(); + unsubscribeBlur(); + }; + }, [navigation, shouldTrackVisualization, trackingProperties]); + + const trackAlertTap = useCallback(() => { + if (!isItw) { + trackItWalletBannerTap(trackingProperties); + } + }, [isItw, trackingProperties]); + + return { trackAlertTap }; +}; diff --git a/ts/features/itwallet/presentation/details/components/ItwPresentationCredentialStatusAlert.tsx b/ts/features/itwallet/presentation/details/components/ItwPresentationCredentialStatusAlert.tsx index ba7f3dce763..eac9cc0db5c 100644 --- a/ts/features/itwallet/presentation/details/components/ItwPresentationCredentialStatusAlert.tsx +++ b/ts/features/itwallet/presentation/details/components/ItwPresentationCredentialStatusAlert.tsx @@ -1,5 +1,6 @@ import { memo, useCallback } from "react"; import { View } from "react-native"; +import { useRoute } from "@react-navigation/native"; import { Alert, IOButton, IOToast, VStack } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; @@ -166,6 +167,7 @@ const ItwPresentationCredentialStatusAlert = ({ credential }: Props) => { ); const isItwL3 = useIOSelector(itwLifecycleIsITWalletValidSelector); const offlineAccessReason = useIOSelector(offlineAccessReasonSelector); + const { name: currentScreenName } = useRoute(); const trackCredentialAlertEvent = (action: CredentialAlertEvents): void => { if (!status) { @@ -208,7 +210,12 @@ const ItwPresentationCredentialStatusAlert = ({ credential }: Props) => { switch (alertType) { case CredentialAlertType.EID_LIFECYCLE: - return ; + return ( + + ); case CredentialAlertType.JWT_VERIFICATION: return ( { const navigation = useIONavigation(); @@ -29,6 +34,12 @@ export const ItwPresentationEidVerificationExpiredScreen = () => { }); }; + useFocusEffect( + useCallback(() => { + trackItwEidReissuingMandatory(ItwEidReissuingTrigger.ADD_CREDENTIAL); + }, []) + ); + const bodyPropsArray: Array = [ { text: I18n.t( diff --git a/ts/features/itwallet/wallet/components/ItwWalletCardsContainer.tsx b/ts/features/itwallet/wallet/components/ItwWalletCardsContainer.tsx index 3723e4ee413..671f31b97db 100644 --- a/ts/features/itwallet/wallet/components/ItwWalletCardsContainer.tsx +++ b/ts/features/itwallet/wallet/components/ItwWalletCardsContainer.tsx @@ -1,5 +1,5 @@ import { ListItemHeader, VStack } from "@pagopa/io-app-design-system"; -import { useFocusEffect } from "@react-navigation/native"; +import { useFocusEffect, useRoute } from "@react-navigation/native"; import { useCallback, useMemo } from "react"; import I18n from "i18next"; import { useDebugInfo } from "../../../../hooks/useDebugInfo"; @@ -42,6 +42,7 @@ export const ItwWalletCardsContainer = withWalletCategoryFilter("itw", () => { const eidStatus = useIOSelector(itwCredentialsEidStatusSelector); const isEidExpired = eidStatus === "jwtExpired"; const iconColor = useItwStatusIconColor(isEidExpired); + const { name: currentScreenName } = useRoute(); useItwPendingReviewRequest(); @@ -127,6 +128,7 @@ export const ItwWalletCardsContainer = withWalletCategoryFilter("itw", () => { )} diff --git a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx index 052d4b0d926..562e33e2111 100644 --- a/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx +++ b/ts/features/wallet/components/__tests__/WalletCardsContainer.test.tsx @@ -43,13 +43,15 @@ jest.mock("react-native-reanimated", () => ({ })); const mockNavigate = jest.fn(); +const mockAddListener = jest.fn().mockImplementation(_event => jest.fn()); jest.mock("@react-navigation/native", () => ({ ...jest.requireActual( "@react-navigation/native" ), useNavigation: () => ({ - navigate: mockNavigate + navigate: mockNavigate, + addListener: mockAddListener }) }));