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
})
}));