Skip to content

Commit

Permalink
fix(IT Wallet): [SIW-2003] ITW auth level tracking (#6710)
Browse files Browse the repository at this point in the history
## Short description
This PR adds a new field in the `preferences` itw store that contains
the auth level used when a WI is requested.
It also handle the updates related to that field and the correct
tracking with Mixpanel

## List of changes proposed in this pull request
- Added a new field in the `preferences` itw store called authLevel that
can be `undefined`, `L2` or `L3`
- Stored the correct auth level when requesting the WI.
- Handled the store field reset when revocating the WI.
- Updated mixpanel tracking.

## How to test
Verify that the store is filled with the `L2` value when updating from a
previous version.
Try now to revoke the WI and request it again using an `L3` level
(CIE+PIN). Check redux if it is correct.
Verify that mixpanel receives the correct information.

---------

Co-authored-by: Gianluca Spada <[email protected]>
  • Loading branch information
ale-mazz and gispada authored Feb 17, 2025
1 parent 6b6b981 commit ca154f3
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 49 deletions.
27 changes: 18 additions & 9 deletions ts/features/itwallet/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { buildEventProperties } from "../../../utils/analytics";
import { IdentificationContext } from "../machine/eid/context";
import { IssuanceFailure } from "../machine/eid/failure";
import { ItwCredentialStatus } from "../common/utils/itwTypesUtils";
import { itwAuthLevelSelector } from "../common/store/selectors/preferences.ts";
import {
ITW_ACTIONS_EVENTS,
ITW_CONFIRM_EVENTS,
Expand Down Expand Up @@ -776,22 +777,25 @@ export const trackBackToWallet = ({ exit_page, credential }: BackToWallet) => {

// #region TECH

export const trackItwRequest = (ITW_ID_method?: ItwIdMethod) => {
if (ITW_ID_method) {
export const trackItwRequest = (method?: ItwIdMethod) => {
if (method) {
void mixpanelTrack(
ITW_TECH_EVENTS.ITW_ID_REQUEST,
buildEventProperties("TECH", undefined, { ITW_ID_method })
buildEventProperties("TECH", undefined, { ITW_ID_method: method })
);
}
};

export const trackItwRequestSuccess = (ITW_ID_method?: ItwIdMethod) => {
if (ITW_ID_method) {
export const trackItwRequestSuccess = (
method?: ItwIdMethod,
status?: ItwStatus
) => {
if (method) {
void mixpanelTrack(
ITW_TECH_EVENTS.ITW_ID_REQUEST_SUCCESS,
buildEventProperties("TECH", undefined, {
ITW_ID_method,
ITW_ID_V2: "L2"
ITW_ID_method: method,
ITW_ID_V2: status
})
);
}
Expand All @@ -801,13 +805,18 @@ export const trackItwRequestSuccess = (ITW_ID_method?: ItwIdMethod) => {
// #region PROFILE AND SUPER PROPERTIES UPDATE

export const updateITWStatusAndIDProperties = (state: GlobalState) => {
const authLevel = itwAuthLevelSelector(state);
if (!authLevel) {
return;
}

void updateMixpanelProfileProperties(state, {
property: "ITW_STATUS_V2",
value: "L2"
value: authLevel
});
void updateMixpanelSuperProperties(state, {
property: "ITW_STATUS_V2",
value: "L2"
value: authLevel
});
void updateMixpanelProfileProperties(state, {
property: "ITW_ID_V2",
Expand Down
8 changes: 7 additions & 1 deletion ts/features/itwallet/common/store/actions/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ActionType, createStandardAction } from "typesafe-actions";
import { ItwAuthLevel } from "../../utils/itwTypesUtils.ts";

export const itwCloseFeedbackBanner = createStandardAction(
"ITW_CLOSE_FEEDBACK_BANNER"
Expand All @@ -20,9 +21,14 @@ export const itwSetReviewPending = createStandardAction(
"ITW_SET_REVIEW_PENDING"
)<boolean>();

export const itwSetAuthLevel = createStandardAction("ITW_SET_AUTH_LEVEL")<
ItwAuthLevel | undefined
>();

export type ItwPreferencesActions =
| ActionType<typeof itwCloseFeedbackBanner>
| ActionType<typeof itwCloseDiscoveryBanner>
| ActionType<typeof itwFlagCredentialAsRequested>
| ActionType<typeof itwUnflagCredentialAsRequested>
| ActionType<typeof itwSetReviewPending>;
| ActionType<typeof itwSetReviewPending>
| ActionType<typeof itwSetAuthLevel>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {
itwCloseDiscoveryBanner,
itwCloseFeedbackBanner,
itwFlagCredentialAsRequested,
itwSetAuthLevel,
itwUnflagCredentialAsRequested
} from "../../actions/preferences";
import reducer, {
ItwPreferencesState,
itwPreferencesInitialState
itwPreferencesInitialState,
ItwPreferencesState
} from "../preferences";
import { itwLifecycleStoresReset } from "../../../../lifecycle/store/actions";

Expand Down Expand Up @@ -98,4 +99,14 @@ describe("IT Wallet preferences reducer", () => {

expect(newState).toEqual(itwPreferencesInitialState);
});

it("should handle itwSetAuthLevel action", () => {
const action = itwSetAuthLevel("L2");
const newState = reducer(INITIAL_STATE, action);

expect(newState).toEqual({
...newState,
authLevel: "L2"
});
});
});
24 changes: 22 additions & 2 deletions ts/features/itwallet/common/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as O from "fp-ts/lib/Option";
import AsyncStorage from "@react-native-async-storage/async-storage";
import _ from "lodash";
import { combineReducers } from "redux";
Expand Down Expand Up @@ -43,15 +44,34 @@ const itwReducer = combineReducers({
preferences: preferencesReducer
});

const CURRENT_REDUX_ITW_STORE_VERSION = 1;
const CURRENT_REDUX_ITW_STORE_VERSION = 2;

const migrations: MigrationManifest = {
// Added preferences store
"0": (state: PersistedState): PersistedState =>
_.set(state, "preferences", {}),

// Added requestedCredentials to preferences store
"1": (state: PersistedState): PersistedState =>
_.set(state, "preferences.requestedCredentials", {})
_.set(state, "preferences.requestedCredentials", {}),

// Added authLevel to preferences store and set it to "L2" if eid is present
"2": (state: PersistedState): PersistedState => {
const { credentials, preferences } = state as PersistedItWalletState;

// If eid is a Some(value), set authLevel to "L2"
if (O.isSome(credentials.eid)) {
return {
...state,
preferences: {
...preferences,
authLevel: "L2"
}
} as PersistedItWalletState;
}

return state;
}
};

const itwPersistConfig: PersistConfig = {
Expand Down
15 changes: 13 additions & 2 deletions ts/features/itwallet/common/store/reducers/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
itwCloseDiscoveryBanner,
itwCloseFeedbackBanner,
itwFlagCredentialAsRequested,
itwUnflagCredentialAsRequested,
itwSetReviewPending
itwSetAuthLevel,
itwSetReviewPending,
itwUnflagCredentialAsRequested
} from "../actions/preferences";
import { itwLifecycleStoresReset } from "../../../lifecycle/store/actions";
import { ItwAuthLevel } from "../../utils/itwTypesUtils.ts";

export type ItwPreferencesState = {
// Date until which the feedback banner should be hidden
Expand All @@ -21,6 +23,8 @@ export type ItwPreferencesState = {
requestedCredentials: { [credentialType: string]: string };
// Indicates whether the user should see the modal to review the app.
isPendingReview?: boolean;
// Indicates the SPID/CIE authentication level used to obtain the eid
authLevel?: ItwAuthLevel;
};

export const itwPreferencesInitialState: ItwPreferencesState = {
Expand Down Expand Up @@ -76,6 +80,13 @@ const reducer = (
};
}

case getType(itwSetAuthLevel): {
return {
...state,
authLevel: action.payload
};
}

case getType(itwLifecycleStoresReset):
return { ...itwPreferencesInitialState };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import MockDate from "mockdate";
import { applicationChangeState } from "../../../../../../store/actions/application";
import { appReducer } from "../../../../../../store/reducers";
import {
itwAuthLevelSelector,
itwIsDiscoveryBannerHiddenSelector,
itwIsFeedbackBannerHiddenSelector,
itwRequestedCredentialsSelector
} from "../preferences";
import { ItwAuthLevel } from "../../../utils/itwTypesUtils.ts";

describe("itwIsFeedbackBannerHiddenSelector", () => {
it.each([
Expand Down Expand Up @@ -70,3 +72,48 @@ describe("itwRequestedCredentialsSelector", () => {
MockDate.reset();
});
});

describe("itwAuthLevelSelector", () => {
afterEach(() => {
// Always reset the date after each test to avoid side effects
MockDate.reset();
});

it("returns the auth level when it is set", () => {
const state = appReducer(undefined, applicationChangeState("active"));
const updatedState = {
...state,
features: {
...state.features,
itWallet: {
...state.features?.itWallet,
preferences: {
...state.features?.itWallet?.preferences,
authLevel: "L2" as ItwAuthLevel
}
}
}
};

expect(itwAuthLevelSelector(updatedState)).toEqual("L2");
});

it("returns undefined when the auth level is not set", () => {
const state = appReducer(undefined, applicationChangeState("active"));
const updatedState = {
...state,
features: {
...state.features,
itWallet: {
...state.features?.itWallet,
preferences: {
...state.features?.itWallet?.preferences,
authLevel: undefined
}
}
}
};

expect(itwAuthLevelSelector(updatedState)).toBeUndefined();
});
});
6 changes: 6 additions & 0 deletions ts/features/itwallet/common/store/selectors/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ export const itwRequestedCredentialsSelector = createSelector(
*/
export const itwIsPendingReviewSelector = (state: GlobalState) =>
state.features.itWallet.preferences.isPendingReview;

/**
* Returns the authentication level used to obtain the eID.
*/
export const itwAuthLevelSelector = (state: GlobalState) =>
state.features.itWallet.preferences.authLevel;
2 changes: 2 additions & 0 deletions ts/features/itwallet/common/utils/itwTypesUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ export type ItwCredentialStatus =
| "expiring"
| "expired"
| ItwJwtCredentialStatus;

export type ItwAuthLevel = "L2" | "L3";
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "@pagopa/io-app-design-system";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { useLayoutEffect, useMemo } from "react";
import { useCallback, useLayoutEffect, useMemo } from "react";
import { StyleSheet, View } from "react-native";
import { useFocusEffect, useRoute } from "@react-navigation/native";
import { useDebugInfo } from "../../../../hooks/useDebugInfo";
Expand Down Expand Up @@ -80,10 +80,14 @@ const ContentView = ({ eid }: ContentViewProps) => {
[eid.credentialType]
);

useFocusEffect(() => {
trackCredentialPreview(mixPanelCredential);
trackItwRequestSuccess(identification?.mode);
});
useFocusEffect(
useCallback(() => {
trackCredentialPreview(mixPanelCredential);
if (identification) {
trackItwRequestSuccess(identification?.mode, identification?.level);
}
}, [identification, mixPanelCredential])
);

useDebugInfo({
parsedCredential: eid.parsedCredential
Expand Down
13 changes: 10 additions & 3 deletions ts/features/itwallet/machine/eid/__tests__/machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ describe("itwEidIssuanceMachine", () => {
const setWalletInstanceToOperational = jest.fn();
const setWalletInstanceToValid = jest.fn();
const handleSessionExpired = jest.fn();
const abortIdentification = jest.fn();
const onInit = jest.fn();

const createWalletInstance = jest.fn();
Expand All @@ -64,6 +63,7 @@ describe("itwEidIssuanceMachine", () => {
const trackWalletInstanceCreation = jest.fn();
const trackWalletInstanceRevocation = jest.fn();
const revokeWalletInstance = jest.fn();
const storeAuthLevel = jest.fn();

const mockedMachine = itwEidIssuanceMachine.provide({
actions: {
Expand All @@ -90,10 +90,10 @@ describe("itwEidIssuanceMachine", () => {
setWalletInstanceToOperational,
setWalletInstanceToValid,
handleSessionExpired,
abortIdentification,
resetWalletInstance,
trackWalletInstanceCreation,
trackWalletInstanceRevocation,
storeAuthLevel,
onInit: assign(onInit),
setIsReissuing: assign({
isReissuing: true
Expand Down Expand Up @@ -236,6 +236,7 @@ describe("itwEidIssuanceMachine", () => {
walletInstanceAttestation: T_WIA,
identification: {
mode: "spid",
level: "L2",
idpId: idps[0].id
}
});
Expand Down Expand Up @@ -287,6 +288,7 @@ describe("itwEidIssuanceMachine", () => {
walletInstanceAttestation: T_WIA,
identification: {
mode: "spid",
level: "L2",
idpId: idps[0].id
},
authenticationContext: expect.objectContaining({
Expand Down Expand Up @@ -345,7 +347,7 @@ describe("itwEidIssuanceMachine", () => {
walletInstanceAttestation: T_WIA,
identification: {
mode: "cieId",
abortController: new AbortController()
level: "L2"
}
});
expect(navigateToCieIdLoginScreen).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -446,6 +448,7 @@ describe("itwEidIssuanceMachine", () => {
walletInstanceAttestation: T_WIA,
identification: {
mode: "ciePin",
level: "L3",
pin: "12345678"
},
cieContext: {
Expand Down Expand Up @@ -537,6 +540,7 @@ describe("itwEidIssuanceMachine", () => {
walletInstanceAttestation: T_WIA,
identification: {
mode: "ciePin",
level: "L3",
pin: "12345678"
},
cieContext: {
Expand Down Expand Up @@ -565,6 +569,7 @@ describe("itwEidIssuanceMachine", () => {
walletInstanceAttestation: T_WIA,
identification: {
mode: "ciePin",
level: "L3",
pin: "12345678"
},
cieContext: {
Expand Down Expand Up @@ -1061,6 +1066,7 @@ describe("itwEidIssuanceMachine", () => {
isReissuing: true,
identification: {
mode: "spid",
level: "L2",
idpId: idps[0].id
}
});
Expand Down Expand Up @@ -1114,6 +1120,7 @@ describe("itwEidIssuanceMachine", () => {
isReissuing: true,
identification: {
mode: "spid",
level: "L2",
idpId: idps[0].id
},
authenticationContext: expect.objectContaining({
Expand Down
Loading

0 comments on commit ca154f3

Please sign in to comment.