From 085181ba76299a59301013957c1d3856f829e3cf Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 3 Mar 2025 12:45:31 +0100 Subject: [PATCH] chore(IT Wallet): [SIW-2019] Improve integrity service warm up and availability check (#6746) ## Short description This PR improves the integrity service warm up phase and subsequent checks, allowing for a more detailed error handling. The `integrityServiceReady` flag is refactored into `integrityServiceStatus` which can explicitly tell the current status of the integrity service: - `undefined`: the warm up is still pending/waiting for the result - `"error"`: there was an error during the warm up phase - `"unavailable"`: the integrity service is not available for the current device - `"ready"`: integrity service is available and ready to be invoked ## List of changes proposed in this pull request - Added `IntegrityServiceStatus` type which is used to indicates the integrity service status - Added `warmUpIntegrityServiceSaga` saga, which handles the integrity service warm up and stores the result into the redux store - Added `checkIntegrityServiceReadySaga` saga, which can be used to retrieve the integrity service status within redux-saga contexts. If the warm up is yet to be completed, it waits for the result until a parametrized timeout - EID issuance machine refactor: - simplified guards logic - simplified `createWalletInstance` actor, which now throws a detailed error based on the integrity service status (if not `"ready"`) ## How to test Tests should pass and check for the absence of any regression during eID issuance flows. Mock different integrity service statuses from within the `warmUpIntegrityServiceSaga` function and verify expected behaviors. --- ts/features/itwallet/common/saga/index.ts | 2 + .../itwallet/issuance/store/actions/index.ts | 9 +- .../store/reducers/__tests__/index.test.ts | 16 +-- .../itwallet/issuance/store/reducers/index.ts | 22 ++-- .../issuance/store/selectors/index.ts | 6 +- .../checkIntegrityServiceReadySaga.test.ts | 118 ++++++++++++++++++ .../checkWalletInstanceStateSaga.test.ts | 42 +++---- .../saga/checkIntegrityServiceReadySaga.ts | 65 ++++++++++ .../saga/checkWalletInstanceStateSaga.ts | 18 +-- .../machine/eid/__tests__/machine.test.ts | 5 +- ts/features/itwallet/machine/eid/actors.ts | 29 ++--- ts/features/itwallet/machine/eid/machine.ts | 29 +++-- 12 files changed, 272 insertions(+), 89 deletions(-) create mode 100644 ts/features/itwallet/lifecycle/saga/__tests__/checkIntegrityServiceReadySaga.test.ts create mode 100644 ts/features/itwallet/lifecycle/saga/checkIntegrityServiceReadySaga.ts diff --git a/ts/features/itwallet/common/saga/index.ts b/ts/features/itwallet/common/saga/index.ts index a323fa5fd7f..69fa94fed9b 100644 --- a/ts/features/itwallet/common/saga/index.ts +++ b/ts/features/itwallet/common/saga/index.ts @@ -4,6 +4,7 @@ import { watchItwCredentialsSaga } from "../../credentials/saga"; import { checkCredentialsStatusAttestation } from "../../credentials/saga/checkCredentialsStatusAttestation"; import { handleWalletCredentialsRehydration } from "../../credentials/saga/handleWalletCredentialsRehydration"; import { watchItwLifecycleSaga } from "../../lifecycle/saga"; +import { warmUpIntegrityServiceSaga } from "../../lifecycle/saga/checkIntegrityServiceReadySaga"; import { checkWalletInstanceStateSaga } from "../../lifecycle/saga/checkWalletInstanceStateSaga"; function* checkWalletInstanceAndCredentialsValiditySaga() { @@ -14,6 +15,7 @@ function* checkWalletInstanceAndCredentialsValiditySaga() { } export function* watchItwSaga(): SagaIterator { + yield* fork(warmUpIntegrityServiceSaga); yield* fork(checkWalletInstanceAndCredentialsValiditySaga); yield* fork(handleWalletCredentialsRehydration); yield* fork(watchItwCredentialsSaga); diff --git a/ts/features/itwallet/issuance/store/actions/index.ts b/ts/features/itwallet/issuance/store/actions/index.ts index 6efdd883d35..226d3354b59 100644 --- a/ts/features/itwallet/issuance/store/actions/index.ts +++ b/ts/features/itwallet/issuance/store/actions/index.ts @@ -1,4 +1,5 @@ import { ActionType, createStandardAction } from "typesafe-actions"; +import { IntegrityServiceStatus } from "../reducers"; export const itwStoreIntegrityKeyTag = createStandardAction( "ITW_STORE_INTEGRITY_KEY_TAG" @@ -8,11 +9,11 @@ export const itwRemoveIntegrityKeyTag = createStandardAction( "ITW_REMOVE_INTEGRITY_KEY_TAG" )(); -export const itwIntegritySetServiceIsReady = createStandardAction( - "ITW_INTEGRITY_SET_SERVICE_IS_READY" -)(); +export const itwSetIntegrityServiceStatus = createStandardAction( + "ITW_SET_INTEGRITY_SERVICE_STATUS" +)(); export type ItwIssuanceActions = | ActionType | ActionType - | ActionType; + | ActionType; diff --git a/ts/features/itwallet/issuance/store/reducers/__tests__/index.test.ts b/ts/features/itwallet/issuance/store/reducers/__tests__/index.test.ts index ffcea8e3b25..c8b6ba76d32 100644 --- a/ts/features/itwallet/issuance/store/reducers/__tests__/index.test.ts +++ b/ts/features/itwallet/issuance/store/reducers/__tests__/index.test.ts @@ -6,7 +6,7 @@ import { Action } from "../../../../../../store/actions/types"; import { GlobalState } from "../../../../../../store/reducers/types"; import { itwLifecycleStoresReset } from "../../../../lifecycle/store/actions"; import { - itwIntegritySetServiceIsReady, + itwSetIntegrityServiceStatus, itwRemoveIntegrityKeyTag, itwStoreIntegrityKeyTag } from "../../actions"; @@ -58,20 +58,20 @@ describe("ITW issuance reducer", () => { expect(targetSate.features.itWallet.issuance.integrityKeyTag).toEqual( O.none ); - expect(targetSate.features.itWallet.issuance.integrityServiceReady).toEqual( - undefined - ); + expect( + targetSate.features.itWallet.issuance.integrityServiceStatus + ).toEqual(undefined); }); it("should set the integrity preparation flag", () => { const targetSate = pipe( undefined, curriedAppReducer(applicationChangeState("active")), - curriedAppReducer(itwIntegritySetServiceIsReady(true)) + curriedAppReducer(itwSetIntegrityServiceStatus("ready")) ); - expect(targetSate.features.itWallet.issuance.integrityServiceReady).toEqual( - true - ); + expect( + targetSate.features.itWallet.issuance.integrityServiceStatus + ).toEqual("ready"); }); }); diff --git a/ts/features/itwallet/issuance/store/reducers/index.ts b/ts/features/itwallet/issuance/store/reducers/index.ts index 9cba6e92154..a511b4419d5 100644 --- a/ts/features/itwallet/issuance/store/reducers/index.ts +++ b/ts/features/itwallet/issuance/store/reducers/index.ts @@ -1,20 +1,22 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; import * as O from "fp-ts/lib/Option"; -import { getType } from "typesafe-actions"; import { PersistConfig, persistReducer } from "redux-persist"; -import AsyncStorage from "@react-native-async-storage/async-storage"; +import { getType } from "typesafe-actions"; import { Action } from "../../../../../store/actions/types"; +import { itwLifecycleStoresReset } from "../../../lifecycle/store/actions"; import { - itwIntegritySetServiceIsReady, itwRemoveIntegrityKeyTag, + itwSetIntegrityServiceStatus, itwStoreIntegrityKeyTag } from "../actions"; -import { itwLifecycleStoresReset } from "../../../lifecycle/store/actions"; const CURRENT_REDUX_ITW_ISSUANCE_STORE_VERSION = -1; +export type IntegrityServiceStatus = "ready" | "unavailable" | "error"; + export type ItwIssuanceState = { integrityKeyTag: O.Option; - integrityServiceReady?: boolean; + integrityServiceStatus?: IntegrityServiceStatus; }; export const itwIssuanceInitialState: ItwIssuanceState = { @@ -26,6 +28,11 @@ const reducer = ( action: Action ): ItwIssuanceState => { switch (action.type) { + case getType(itwSetIntegrityServiceStatus): + return { + ...state, + integrityServiceStatus: action.payload + }; case getType(itwStoreIntegrityKeyTag): return { ...state, @@ -37,11 +44,6 @@ const reducer = ( ...state, integrityKeyTag: O.none }; - case getType(itwIntegritySetServiceIsReady): - return { - ...state, - integrityServiceReady: action.payload - }; } return state; }; diff --git a/ts/features/itwallet/issuance/store/selectors/index.ts b/ts/features/itwallet/issuance/store/selectors/index.ts index af943f9caa6..b5545dbeba8 100644 --- a/ts/features/itwallet/issuance/store/selectors/index.ts +++ b/ts/features/itwallet/issuance/store/selectors/index.ts @@ -4,7 +4,7 @@ export const itwIntegrityKeyTagSelector = (state: GlobalState) => state.features.itWallet.issuance.integrityKeyTag; /** - * Selector that returns the integrityServiceReady flag. + * Selector that returns the integrityService status */ -export const itwIntegrityServiceReadySelector = (state: GlobalState) => - state.features.itWallet.issuance.integrityServiceReady; +export const itwIntegrityServiceStatusSelector = (state: GlobalState) => + state.features.itWallet.issuance.integrityServiceStatus; diff --git a/ts/features/itwallet/lifecycle/saga/__tests__/checkIntegrityServiceReadySaga.test.ts b/ts/features/itwallet/lifecycle/saga/__tests__/checkIntegrityServiceReadySaga.test.ts new file mode 100644 index 00000000000..ea52461083b --- /dev/null +++ b/ts/features/itwallet/lifecycle/saga/__tests__/checkIntegrityServiceReadySaga.test.ts @@ -0,0 +1,118 @@ +import { type DeepPartial } from "redux"; +import { expectSaga } from "redux-saga-test-plan"; +import * as matchers from "redux-saga-test-plan/matchers"; +import { throwError } from "redux-saga-test-plan/providers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { ensureIntegrityServiceIsReady } from "../../../common/utils/itwIntegrityUtils"; +import { itwSetIntegrityServiceStatus } from "../../../issuance/store/actions"; +import { itwIntegrityServiceStatusSelector } from "../../../issuance/store/selectors"; +import { + checkIntegrityServiceReadySaga, + warmUpIntegrityServiceSaga +} from "../checkIntegrityServiceReadySaga"; + +describe("checkIntegrityServiceReadySaga", () => { + it("Should wait for the integrity service status to be set", () => { + const store: DeepPartial = { + features: { + itWallet: { + issuance: { + integrityServiceStatus: undefined + } + } + } + }; + return expectSaga(checkIntegrityServiceReadySaga) + .withState(store) + .select(itwIntegrityServiceStatusSelector) + .take(itwSetIntegrityServiceStatus) + .not.call.fn(warmUpIntegrityServiceSaga) + .not.returns(expect.anything()) + .run(); + }); + + it("Should return true when the integrity service status is ready", () => { + const store: DeepPartial = { + features: { + itWallet: { + issuance: { + integrityServiceStatus: "ready" + } + } + } + }; + return expectSaga(checkIntegrityServiceReadySaga) + .withState(store) + .select(itwIntegrityServiceStatusSelector) + .not.take(itwSetIntegrityServiceStatus) + .not.call.fn(warmUpIntegrityServiceSaga) + .returns(true) + .run(); + }); + + it("Should return false when the integrity service status is unavailable", () => { + const store: DeepPartial = { + features: { + itWallet: { + issuance: { + integrityServiceStatus: "unavailable" + } + } + } + }; + return expectSaga(checkIntegrityServiceReadySaga) + .withState(store) + .select(itwIntegrityServiceStatusSelector) + .not.take(itwSetIntegrityServiceStatus) + .not.call.fn(warmUpIntegrityServiceSaga) + .returns(false) + .run(); + }); + + it("Should retry the integrity service warm up when the integrity service status is error", () => { + const store: DeepPartial = { + features: { + itWallet: { + issuance: { + integrityServiceStatus: "error" + } + } + } + }; + return expectSaga(checkIntegrityServiceReadySaga) + .withState(store) + .select(itwIntegrityServiceStatusSelector) + .call.fn(warmUpIntegrityServiceSaga) + .take(itwSetIntegrityServiceStatus) + .not.returns(expect.anything()) + .run(); + }); +}); + +describe("warmUpIntegrityServiceSaga", () => { + it("Sets the integrity service status to ready when the integrity service is ready", () => + expectSaga(warmUpIntegrityServiceSaga) + .provide([[matchers.call.fn(ensureIntegrityServiceIsReady), true]]) + .call.fn(ensureIntegrityServiceIsReady) + .put(itwSetIntegrityServiceStatus("ready")) + .run()); + + it("Sets the integrity service status to unavailable when the integrity service is unavailable", () => + expectSaga(warmUpIntegrityServiceSaga) + .provide([[matchers.call.fn(ensureIntegrityServiceIsReady), false]]) + .call.fn(ensureIntegrityServiceIsReady) + .put(itwSetIntegrityServiceStatus("unavailable")) + .run()); + + it("Sets the integrity service status to error when the integrity service is unavailable", () => + expectSaga(warmUpIntegrityServiceSaga) + .provide([ + [ + matchers.call.fn(ensureIntegrityServiceIsReady), + throwError(new Error("Integrity service error")) + ] + ]) + .call.fn(ensureIntegrityServiceIsReady) + .put(itwSetIntegrityServiceStatus("error")) + .run()); +}); diff --git a/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts b/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts index 574090dea19..ada185ec27a 100644 --- a/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts +++ b/ts/features/itwallet/lifecycle/saga/__tests__/checkWalletInstanceStateSaga.test.ts @@ -1,20 +1,20 @@ +import * as O from "fp-ts/lib/Option"; import { type DeepPartial } from "redux"; import { expectSaga } from "redux-saga-test-plan"; import * as matchers from "redux-saga-test-plan/matchers"; -import * as O from "fp-ts/lib/Option"; +import { sessionTokenSelector } from "../../../../../store/reducers/authentication"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { getWalletInstanceStatus } from "../../../common/utils/itwAttestationUtils"; +import { StoredCredential } from "../../../common/utils/itwTypesUtils"; +import { itwIntegrityServiceStatusSelector } from "../../../issuance/store/selectors"; +import { itwIsWalletInstanceAttestationValidSelector } from "../../../walletInstance/store/selectors"; +import { ItwLifecycleState } from "../../store/reducers"; +import { checkIntegrityServiceReadySaga } from "../checkIntegrityServiceReadySaga"; import { checkWalletInstanceStateSaga, getStatusOrResetWalletInstance } from "../checkWalletInstanceStateSaga"; -import { ItwLifecycleState } from "../../store/reducers"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { getWalletInstanceStatus } from "../../../common/utils/itwAttestationUtils"; -import { StoredCredential } from "../../../common/utils/itwTypesUtils"; -import { sessionTokenSelector } from "../../../../../store/reducers/authentication"; import { handleWalletInstanceResetSaga } from "../handleWalletInstanceResetSaga"; -import { ensureIntegrityServiceIsReady } from "../../../common/utils/itwIntegrityUtils"; -import { itwIntegrityServiceReadySelector } from "../../../issuance/store/selectors"; -import { itwIsWalletInstanceAttestationValidSelector } from "../../../walletInstance/store/selectors"; jest.mock("@pagopa/io-react-native-crypto", () => ({ deleteKey: jest.fn @@ -34,8 +34,8 @@ describe("checkWalletInstanceStateSaga", () => { }; return expectSaga(checkWalletInstanceStateSaga) .withState(store) - .provide([[matchers.call.fn(ensureIntegrityServiceIsReady), true]]) - .call.fn(ensureIntegrityServiceIsReady) + .provide([[matchers.call.fn(checkIntegrityServiceReadySaga), true]]) + .call.fn(checkIntegrityServiceReadySaga) .not.call.fn(getStatusOrResetWalletInstance) .run(); }); @@ -46,7 +46,7 @@ describe("checkWalletInstanceStateSaga", () => { itWallet: { lifecycle: ItwLifecycleState.ITW_LIFECYCLE_OPERATIONAL, issuance: { - integrityServiceReady: true, + integrityServiceStatus: "ready", integrityKeyTag: O.some("aac6e82a-e27e-4293-9b55-94a9fab22763") }, credentials: { eid: O.none, credentials: [] } @@ -59,11 +59,11 @@ describe("checkWalletInstanceStateSaga", () => { .provide([ [matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"], [matchers.select(itwIsWalletInstanceAttestationValidSelector), false], - [matchers.select(itwIntegrityServiceReadySelector), true], + [matchers.select(itwIntegrityServiceStatusSelector), "ready"], [matchers.call.fn(getWalletInstanceStatus), { is_revoked: false }], - [matchers.call.fn(ensureIntegrityServiceIsReady), true] + [matchers.call.fn(checkIntegrityServiceReadySaga), true] ]) - .call.fn(ensureIntegrityServiceIsReady) + .call.fn(checkIntegrityServiceReadySaga) .call.fn(getStatusOrResetWalletInstance) .not.call.fn(handleWalletInstanceResetSaga) .run(); @@ -88,9 +88,9 @@ describe("checkWalletInstanceStateSaga", () => { [matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"], [matchers.select(itwIsWalletInstanceAttestationValidSelector), false], [matchers.call.fn(getWalletInstanceStatus), { is_revoked: true }], - [matchers.call.fn(ensureIntegrityServiceIsReady), true] + [matchers.call.fn(checkIntegrityServiceReadySaga), true] ]) - .call.fn(ensureIntegrityServiceIsReady) + .call.fn(checkIntegrityServiceReadySaga) .call.fn(getStatusOrResetWalletInstance) .call.fn(handleWalletInstanceResetSaga) .run(); @@ -115,9 +115,9 @@ describe("checkWalletInstanceStateSaga", () => { [matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"], [matchers.select(itwIsWalletInstanceAttestationValidSelector), false], [matchers.call.fn(getWalletInstanceStatus), { is_revoked: false }], - [matchers.call.fn(ensureIntegrityServiceIsReady), true] + [matchers.call.fn(checkIntegrityServiceReadySaga), true] ]) - .call.fn(ensureIntegrityServiceIsReady) + .call.fn(checkIntegrityServiceReadySaga) .call.fn(getStatusOrResetWalletInstance) .not.call.fn(handleWalletInstanceResetSaga) .run(); @@ -142,9 +142,9 @@ describe("checkWalletInstanceStateSaga", () => { [matchers.select(sessionTokenSelector), "h94LhbfJCLGH1S3qHj"], [matchers.select(itwIsWalletInstanceAttestationValidSelector), false], [matchers.call.fn(getWalletInstanceStatus), { is_revoked: true }], - [matchers.call.fn(ensureIntegrityServiceIsReady), true] + [matchers.call.fn(checkIntegrityServiceReadySaga), true] ]) - .call.fn(ensureIntegrityServiceIsReady) + .call.fn(checkIntegrityServiceReadySaga) .call.fn(getStatusOrResetWalletInstance) .call.fn(handleWalletInstanceResetSaga) .run(); diff --git a/ts/features/itwallet/lifecycle/saga/checkIntegrityServiceReadySaga.ts b/ts/features/itwallet/lifecycle/saga/checkIntegrityServiceReadySaga.ts new file mode 100644 index 00000000000..e7472c5d257 --- /dev/null +++ b/ts/features/itwallet/lifecycle/saga/checkIntegrityServiceReadySaga.ts @@ -0,0 +1,65 @@ +import { call, delay, put, race, select, take } from "typed-redux-saga/macro"; +import { ReduxSagaEffect } from "../../../../types/utils"; +import { ensureIntegrityServiceIsReady } from "../../common/utils/itwIntegrityUtils"; +import { itwSetIntegrityServiceStatus } from "../../issuance/store/actions"; +import { itwIntegrityServiceStatusSelector } from "../../issuance/store/selectors"; + +/** + * Checks if the integrity service is ready by checking its current status and waiting for updates if needed. + * + * The integrity service can be in one of three states: + * - "ready": The service is initialized and ready to use + * - "unavailable": The device does not support the integrity service + * - "error": An error occurred while initializing the service + * + * If the service is in an error state, this will trigger a warmup retry via warmUpIntegrityServiceSaga. + * If the status is not conclusive, it will wait up to 10 seconds for the status to update. + * + * @returns true if the integrity service becomes ready within the timeout period, false otherwise + */ +export function* checkIntegrityServiceReadySaga( + timeout: number = 10000 +): Generator { + const integrityServiceStatus = yield* select( + itwIntegrityServiceStatusSelector + ); + + // If integrity service is ready means we can continue + if (integrityServiceStatus === "ready") { + return true; + } + + // If integrity service is unavailable means the device does not support it + if (integrityServiceStatus === "unavailable") { + return false; + } + + // If integrity service is in error state, retry warming it up + if (integrityServiceStatus === "error") { + yield* call(warmUpIntegrityServiceSaga); + } + + // Wait for the integrity service status to be updated up to 10 seconds + const { result } = yield* race({ + result: take(itwSetIntegrityServiceStatus), + timeout: delay(timeout) + }); + + return result?.payload === "ready" ?? false; +} + +/** + * Saga responsible to check whether the integrity service is ready. + * Works as a warmup process for the integrity service on Android. + */ +export function* warmUpIntegrityServiceSaga(): Generator< + ReduxSagaEffect, + void +> { + try { + const isReady: boolean = yield* call(ensureIntegrityServiceIsReady); + yield* put(itwSetIntegrityServiceStatus(isReady ? "ready" : "unavailable")); + } catch (e) { + yield* put(itwSetIntegrityServiceStatus("error")); + } +} diff --git a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts index 85489590a1a..628711aba8a 100644 --- a/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts +++ b/ts/features/itwallet/lifecycle/saga/checkWalletInstanceStateSaga.ts @@ -3,15 +3,14 @@ import { call, put, select } from "typed-redux-saga/macro"; import { sessionTokenSelector } from "../../../../store/reducers/authentication"; import { ReduxSagaEffect } from "../../../../types/utils"; import { assert } from "../../../../utils/assert"; +import { getNetworkError } from "../../../../utils/errors"; +import { trackItwStatusWalletAttestationFailure } from "../../analytics"; import { getWalletInstanceStatus } from "../../common/utils/itwAttestationUtils"; -import { ensureIntegrityServiceIsReady } from "../../common/utils/itwIntegrityUtils"; import { itwIntegrityKeyTagSelector } from "../../issuance/store/selectors"; import { itwUpdateWalletInstanceStatus } from "../../walletInstance/store/actions"; import { itwLifecycleIsOperationalOrValid } from "../store/selectors"; -import { itwIntegritySetServiceIsReady } from "../../issuance/store/actions"; -import { getNetworkError } from "../../../../utils/errors"; -import { trackItwStatusWalletAttestationFailure } from "../../analytics"; import { handleWalletInstanceResetSaga } from "./handleWalletInstanceResetSaga"; +import { checkIntegrityServiceReadySaga } from "./checkIntegrityServiceReadySaga"; export function* getStatusOrResetWalletInstance(integrityKeyTag: string) { const sessionToken = yield* select(sessionTokenSelector); @@ -44,13 +43,8 @@ export function* checkWalletInstanceStateSaga(): Generator< ReduxSagaEffect, void > { - // We start the warming up process of the integrity service on Android - try { - const integrityServiceReadyResult: boolean = yield* call( - ensureIntegrityServiceIsReady - ); - yield* put(itwIntegritySetServiceIsReady(integrityServiceReadyResult)); - + // Before any check we need to ensure the integrity service is ready + if (yield* call(checkIntegrityServiceReadySaga)) { const isItwOperationalOrValid = yield* select( itwLifecycleIsOperationalOrValid ); @@ -60,7 +54,5 @@ export function* checkWalletInstanceStateSaga(): Generator< if (isItwOperationalOrValid && O.isSome(integrityKeyTag)) { yield* call(getStatusOrResetWalletInstance, integrityKeyTag.value); } - } catch (e) { - // Ignore the error, the integrity service is not available and an error will occur if the wallet requests an attestation } } diff --git a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts index 84cb5050210..95badd8de4e 100644 --- a/ts/features/itwallet/machine/eid/__tests__/machine.test.ts +++ b/ts/features/itwallet/machine/eid/__tests__/machine.test.ts @@ -123,8 +123,9 @@ describe("itwEidIssuanceMachine", () => { } }); - afterEach(() => { + beforeEach(() => { jest.clearAllMocks(); + jest.resetAllMocks(); }); it("Should obtain an eID (SPID)", async () => { @@ -706,6 +707,8 @@ describe("itwEidIssuanceMachine", () => { }); it("Should return to TOS acceptance if session expires when creating a Wallet Instance", async () => { + hasValidWalletInstanceAttestation.mockImplementation(() => false); + const actor = createActor(mockedMachine); actor.start(); diff --git a/ts/features/itwallet/machine/eid/actors.ts b/ts/features/itwallet/machine/eid/actors.ts index e565dc726d3..c93dcfb0325 100644 --- a/ts/features/itwallet/machine/eid/actors.ts +++ b/ts/features/itwallet/machine/eid/actors.ts @@ -17,7 +17,7 @@ import { pollForStoreValue } from "../../common/utils/itwStoreUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; import { itwIntegrityKeyTagSelector, - itwIntegrityServiceReadySelector + itwIntegrityServiceStatusSelector } from "../../issuance/store/selectors"; import { itwLifecycleStoresReset } from "../../lifecycle/store/actions"; import type { @@ -47,33 +47,26 @@ export const createEidIssuanceActorsImplementation = ( createWalletInstance: fromPromise(async () => { const sessionToken = sessionTokenSelector(store.getState()); assert(sessionToken, "sessionToken is undefined"); - const storedIntegrityKeyTag = itwIntegrityKeyTagSelector(store.getState()); - - // If there is a stored key tag we assume the wallet instance was already created - // so we just need to prepare the integrity service and return the existing key tag. - if (O.isSome(storedIntegrityKeyTag)) { - return storedIntegrityKeyTag.value; - } // Reset the wallet store to prevent having dirty state before registering a new wallet instance store.dispatch(itwLifecycleStoresReset()); + // Await the integrity preparation before requesting the integrity key tag - const isIntegrityServiceReady = await pollForStoreValue({ + const integrityServiceStatus = await pollForStoreValue({ getState: store.getState, - selector: itwIntegrityServiceReadySelector, + selector: itwIntegrityServiceStatusSelector, condition: value => value !== undefined + }).catch(() => { + throw new Error("Integrity service status check timed out"); }); - // If the integrity service preparation is not ready (still undefined) after 10 seconds the user will be prompted with an error, + + // If the integrity service preparation is not ready (still undefined) or in an error state after 10 seconds the user will be prompted with an error, // he will need to retry. - // TODO: Create a personalized error message for this case informing the user that the integrity service is not ready yet. assert( - isIntegrityServiceReady !== undefined, - "Integrity service not ready after 10 seconds" + integrityServiceStatus === "ready", + `Integrity service status is ${integrityServiceStatus}` ); - // If the integrity service preparation is ready, but it is failed, the user will be prompted with an error - // and the wallet instance creation will be aborted. - // TODO: Create a personalized error message for this case informing the user that the integrity service is not available on his device. - assert(isIntegrityServiceReady, "Integrity service not available"); + const hardwareKeyTag = await getIntegrityHardwareKeyTag(); await registerWalletInstance(hardwareKeyTag, sessionToken); diff --git a/ts/features/itwallet/machine/eid/machine.ts b/ts/features/itwallet/machine/eid/machine.ts index d6c4e5c856e..1d739d60070 100644 --- a/ts/features/itwallet/machine/eid/machine.ts +++ b/ts/features/itwallet/machine/eid/machine.ts @@ -95,6 +95,7 @@ export const itwEidIssuanceMachine = setup({ issuedEidMatchesAuthenticatedUser: notImplemented, isSessionExpired: notImplemented, isOperationAborted: notImplemented, + hasIntegrityKeyTag: ({ context }) => context.integrityKeyTag !== undefined, hasValidWalletInstanceAttestation: notImplemented, isNFCEnabled: ({ context }) => context.cieContext?.isNFCEnabled || false, isReissuing: ({ context }) => context.isReissuing === true @@ -147,15 +148,21 @@ export const itwEidIssuanceMachine = setup({ on: { "accept-tos": [ { - guard: ({ context }) => !context.integrityKeyTag, - target: "WalletInstanceCreation" + // If the wallet instance attestation is valid, we can proceed to the IPZS privacy acceptance + guard: "hasValidWalletInstanceAttestation", + target: "IpzsPrivacyAcceptance" }, { - guard: not("hasValidWalletInstanceAttestation"), + // If do not have a valid wallet instance attestation but integrity key tag is present, we can + // assume the wallet instance was already created and we can proceed to the wallet instance + // attestation obtainment + guard: "hasIntegrityKeyTag", target: "WalletInstanceAttestationObtainment" }, { - target: "IpzsPrivacyAcceptance" + // If do not have a valid wallet instance attestation and integrity key tag, we need to create a + // new wallet instance first + target: "WalletInstanceCreation" } ] } @@ -171,8 +178,8 @@ export const itwEidIssuanceMachine = setup({ assign(({ event }) => ({ integrityKeyTag: event.output })), - { type: "storeIntegrityKeyTag" }, - { type: "setWalletInstanceToOperational" } + "storeIntegrityKeyTag", + "setWalletInstanceToOperational" ], target: "WalletInstanceAttestationObtainment" }, @@ -232,7 +239,7 @@ export const itwEidIssuanceMachine = setup({ assign(({ event }) => ({ walletInstanceAttestation: event.output })), - { type: "storeWalletInstanceAttestation" } + "storeWalletInstanceAttestation" ], target: "UserIdentification" }, @@ -241,7 +248,7 @@ export const itwEidIssuanceMachine = setup({ assign(({ event }) => ({ walletInstanceAttestation: event.output })), - { type: "storeWalletInstanceAttestation" } + "storeWalletInstanceAttestation" ], target: "IpzsPrivacyAcceptance" } @@ -326,7 +333,7 @@ export const itwEidIssuanceMachine = setup({ StartingCieIDAuthFlow: { entry: [ assign(() => ({ authenticationContext: undefined })), - { type: "navigateToCieIdLoginScreen" } + "navigateToCieIdLoginScreen" ], invoke: { src: "startAuthFlow", @@ -380,7 +387,7 @@ export const itwEidIssuanceMachine = setup({ IdpSelection: { entry: [ assign(() => ({ authenticationContext: undefined })), - { type: "navigateToIdpSelectionScreen" } + "navigateToIdpSelectionScreen" ], on: { "select-spid-idp": { @@ -452,7 +459,7 @@ export const itwEidIssuanceMachine = setup({ InsertingCardPin: { entry: [ assign(() => ({ authenticationContext: undefined })), // Reset the authentication context, otherwise retries will use stale data - { type: "navigateToCiePinScreen" } + "navigateToCiePinScreen" ], on: { "cie-pin-entered": [