From ca8bc3245507a865ad469346ecb0208c18a73687 Mon Sep 17 00:00:00 2001 From: LazyAfternoons Date: Thu, 3 Oct 2024 17:18:33 +0200 Subject: [PATCH] chore(IT Wallet): [SIW-1702] Add automatic subscription to the ITW trial system (#6228) > [!WARNING] > Can be tested with `io-dev-api-server` by pointing to [this PR.](https://github.com/pagopa/io-dev-api-server/pull/414) ## Short description This PR adds an automatic subscription to the ITW trial system when opening the app. ## List of changes proposed in this pull request - Add the `handleTrialSystemSubscription` function to check the status of the trial system. If a citizen is unsubscribed, the function should automatically subscribe them to the trial system; - Mock the `CONFIG` object from `react-native-config`; - Add tests for `handleTrialSystemSubscription`. ## How to test This PR can be tested with the `io-dev-api-server` by checking the network traffic of the app: - Start `io-dev-api-server` with the default config which is `SUBSCRIBED`; - Start the app with the `.env.local` config; - Login and check for the network traffic. This can be checked either with ProxyMan or via the logs of `io-dev-api-server`; - Do the same test by rotating the trial states in the configuration of `io-dev-api-server`, either by changing it directly in `config.ts` or with a custom `config.json` file. The expected behavior is: ``` Any other state -> Only a `GET` to `trialId/subscriptions` should be performed UNSUBSCRIBED -> Both a `GET` and a `POST` to `trialId/subscriptions` should be performed ``` --------- Co-authored-by: Mario Perrotta Co-authored-by: Andrea --- locales/en/index.yml | 3 - locales/it/index.yml | 3 - ts/__mocks__/react-native-config.ts | 3 +- .../common/saga/__tests__/index.test.ts | 149 ++++++++++++++++++ ts/features/itwallet/common/saga/index.ts | 32 +++- .../store/sagas/watchTrialSystemSaga.ts | 5 - 6 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 ts/features/itwallet/common/saga/__tests__/index.test.ts diff --git a/locales/en/index.yml b/locales/en/index.yml index 4b34cbc2618..9a8dab5a09b 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3413,9 +3413,6 @@ features: title: Certificato di autenticità usageDescription: Quando ti viene richiesto, mostra il QR Code per attestare l'autenticità del documento. certifiedLabel: Questo documento è certificato dall'ente emittente. - trialSystem: - toast: - subscribed: Done! You will receive a message when the trial period will begin. support: ticketList: noTicket: diff --git a/locales/it/index.yml b/locales/it/index.yml index 8fd67f40bcb..1be2d494071 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3413,9 +3413,6 @@ features: title: Certificato di autenticità usageDescription: Quando ti viene richiesto, mostra il QR Code per attestare l'autenticità del documento. certifiedLabel: Questo documento è certificato dall'ente emittente. - trialSystem: - toast: - subscribed: Fatto! Riceverai un messaggio all’avvio della sperimentazione support: ticketList: noTicket: diff --git a/ts/__mocks__/react-native-config.ts b/ts/__mocks__/react-native-config.ts index e624e1c7e6d..059c0223523 100644 --- a/ts/__mocks__/react-native-config.ts +++ b/ts/__mocks__/react-native-config.ts @@ -1,5 +1,6 @@ export default { UA_DONATIONS_ENABLED: "YES", PREMIUM_MESSAGES_OPT_IN_ENABLED: "YES", - SCAN_ADDITIONAL_BARCODES_ENABLED: "YES" + SCAN_ADDITIONAL_BARCODES_ENABLED: "YES", + ITW_TRIAL_ID: "baz" }; diff --git a/ts/features/itwallet/common/saga/__tests__/index.test.ts b/ts/features/itwallet/common/saga/__tests__/index.test.ts new file mode 100644 index 00000000000..d7e240fb4ff --- /dev/null +++ b/ts/features/itwallet/common/saga/__tests__/index.test.ts @@ -0,0 +1,149 @@ +import { expectSaga } from "redux-saga-test-plan"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { DeepPartial } from "redux"; +import * as matchers from "redux-saga-test-plan/matchers"; +import { handleTrialSystemSubscription } from "../index"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { + trialSystemActivationStatus, + trialSystemActivationStatusUpsert +} from "../../../../trialSystem/store/actions"; +import { SubscriptionStateEnum } from "../../../../../../definitions/trial_system/SubscriptionState"; +import { trialStatusSelector } from "../../../../trialSystem/store/reducers"; +import { TrialId } from "../../../../../../definitions/trial_system/TrialId"; + +describe("handleTrialSystemSubscription", () => { + it("should handle trial system subscription correctly", async () => { + const trialId = "baz" as TrialId; + const state = SubscriptionStateEnum.UNSUBSCRIBED; + const store: DeepPartial = { + trialSystem: { + [trialId]: pot.some(state) + } + }; + return expectSaga(handleTrialSystemSubscription) + .withState(store) + .put(trialSystemActivationStatus.request(trialId)) + .dispatch( + trialSystemActivationStatus.success({ + trialId, + state, + createdAt: new Date() + }) + ) + .take([ + trialSystemActivationStatus.success, + trialSystemActivationStatus.failure + ]) + .provide([[matchers.select(trialStatusSelector), state]]) + .put(trialSystemActivationStatusUpsert.request(trialId)) + .run(); + }); + + it("shouldn't do anything if the user is already subscribed", async () => { + const trialId = "baz" as TrialId; + const state = SubscriptionStateEnum.SUBSCRIBED; + const store: DeepPartial = { + trialSystem: { + [trialId]: pot.some(state) + } + }; + return expectSaga(handleTrialSystemSubscription) + .withState(store) + .put(trialSystemActivationStatus.request(trialId)) + .dispatch( + trialSystemActivationStatus.success({ + trialId, + state, + createdAt: new Date() + }) + ) + .take([ + trialSystemActivationStatus.success, + trialSystemActivationStatus.failure + ]) + .provide([[matchers.select(trialStatusSelector), state]]) + .not.put(trialSystemActivationStatusUpsert.request(trialId)) + .run(); + }); + + it("shouldn't do anything if the user is active", async () => { + const trialId = "baz" as TrialId; + const state = SubscriptionStateEnum.ACTIVE; + const store: DeepPartial = { + trialSystem: { + [trialId]: pot.some(state) + } + }; + return expectSaga(handleTrialSystemSubscription) + .withState(store) + .put(trialSystemActivationStatus.request(trialId)) + .dispatch( + trialSystemActivationStatus.success({ + trialId, + state, + createdAt: new Date() + }) + ) + .take([ + trialSystemActivationStatus.success, + trialSystemActivationStatus.failure + ]) + .provide([[matchers.select(trialStatusSelector), state]]) + .not.put(trialSystemActivationStatusUpsert.request(trialId)) + .run(); + }); + + it("shouldn't do anything if the user is disabled", async () => { + const trialId = "baz" as TrialId; + const state = SubscriptionStateEnum.DISABLED; + const store: DeepPartial = { + trialSystem: { + [trialId]: pot.some(state) + } + }; + return expectSaga(handleTrialSystemSubscription) + .withState(store) + .put(trialSystemActivationStatus.request(trialId)) + .dispatch( + trialSystemActivationStatus.success({ + trialId, + state, + createdAt: new Date() + }) + ) + .take([ + trialSystemActivationStatus.success, + trialSystemActivationStatus.failure + ]) + .provide([[matchers.select(trialStatusSelector), state]]) + .not.put(trialSystemActivationStatusUpsert.request(trialId)) + .run(); + }); + + it("shouldn't do anything if an error occurs", async () => { + const trialId = "baz" as TrialId; + const state = SubscriptionStateEnum.UNSUBSCRIBED; + const store: DeepPartial = { + trialSystem: { + [trialId]: pot.some(state) + } + }; + return expectSaga(handleTrialSystemSubscription) + .withState(store) + .put(trialSystemActivationStatus.request(trialId)) + .dispatch( + trialSystemActivationStatus.failure({ + trialId, + error: new Error("foo") + }) + ) + .take([ + trialSystemActivationStatus.success, + trialSystemActivationStatus.failure + ]) + .provide([[matchers.select(trialStatusSelector), state]]) + .not.put(trialSystemActivationStatusUpsert.request(trialId)) + .run(); + }); +}); diff --git a/ts/features/itwallet/common/saga/index.ts b/ts/features/itwallet/common/saga/index.ts index d47125816ce..b877a1ab5a4 100644 --- a/ts/features/itwallet/common/saga/index.ts +++ b/ts/features/itwallet/common/saga/index.ts @@ -1,6 +1,10 @@ import { SagaIterator } from "redux-saga"; -import { fork, put, call } from "typed-redux-saga/macro"; -import { trialSystemActivationStatus } from "../../../trialSystem/store/actions"; +import { fork, put, call, take, select } from "typed-redux-saga/macro"; +import { isActionOf } from "typesafe-actions"; +import { + trialSystemActivationStatus, + trialSystemActivationStatusUpsert +} from "../../../trialSystem/store/actions"; import { watchItwIdentificationSaga } from "../../identification/saga"; import { checkWalletInstanceStateSaga } from "../../lifecycle/saga/checkWalletInstanceStateSaga"; import { handleWalletCredentialsRehydration } from "../../credentials/saga/handleWalletCredentialsRehydration"; @@ -9,6 +13,8 @@ import { itwCieIsSupported } from "../../identification/store/actions"; import { watchItwCredentialsSaga } from "../../credentials/saga"; import { watchItwLifecycleSaga } from "../../lifecycle/saga"; import { checkCredentialsStatusAttestation } from "../../credentials/saga/checkCredentialsStatusAttestation"; +import { trialStatusSelector } from "../../../trialSystem/store/reducers"; +import { SubscriptionStateEnum } from "../../../../../definitions/trial_system/SubscriptionState"; function* checkWalletInstanceAndCredentialsValiditySaga() { // Status attestations of credentials are checked only in case of a valid wallet instance. @@ -17,15 +23,33 @@ function* checkWalletInstanceAndCredentialsValiditySaga() { yield* call(checkCredentialsStatusAttestation); } +/** + * Handle the trial system subscription which currently fetches the trial status + * and if the user is unsubscribed it automatically subscribes the user to the trial. + */ +export function* handleTrialSystemSubscription() { + // IT Wallet trial status refresh + yield* put(trialSystemActivationStatus.request(itwTrialId)); + const outputAction = yield* take([ + trialSystemActivationStatus.success, + trialSystemActivationStatus.failure + ]); + if (isActionOf(trialSystemActivationStatus.success, outputAction)) { + const status = yield* select(trialStatusSelector(itwTrialId)); + if (status && status === SubscriptionStateEnum.UNSUBSCRIBED) { + yield* put(trialSystemActivationStatusUpsert.request(itwTrialId)); + } + } +} + export function* watchItwSaga(): SagaIterator { yield* fork(checkWalletInstanceAndCredentialsValiditySaga); yield* fork(handleWalletCredentialsRehydration); yield* fork(watchItwIdentificationSaga); yield* fork(watchItwCredentialsSaga); yield* fork(watchItwLifecycleSaga); + yield* fork(handleTrialSystemSubscription); // TODO: [SIW-1404] remove this CIE check and move the logic to xstate yield* put(itwCieIsSupported.request()); - // IT Wallet trial status refresh - yield* put(trialSystemActivationStatus.request(itwTrialId)); } diff --git a/ts/features/trialSystem/store/sagas/watchTrialSystemSaga.ts b/ts/features/trialSystem/store/sagas/watchTrialSystemSaga.ts index de230e97f08..f44382170bc 100644 --- a/ts/features/trialSystem/store/sagas/watchTrialSystemSaga.ts +++ b/ts/features/trialSystem/store/sagas/watchTrialSystemSaga.ts @@ -3,7 +3,6 @@ import { SagaIterator } from "redux-saga"; import { call, put, takeLatest } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; import { readableReport } from "@pagopa/ts-commons/lib/reporters"; -import { IOToast } from "@pagopa/io-app-design-system"; import { SessionToken } from "../../../../types/SessionToken"; import { TrialSystemClient, createTrialSystemClient } from "../../api/client"; import { apiUrlPrefix } from "../../../../config"; @@ -13,7 +12,6 @@ import { trialSystemActivationStatusUpsert } from "../actions"; import { getError } from "../../../../utils/errors"; -import I18n from "../../../../i18n"; function* handleTrialSystemActivationStatusUpsert( upsertTrialSystemActivationStatus: TrialSystemClient["createSubscription"], @@ -33,7 +31,6 @@ function* handleTrialSystemActivationStatusUpsert( ); } else if (result.right.status === 201) { yield* put(trialSystemActivationStatusUpsert.success(result.right.value)); - IOToast.success(I18n.t("features.trialSystem.toast.subscribed")); } else { yield* put( trialSystemActivationStatusUpsert.failure({ @@ -41,7 +38,6 @@ function* handleTrialSystemActivationStatusUpsert( error: new Error(`response status ${result.right.status}`) }) ); - IOToast.error(I18n.t("global.genericError")); } } catch (e) { yield* put( @@ -50,7 +46,6 @@ function* handleTrialSystemActivationStatusUpsert( error: getError(e) }) ); - IOToast.error(I18n.t("global.genericError")); } }