Skip to content
3 changes: 2 additions & 1 deletion ts/features/itwallet/analytics/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions ts/features/itwallet/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const ItwEidInfoBottomSheetContent = ({
"features.itWallet.presentation.bottomSheets.eidInfo.contentTop"
)}
/>
<ItwEidLifecycleAlert navigation={navigation} />
<ItwEidLifecycleAlert navigation={navigation} skipViewTracking={true} />
<View>
{claims.map((claim, index) => (
<Fragment key={index}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ItwJwtCredentialStatus> = [
"valid",
Expand All @@ -32,21 +33,36 @@ type Props = {
*/
lifecycleStatus?: Array<ItwJwtCredentialStatus>;
navigation: ReturnType<typeof useIONavigation>;
skipViewTracking?: boolean;
currentScreenName?: string;
};

/**
* This component renders an alert that displays information on the eID status.
*/
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: {
Expand Down
104 changes: 104 additions & 0 deletions ts/features/itwallet/common/hooks/useItwEidLifecycleAlertTracking.ts
Original file line number Diff line number Diff line change
@@ -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<typeof useIONavigation>;
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 };
};
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -208,7 +210,12 @@ const ItwPresentationCredentialStatusAlert = ({ credential }: Props) => {

switch (alertType) {
case CredentialAlertType.EID_LIFECYCLE:
return <ItwEidLifecycleAlert navigation={navigation} />;
return (
<ItwEidLifecycleAlert
navigation={navigation}
currentScreenName={currentScreenName}
/>
);
case CredentialAlertType.JWT_VERIFICATION:
return (
<JwtVerificationAlert
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { BodyProps } from "@pagopa/io-app-design-system";
import I18n from "i18next";
import { useCallback } from "react";
import { useFocusEffect } from "@react-navigation/native";
import I18n from "i18next";
import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent.tsx";
import { useIONavigation } from "../../../../../navigation/params/AppParamsList.ts";
import { ITW_ROUTES } from "../../../navigation/routes.ts";
import { useItwEidFeedbackBottomSheet } from "../../../common/hooks/useItwEidFeedbackBottomSheet.tsx";
import {
ItwEidReissuingTrigger,
trackItwEidReissuingMandatory
} from "../../../analytics";

export const ItwPresentationEidVerificationExpiredScreen = () => {
const navigation = useIONavigation();
Expand All @@ -29,6 +34,12 @@ export const ItwPresentationEidVerificationExpiredScreen = () => {
});
};

useFocusEffect(
useCallback(() => {
trackItwEidReissuingMandatory(ItwEidReissuingTrigger.ADD_CREDENTIAL);
}, [])
);

const bodyPropsArray: Array<BodyProps> = [
{
text: I18n.t(
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -127,6 +128,7 @@ export const ItwWalletCardsContainer = withWalletCategoryFilter("itw", () => {
<ItwEidLifecycleAlert
lifecycleStatus={LIFECYCLE_STATUS}
navigation={navigation}
currentScreenName={currentScreenName}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import("@react-navigation/native")>(
"@react-navigation/native"
),
useNavigation: () => ({
navigate: mockNavigate
navigate: mockNavigate,
addListener: mockAddListener
})
}));

Expand Down
Loading