Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(IT Wallet): [SIW-2003] ITW auth level tracking #6710

Merged
merged 15 commits into from
Feb 17, 2025
Merged
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MockDate.reset();

I'd also suggest to remove the _.set, which could lead to problems and unwanted behaviors within tests

Copy link
Contributor Author

@ale-mazz ale-mazz Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mastro993, I improved the test using the spread operator and added another small test. Anyway IMO if we want to improve these test suites by replacing the usage of lodash, we should refactor every test (at least these) by removing it, since almost every test is using the .set() function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, we should refactor them.

});

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
Loading