From 633ecad325ff1805484384de8047831bac204822 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Wed, 24 Apr 2024 10:52:56 +0200 Subject: [PATCH 01/31] chore: add xstate v5 --- package.json | 4 +++- yarn.lock | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9db270f24a1..95c52d63eef 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@react-navigation/stack": "6.3.20", "@redux-saga/testing-utils": "^1.1.3", "@xstate/react": "^3.0.1", + "@xstate/react5": "npm:@xstate/react@4.1.1", "async-mutex": "^0.1.3", "buffer": "^4.9.1", "color": "^3.0.0", @@ -217,7 +218,8 @@ "vision-camera-code-scanner": "^0.2.0", "xml2js": "^0.5.0", "xss": "1.0.10", - "xstate": "^4.33.6" + "xstate": "^4.33.6", + "xstate5": "npm:xstate@5.11.0" }, "devDependencies": { "@babel/core": "^7.18.8", diff --git a/yarn.lock b/yarn.lock index 650549a96ef..cbe07922d33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4545,6 +4545,14 @@ resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.7.1.tgz#157d5083db3f116b7ae28b5b3aef8f457f052491" integrity sha512-dQEt6enmHXtD93vDcMefhb5bh1zh0mLCRT8CvYJjCpTjaTth7sXqlU6ri1qP0HDR6IbU9s2/WVNw7Oy7O/Sqfg== +"@xstate/react5@npm:@xstate/react@4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa" + integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA== + dependencies: + use-isomorphic-layout-effect "^1.1.2" + use-sync-external-store "^1.2.0" + "@xstate/react@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095" @@ -16941,7 +16949,7 @@ url-parse@^1.5.9: querystringify "^2.1.1" requires-port "^1.0.0" -use-isomorphic-layout-effect@^1.0.0: +use-isomorphic-layout-effect@^1.0.0, use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== @@ -16951,7 +16959,7 @@ use-latest-callback@^0.1.5, use-latest-callback@^0.1.7: resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== -use-sync-external-store@^1.0.0: +use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -17355,6 +17363,11 @@ xss@1.0.10: commander "^2.20.3" cssfilter "0.0.10" +"xstate5@npm:xstate@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.11.0.tgz#2155f750d9ff6c4d24b3a9aee620e7e6661dae0c" + integrity sha512-0MqTLpc7dr/hXFHY25oN4sdnO3Ey6MYy9WkWxOgiwjPV0S6rWwLb5nZlRlPDSku2GEV4/y6AR8bX+GNCOxnEwA== + xstate@^4.29.0, xstate@^4.33.6: version "4.35.4" resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.35.4.tgz#87b2a45b6c7e84d820f56378408c6531ca5c4662" From 1bb524982367d8349bd1c856c025bc1b44bed12d Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Wed, 24 Apr 2024 15:48:35 +0200 Subject: [PATCH 02/31] chore: migrate idpay unsubscription machine --- .../__tests__/machine.test.ts | 0 .../__tests__/services.test.ts | 4 +- .../{xstate => machine}/actions.ts | 7 +- .../idpay/unsubscription/machine/actors.ts | 91 ++++++++++ .../idpay/unsubscription/machine/context.ts | 13 ++ .../idpay/unsubscription/machine/events.ts | 16 ++ .../idpay/unsubscription/machine/input.ts | 11 ++ .../idpay/unsubscription/machine/machine.ts | 149 ++++++++++++++++ .../{xstate => machine}/provider.tsx | 68 ++------ .../{xstate => machine}/selectors.ts | 8 +- .../unsubscription/navigation/navigator.tsx | 2 +- .../UnsubscriptionConfirmationScreen.tsx | 19 ++- .../screens/UnsubscriptionResultScreen.tsx | 20 +-- .../{xstate => types}/failure.ts | 0 .../idpay/unsubscription/xstate/context.ts | 7 - .../idpay/unsubscription/xstate/events.ts | 9 - .../idpay/unsubscription/xstate/machine.ts | 159 ------------------ .../idpay/unsubscription/xstate/services.ts | 103 ------------ ts/xstate/utils/index.ts | 4 + 19 files changed, 330 insertions(+), 360 deletions(-) rename ts/features/idpay/unsubscription/{xstate => machine}/__tests__/machine.test.ts (100%) rename ts/features/idpay/unsubscription/{xstate => machine}/__tests__/services.test.ts (96%) rename ts/features/idpay/unsubscription/{xstate => machine}/actions.ts (88%) create mode 100644 ts/features/idpay/unsubscription/machine/actors.ts create mode 100644 ts/features/idpay/unsubscription/machine/context.ts create mode 100644 ts/features/idpay/unsubscription/machine/events.ts create mode 100644 ts/features/idpay/unsubscription/machine/input.ts create mode 100644 ts/features/idpay/unsubscription/machine/machine.ts rename ts/features/idpay/unsubscription/{xstate => machine}/provider.tsx (56%) rename ts/features/idpay/unsubscription/{xstate => machine}/selectors.ts (90%) rename ts/features/idpay/unsubscription/{xstate => types}/failure.ts (100%) delete mode 100644 ts/features/idpay/unsubscription/xstate/context.ts delete mode 100644 ts/features/idpay/unsubscription/xstate/events.ts delete mode 100644 ts/features/idpay/unsubscription/xstate/machine.ts delete mode 100644 ts/features/idpay/unsubscription/xstate/services.ts diff --git a/ts/features/idpay/unsubscription/xstate/__tests__/machine.test.ts b/ts/features/idpay/unsubscription/machine/__tests__/machine.test.ts similarity index 100% rename from ts/features/idpay/unsubscription/xstate/__tests__/machine.test.ts rename to ts/features/idpay/unsubscription/machine/__tests__/machine.test.ts diff --git a/ts/features/idpay/unsubscription/xstate/__tests__/services.test.ts b/ts/features/idpay/unsubscription/machine/__tests__/services.test.ts similarity index 96% rename from ts/features/idpay/unsubscription/xstate/__tests__/services.test.ts rename to ts/features/idpay/unsubscription/machine/__tests__/services.test.ts index 6c3dc69b45a..859549504aa 100644 --- a/ts/features/idpay/unsubscription/xstate/__tests__/services.test.ts +++ b/ts/features/idpay/unsubscription/machine/__tests__/services.test.ts @@ -7,9 +7,9 @@ import { import { mockIDPayClient } from "../../../common/api/__mocks__/client"; import { Context } from "../context"; -import { createServicesImplementation } from "../services"; +import { createServicesImplementation } from "../actors"; import { ErrorDTO } from "../../../../../../definitions/idpay/ErrorDTO"; -import { UnsubscriptionFailureEnum } from "../failure"; +import { UnsubscriptionFailureEnum } from "../../types/failure"; const T_PREFERRED_LANGUAGE = PreferredLanguageEnum.it_IT; const T_AUTH_TOKEN = "abc123"; diff --git a/ts/features/idpay/unsubscription/xstate/actions.ts b/ts/features/idpay/unsubscription/machine/actions.ts similarity index 88% rename from ts/features/idpay/unsubscription/xstate/actions.ts rename to ts/features/idpay/unsubscription/machine/actions.ts index 1b98cf9af02..2f2474f03d1 100644 --- a/ts/features/idpay/unsubscription/xstate/actions.ts +++ b/ts/features/idpay/unsubscription/machine/actions.ts @@ -1,14 +1,11 @@ -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import ROUTES from "../../../../navigation/routes"; import { useIODispatch } from "../../../../store/hooks"; import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayUnsubscriptionRoutes } from "../navigation/navigator"; const createActionsImplementation = ( - navigation: IOStackNavigationProp, + navigation: ReturnType, dispatch: ReturnType ) => { const handleSessionExpired = () => { diff --git a/ts/features/idpay/unsubscription/machine/actors.ts b/ts/features/idpay/unsubscription/machine/actors.ts new file mode 100644 index 00000000000..2e835b212f7 --- /dev/null +++ b/ts/features/idpay/unsubscription/machine/actors.ts @@ -0,0 +1,91 @@ +import * as E from "fp-ts/lib/Either"; +import * as TE from "fp-ts/lib/TaskEither"; +import { flow, pipe } from "fp-ts/lib/function"; +import { fromPromise } from "xstate5"; +import { PreferredLanguage } from "../../../../../definitions/backend/PreferredLanguage"; +import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; +import { IDPayClient } from "../../common/api/client"; +import { UnsubscriptionFailureEnum } from "../types/failure"; + +export const createActorsImplementation = ( + client: IDPayClient, + token: string, + language: PreferredLanguage +) => { + const getInitiativeInfo = fromPromise( + async (params: { input: string }): Promise => { + const dataResponse = await TE.tryCatch( + async () => + await client.getWalletDetail({ + bearerAuth: token, + "Accept-Language": language, + initiativeId: params.input + }), + E.toError + )(); + + return pipe( + dataResponse, + E.fold( + () => Promise.reject(UnsubscriptionFailureEnum.UNEXPECTED), + flow( + E.map(({ status, value }) => { + switch (status) { + case 200: + return Promise.resolve(value); + case 401: + return Promise.reject( + UnsubscriptionFailureEnum.SESSION_EXPIRED + ); + default: + return Promise.reject(UnsubscriptionFailureEnum.GENERIC); + } + }), + E.getOrElse(() => Promise.reject(UnsubscriptionFailureEnum.GENERIC)) + ) + ) + ); + } + ); + + const unsubscribeFromInitiative = fromPromise( + async (params: { input: string }): Promise => { + const dataResponse = await TE.tryCatch( + async () => + await client.unsubscribe({ + bearerAuth: token, + "Accept-Language": language, + initiativeId: params.input + }), + E.toError + )(); + + return pipe( + dataResponse, + E.fold( + () => Promise.reject(UnsubscriptionFailureEnum.UNEXPECTED), + flow( + E.map(({ status }) => { + switch (status) { + case 204: + return Promise.resolve(undefined); + case 401: + return Promise.reject( + UnsubscriptionFailureEnum.SESSION_EXPIRED + ); + default: + return Promise.reject(UnsubscriptionFailureEnum.GENERIC); + } + }), + E.getOrElse(() => Promise.reject(UnsubscriptionFailureEnum.GENERIC)) + ) + ) + ); + } + ); + + return { + getInitiativeInfo, + unsubscribeFromInitiative + }; +}; diff --git a/ts/features/idpay/unsubscription/machine/context.ts b/ts/features/idpay/unsubscription/machine/context.ts new file mode 100644 index 00000000000..f4bde1cc8c0 --- /dev/null +++ b/ts/features/idpay/unsubscription/machine/context.ts @@ -0,0 +1,13 @@ +import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; + +export interface Context { + readonly initiativeId: string; + readonly initiativeName: string | undefined; + readonly initiativeType: InitiativeRewardTypeEnum | undefined; +} + +export const Context: Context = { + initiativeId: "", + initiativeName: undefined, + initiativeType: undefined +}; diff --git a/ts/features/idpay/unsubscription/machine/events.ts b/ts/features/idpay/unsubscription/machine/events.ts new file mode 100644 index 00000000000..40ccb68c4d0 --- /dev/null +++ b/ts/features/idpay/unsubscription/machine/events.ts @@ -0,0 +1,16 @@ +import * as Input from "./input"; + +export interface AutoInit { + readonly type: "xstate.init"; + readonly input: Input.Input; +} + +export interface Exit { + readonly type: "exit"; +} + +export interface ConfirmUnsubscription { + readonly type: "confirm-unsubscription"; +} + +export type Events = AutoInit | Exit | ConfirmUnsubscription; diff --git a/ts/features/idpay/unsubscription/machine/input.ts b/ts/features/idpay/unsubscription/machine/input.ts new file mode 100644 index 00000000000..a9901ba7742 --- /dev/null +++ b/ts/features/idpay/unsubscription/machine/input.ts @@ -0,0 +1,11 @@ +import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; +import * as Context from "./context"; + +export interface Input { + readonly initiativeId: string; + readonly initiativeName: string | undefined; + readonly initiativeType: InitiativeRewardTypeEnum | undefined; +} + +export const Input = (input: Input): Promise => + Promise.resolve({ ...input }); diff --git a/ts/features/idpay/unsubscription/machine/machine.ts b/ts/features/idpay/unsubscription/machine/machine.ts new file mode 100644 index 00000000000..8eeb735ee58 --- /dev/null +++ b/ts/features/idpay/unsubscription/machine/machine.ts @@ -0,0 +1,149 @@ +import { assertEvent, fromPromise, setup } from "xstate5"; +import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; +import { + LOADING_TAG, + WAITING_USER_INPUT_TAG, + notImplementedStub +} from "../../../../xstate/utils"; +import * as Context from "./context"; +import * as Events from "./events"; +import * as Input from "./input"; + +export const idPayUnsubscriptionMachine = setup({ + types: { + input: {} as Input.Input, + context: {} as Context.Context, + events: {} as Events.Events + }, + actions: { + navigateToConfirmationScreen: () => { + throw new Error("Not implemented"); + }, + navigateToResultScreen: () => { + throw new Error("Not implemented"); + }, + exitToWallet: () => { + throw new Error("Not implemented"); + }, + exitUnsubscription: () => { + throw new Error("Not implemented"); + }, + handleSessionExpired: () => { + throw new Error("Not implemented"); + } + }, + actors: { + onInit: fromPromise(({ input }) => + Input.Input(input) + ), + getInitiativeInfo: fromPromise(notImplementedStub), + unsubscribeFromInitiative: fromPromise( + notImplementedStub + ) + }, + guards: { + hasMissingInitiativeData: ({ context }) => + !!context.initiativeName || !!context.initiativeType, + isSessionExpired: data => { + // eslint-disable-next-line no-console + console.log(data); + return false; + } + } +}).createMachine({ + id: "idpay-unsubscription", + context: Context.Context, + entry: "navigateToConfirmationScreen", + invoke: { + src: "onInit", + input: ({ event }) => { + assertEvent(event, "xstate.init"); + return event.input; + }, + onError: { + target: ".UnsubscriptionFailure" + }, + onDone: [ + { + guard: "hasMissingInitiativeData", + target: ".LoadingInitiativeInfo" + }, + { + target: ".Idle" + } + ] + }, + initial: "Idle", + states: { + Idle: { + tags: [LOADING_TAG] + }, + LoadingInitiativeInfo: { + tags: [LOADING_TAG], + invoke: { + input: ({ context }) => context.initiativeId, + src: "getInitiativeInfo", + onError: [ + { + guard: "isSessionExpired", + target: ".SessionExpired" + }, + { + target: ".UnsubscriptionFailure" + } + ], + onDone: { + target: ".WaitingConfirmation" + } + } + }, + WaitingConfirmation: { + tags: [WAITING_USER_INPUT_TAG], + on: { + "confirm-unsubscription": { + target: ".Unsubscribing" + } + } + }, + Unsubscribing: { + tags: [LOADING_TAG], + invoke: { + input: ({ context }) => context.initiativeId, + src: "unsubscribeFromInitiative", + onError: [ + { + guard: "isSessionExpired", + target: ".SessionExpired" + }, + { + target: ".UnsubscriptionFailure" + } + ], + onDone: { + target: ".UnsubscriptionSuccess" + } + } + }, + UnsubscriptionSuccess: { + entry: "navigateToResultScreen", + on: { + exit: { + actions: "exitToWallet" + } + } + }, + UnsubscriptionFailure: { + entry: "navigateToResultScreen", + on: { + exit: { + actions: "exitUnsubscription" + } + } + }, + SessionExpired: { + entry: ["handleSessionExpired", "exitUnsubscription"] + } + } +}); + +export type IdPayUnsubscriptionMachine = typeof idPayUnsubscriptionMachine; diff --git a/ts/features/idpay/unsubscription/xstate/provider.tsx b/ts/features/idpay/unsubscription/machine/provider.tsx similarity index 56% rename from ts/features/idpay/unsubscription/xstate/provider.tsx rename to ts/features/idpay/unsubscription/machine/provider.tsx index e58bee423b3..7630c14ed03 100644 --- a/ts/features/idpay/unsubscription/xstate/provider.tsx +++ b/ts/features/idpay/unsubscription/machine/provider.tsx @@ -1,8 +1,7 @@ -import { useInterpret } from "@xstate/react"; +import { createActorContext } from "@xstate/react5"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; -import { InterpreterFrom } from "xstate"; import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import { @@ -10,7 +9,6 @@ import { idPayApiUatBaseUrl, idPayTestToken } from "../../../../config"; -import { useXStateMachine } from "../../../../xstate/hooks/useXStateMachine"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { sessionInfoSelector } from "../../../../store/reducers/authentication"; @@ -21,19 +19,12 @@ import { import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; -import { - IDPayUnsubscriptionMachineType, - createIDPayUnsubscriptionMachine -} from "./machine"; -import { createServicesImplementation } from "./services"; - -type UnsubscriptionMachineContext = - InterpreterFrom; +import { createActorsImplementation } from "./actors"; +import { idPayUnsubscriptionMachine } from "./machine"; -const UnsubscriptionMachineContext = - React.createContext( - {} as UnsubscriptionMachineContext - ); +export const IdPayUnsubscriptionMachineContext = createActorContext( + idPayUnsubscriptionMachine +); type Props = { children: React.ReactNode; @@ -42,29 +33,20 @@ type Props = { initiativeType?: InitiativeRewardTypeEnum; }; -const IDPayUnsubscriptionMachineProvider = (props: Props) => { - const { initiativeId, initiativeName, initiativeType } = props; - +export const IDPayUnsubscriptionMachineProvider = ({ children }: Props) => { + const navigation = useIONavigation(); const dispatch = useIODispatch(); - const [machine] = useXStateMachine(() => - createIDPayUnsubscriptionMachine({ - initiativeId, - initiativeName, - initiativeType - }) - ); const sessionInfo = useIOSelector(sessionInfoSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); + const preferredLanguageOption = useIOSelector(preferredLanguageSelector); const language = pipe( - useIOSelector(preferredLanguageSelector), + preferredLanguageOption, O.map(fromLocaleToPreferredLanguage), O.getOrElse(() => PreferredLanguageEnum.it_IT) ); - const navigation = useIONavigation(); - if (O.isNone(sessionInfo)) { throw new Error("Session info is undefined"); } @@ -72,36 +54,20 @@ const IDPayUnsubscriptionMachineProvider = (props: Props) => { const { bpdToken } = sessionInfo.value; const idPayToken = idPayTestToken ?? bpdToken; - const idPayClient = createIDPayClient( isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); - const services = createServicesImplementation( - idPayClient, - idPayToken, - language - ); - + const actors = createActorsImplementation(idPayClient, idPayToken, language); const actions = createActionsImplementation(navigation, dispatch); - - const machineService = useInterpret(machine, { - actions, - services + const machine = idPayUnsubscriptionMachine.provide({ + actors, + actions }); return ( - - {props.children} - + + {children} + ); }; - -const useUnsubscriptionMachineService = () => - React.useContext(UnsubscriptionMachineContext); - -export { - IDPayUnsubscriptionMachineProvider, - UnsubscriptionMachineContext, - useUnsubscriptionMachineService -}; diff --git a/ts/features/idpay/unsubscription/xstate/selectors.ts b/ts/features/idpay/unsubscription/machine/selectors.ts similarity index 90% rename from ts/features/idpay/unsubscription/xstate/selectors.ts rename to ts/features/idpay/unsubscription/machine/selectors.ts index 0d68e7f7ad5..030309434cc 100644 --- a/ts/features/idpay/unsubscription/xstate/selectors.ts +++ b/ts/features/idpay/unsubscription/machine/selectors.ts @@ -1,13 +1,13 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; -import { StateFrom } from "xstate"; +import { StateFrom } from "xstate5"; import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import I18n from "../../../../i18n"; import { LOADING_TAG } from "../../../../xstate/utils"; -import { IDPayUnsubscriptionMachineType } from "./machine"; +import { IdPayUnsubscriptionMachine } from "./machine"; -type StateWithContext = StateFrom; +type StateWithContext = StateFrom; export const selectInitiativeName = (state: StateWithContext) => state.context.initiativeName; @@ -19,7 +19,7 @@ export const isLoadingSelector = createSelector(selectTags, tags => ); export const selectIsFailure = (state: StateWithContext) => - state.matches("UNSUBSCRIPTION_FAILURE"); + state.matches("UnsubscriptionFailure"); export const selectInitiativeType = (state: StateWithContext) => pipe( diff --git a/ts/features/idpay/unsubscription/navigation/navigator.tsx b/ts/features/idpay/unsubscription/navigation/navigator.tsx index bc7ea864e67..ab42e28b8ba 100644 --- a/ts/features/idpay/unsubscription/navigation/navigator.tsx +++ b/ts/features/idpay/unsubscription/navigation/navigator.tsx @@ -3,7 +3,7 @@ import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; import UnsubscriptionConfirmationScreen from "../screens/UnsubscriptionConfirmationScreen"; import UnsubscriptionResultScreen from "../screens/UnsubscriptionResultScreen"; -import { IDPayUnsubscriptionMachineProvider } from "../xstate/provider"; +import { IDPayUnsubscriptionMachineProvider } from "../machine/provider"; import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; export const IDPayUnsubscriptionRoutes = { diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx index 1bc3683a0ac..be57bde0b43 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx @@ -4,7 +4,6 @@ import { IconButton, VSpacer } from "@pagopa/io-app-design-system"; -import { useSelector } from "@xstate/react"; import React from "react"; import { SafeAreaView, View } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; @@ -19,29 +18,31 @@ import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; import { UnsubscriptionCheckListItem } from "../components/UnsubscriptionCheckListItem"; -import { useUnsubscriptionMachineService } from "../xstate/provider"; +import { IdPayUnsubscriptionMachineContext } from "../machine/provider"; import { isLoadingSelector, selectInitiativeName, selectUnsubscriptionChecks -} from "../xstate/selectors"; +} from "../machine/selectors"; const UnsubscriptionConfirmationScreen = () => { - const machine = useUnsubscriptionMachineService(); - const isLoading = useSelector(machine, isLoadingSelector); - const initiativeName = useSelector(machine, selectInitiativeName); - const unsubscriptionChecks = useSelector(machine, selectUnsubscriptionChecks); + const { useActorRef, useSelector } = IdPayUnsubscriptionMachineContext; + const machine = useActorRef(); + + const isLoading = useSelector(isLoadingSelector); + const initiativeName = useSelector(selectInitiativeName); + const unsubscriptionChecks = useSelector(selectUnsubscriptionChecks); const checks = useConfirmationChecks(unsubscriptionChecks.length); const handleClosePress = () => machine.send({ - type: "EXIT" + type: "exit" }); const handleConfirmPress = () => { machine.send({ - type: "CONFIRM_UNSUBSCRIPTION" + type: "confirm-unsubscription" }); }; diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx index bcf1000e5c4..1b98dacfead 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx @@ -1,19 +1,18 @@ -import { useSelector } from "@xstate/react"; -import React from "react"; -import { SafeAreaView, StyleSheet, View } from "react-native"; import { + Body, ButtonOutline, - VSpacer, IOPictograms, + IOStyles, Pictogram, - Body, - IOStyles + VSpacer } from "@pagopa/io-app-design-system"; +import React from "react"; +import { SafeAreaView, StyleSheet, View } from "react-native"; import { H3 } from "../../../../components/core/typography/H3"; import I18n from "../../../../i18n"; import themeVariables from "../../../../theme/variables"; -import { useUnsubscriptionMachineService } from "../xstate/provider"; -import { selectIsFailure } from "../xstate/selectors"; +import { IdPayUnsubscriptionMachineContext } from "../machine/provider"; +import { selectIsFailure } from "../machine/selectors"; type ScreenContentType = { pictogram: IOPictograms; @@ -23,8 +22,9 @@ type ScreenContentType = { }; const UnsubscriptionResultScreen = () => { - const machine = useUnsubscriptionMachineService(); - const isFailure = useSelector(machine, selectIsFailure); + const { useActorRef, useSelector } = IdPayUnsubscriptionMachineContext; + const machine = useActorRef(); + const isFailure = useSelector(selectIsFailure); const { pictogram, title, content, buttonLabel }: ScreenContentType = isFailure diff --git a/ts/features/idpay/unsubscription/xstate/failure.ts b/ts/features/idpay/unsubscription/types/failure.ts similarity index 100% rename from ts/features/idpay/unsubscription/xstate/failure.ts rename to ts/features/idpay/unsubscription/types/failure.ts diff --git a/ts/features/idpay/unsubscription/xstate/context.ts b/ts/features/idpay/unsubscription/xstate/context.ts deleted file mode 100644 index 5f0e9b37f0a..00000000000 --- a/ts/features/idpay/unsubscription/xstate/context.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; - -export type Context = { - initiativeId: string; - initiativeName?: string; - initiativeType?: InitiativeRewardTypeEnum; -}; diff --git a/ts/features/idpay/unsubscription/xstate/events.ts b/ts/features/idpay/unsubscription/xstate/events.ts deleted file mode 100644 index 3e6bccc1573..00000000000 --- a/ts/features/idpay/unsubscription/xstate/events.ts +++ /dev/null @@ -1,9 +0,0 @@ -type E_EXIT = { - type: "EXIT"; -}; - -type E_CONFIRM_UNSUBSCRIPTION = { - type: "CONFIRM_UNSUBSCRIPTION"; -}; - -export type Events = E_EXIT | E_CONFIRM_UNSUBSCRIPTION; diff --git a/ts/features/idpay/unsubscription/xstate/machine.ts b/ts/features/idpay/unsubscription/xstate/machine.ts deleted file mode 100644 index 9e410528189..00000000000 --- a/ts/features/idpay/unsubscription/xstate/machine.ts +++ /dev/null @@ -1,159 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { assign, createMachine } from "xstate"; -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; -import { LOADING_TAG, WAITING_USER_INPUT_TAG } from "../../../../xstate/utils"; -import { Context } from "./context"; -import { Events } from "./events"; -import { UnsubscriptionFailure, UnsubscriptionFailureEnum } from "./failure"; -import { Services } from "./services"; - -type UnsubscriptionMachineParams = { - initiativeId: string; - initiativeName?: string; - initiativeType?: InitiativeRewardTypeEnum; -}; - -const createIDPayUnsubscriptionMachine = ( - params: UnsubscriptionMachineParams -) => - createMachine( - { - /** @xstate-layout N4IgpgJg5mDOIC5QEkAiAFAggTQPoFUA5AZXwCFiBhAJWXQBVkB5QgYgFEANZegbQAYAuolAAHAPawAlgBcp4gHYiQAD0QBOABwAmAHQBmAIwA2fQHYALP20XN-Y5oA0IAJ6JD2gL6fnaLHiJSCho6RhZdAHVMHmRCAHFcWJjMRgA1dlxidgAZdkowtizc-MTCZLT2AWEkEAlpOUVlNQR9C0NdTQBWQ06LfSMzdUMLTs7nNwRtfnbjPu1DMzNOxeNRs29fDBwCEnIqWgZmQl1UZGJ0bJxYhMoWADFkagBZFKPWW8IH552g-dCjqrKOqyeRKGrNTTGdrWfpaEZ9CzqCzjRDaGy6KHqQbaSH8azdCwbEB+baBPYhQ7hMnBWhka6sCCKMC6KQKABu4gA1syAK4KWA8gBGsAAxgAnKSCsB3MXiAC2yAUIIAhnI2WBATVgQ0waBmj0jLoYeZlnZ+GYHCjJvxNLozPwjDbcfxeupOkSSQFdjT-lTvX86fFWGAxbKxbpRAAbVUAM3EYrluj5AuF4sl0tlCqVclVUnVmrEkhBjXB7k6huNSzMZotTlciCMxjtDo8xmMFsROO8PhACnEEDgyk9P3JBwKQKLOqaiAAtIYrW7dCMjPoHfpZvw3YSe8PqX9KccojF4qVysh0pkcnlx1rJ6DpwgLGYrWjOrpDIYseoccY8doCR6Wxer8FIFCcZwXFcJ4fF8Lw3oW9T3qWCC9G+FgjDieL2ssnR1hMUwzJ+FrmN+nYuoB-gjj6B66HuISBnEE6ISWeruIY+hvsa6jTJomiGHieHuN+75Ee2Zjruo6gcdumyUXRY5HLR-qgUcmT4JQlDsMQxBMcWuqqO4gy2sYbo2BxG4OGM9YIIYOi6G6jporxm42O6O5AVR+5gfJvqELgdzRNk+DUOwulTshCxaBipl9J0FldC+3H2eWHjTPwy4Otu3hAA */ - context: { - initiativeId: params.initiativeId, - initiativeName: params.initiativeName, - initiativeType: params.initiativeType - }, - tsTypes: {} as import("./machine.typegen").Typegen0, - schema: { - context: {} as Context, - events: {} as Events, - services: {} as Services - }, - predictableActionArguments: true, - id: "IDPAY_UNSUBSCRIPTION", - initial: "START_UNSUBSCRIPTION", - entry: "navigateToConfirmationScreen", - states: { - START_UNSUBSCRIPTION: { - tags: [LOADING_TAG], - always: [ - { - cond: "hasMissingInitiativeInfo", - target: "LOADING_INITIATIVE_INFO" - }, - { - target: "AWAITING_CONFIRMATION" - } - ] - }, - - LOADING_INITIATIVE_INFO: { - tags: [LOADING_TAG], - invoke: { - id: "getInitiativeInfo", - src: "getInitiativeInfo", - onDone: { - actions: "loadInitiativeSuccess", - target: "AWAITING_CONFIRMATION" - }, - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - target: "UNSUBSCRIPTION_FAILURE" - } - ] - } - }, - - AWAITING_CONFIRMATION: { - tags: [WAITING_USER_INPUT_TAG], - on: { - EXIT: { - actions: "exitUnsubscription" - }, - CONFIRM_UNSUBSCRIPTION: { - target: "UNSUBSCRIBING" - } - } - }, - - UNSUBSCRIBING: { - tags: [LOADING_TAG], - invoke: { - id: "unsubscribeFromInitiative", - src: "unsubscribeFromInitiative", - onDone: { - target: "UNSUBSCRIPTION_SUCCESS" - }, - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - target: "UNSUBSCRIPTION_FAILURE" - } - ] - } - }, - - UNSUBSCRIPTION_SUCCESS: { - entry: "navigateToResultScreen", - on: { - EXIT: { - actions: "exitToWallet" - } - } - }, - - UNSUBSCRIPTION_FAILURE: { - entry: "navigateToResultScreen", - on: { - EXIT: { - actions: "exitUnsubscription" - } - } - }, - - SESSION_EXPIRED: { - entry: ["handleSessionExpired", "exitUnsubscription"] - } - } - }, - { - actions: { - loadInitiativeSuccess: assign((_, event) => ({ - initiativeName: event.data.initiativeName, - initiativeType: event.data.initiativeRewardType - })) - }, - guards: { - hasMissingInitiativeInfo, - isSessionExpired: (_, event) => - pipe( - event.data, - UnsubscriptionFailure.decode, - O.fromEither, - O.filter( - failure => failure === UnsubscriptionFailureEnum.SESSION_EXPIRED - ), - O.isSome - ) - } - } - ); - -const hasMissingInitiativeInfo = (context: Context) => - context.initiativeName === undefined; - -type IDPayUnsubscriptionMachineType = ReturnType< - typeof createIDPayUnsubscriptionMachine ->; - -export { createIDPayUnsubscriptionMachine }; -export type { IDPayUnsubscriptionMachineType, UnsubscriptionMachineParams }; diff --git a/ts/features/idpay/unsubscription/xstate/services.ts b/ts/features/idpay/unsubscription/xstate/services.ts deleted file mode 100644 index f7c733077c2..00000000000 --- a/ts/features/idpay/unsubscription/xstate/services.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import * as E from "fp-ts/lib/Either"; -import * as TE from "fp-ts/lib/TaskEither"; -import { flow, pipe } from "fp-ts/lib/function"; -import { PreferredLanguage } from "../../../../../definitions/backend/PreferredLanguage"; -import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; -import { IDPayClient } from "../../common/api/client"; -import { Context } from "./context"; -import { UnsubscriptionFailureEnum } from "./failure"; - -export type Services = { - getInitiativeInfo: { - data: InitiativeDTO; - }; - unsubscribeFromInitiative: { - data: undefined; - }; -}; - -const createServicesImplementation = ( - client: IDPayClient, - token: string, - language: PreferredLanguage -) => { - const getInitiativeInfo = async ( - context: Context - ): Promise => { - const dataResponse = await TE.tryCatch( - async () => - await client.getWalletDetail({ - bearerAuth: token, - "Accept-Language": language, - initiativeId: context.initiativeId - }), - E.toError - )(); - - return pipe( - dataResponse, - E.fold( - () => Promise.reject(UnsubscriptionFailureEnum.UNEXPECTED), - flow( - E.map(({ status, value }) => { - switch (status) { - case 200: - return Promise.resolve(value); - case 401: - return Promise.reject( - UnsubscriptionFailureEnum.SESSION_EXPIRED - ); - default: - return Promise.reject(UnsubscriptionFailureEnum.GENERIC); - } - }), - E.getOrElse(() => Promise.reject(undefined)) - ) - ) - ); - }; - - const unsubscribeFromInitiative = async ( - context: Context - ): Promise => { - const dataResponse = await TE.tryCatch( - async () => - await client.unsubscribe({ - bearerAuth: token, - "Accept-Language": language, - initiativeId: context.initiativeId - }), - E.toError - )(); - - return pipe( - dataResponse, - E.fold( - () => Promise.reject(UnsubscriptionFailureEnum.UNEXPECTED), - flow( - E.map(({ status }) => { - switch (status) { - case 204: - return Promise.resolve(undefined); - case 401: - return Promise.reject( - UnsubscriptionFailureEnum.SESSION_EXPIRED - ); - default: - return Promise.reject(UnsubscriptionFailureEnum.GENERIC); - } - }), - E.getOrElse(() => Promise.reject(undefined)) - ) - ) - ); - }; - - return { - getInitiativeInfo, - unsubscribeFromInitiative - }; -}; - -export { createServicesImplementation }; diff --git a/ts/xstate/utils/index.ts b/ts/xstate/utils/index.ts index 6d73ae60baf..2e9ca5514ce 100644 --- a/ts/xstate/utils/index.ts +++ b/ts/xstate/utils/index.ts @@ -3,3 +3,7 @@ const UPSERTING_TAG = "UPSERTING"; const WAITING_USER_INPUT_TAG = "WAITING_USER_INPUT"; export { LOADING_TAG, UPSERTING_TAG, WAITING_USER_INPUT_TAG }; + +export const notImplementedStub = () => { + throw new Error("Not implemented"); +}; From bdbce66f04ff5c4643e7a8ca7348f7a29685c0d9 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Wed, 24 Apr 2024 17:31:01 +0200 Subject: [PATCH 03/31] chore: working unsubscription machine --- package.json | 8 +-- .../idpay/unsubscription/machine/actors.ts | 2 +- .../idpay/unsubscription/machine/machine.ts | 51 ++++++++++++------- .../idpay/unsubscription/machine/provider.tsx | 14 +++-- .../idpay/unsubscription/machine/selectors.ts | 2 +- .../UnsubscriptionConfirmationScreen.tsx | 1 + .../screens/UnsubscriptionResultScreen.tsx | 2 +- yarn.lock | 33 +++++++----- 8 files changed, 70 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 95c52d63eef..76506f0a513 100644 --- a/package.json +++ b/package.json @@ -116,8 +116,8 @@ "@react-navigation/native": "6.1.9", "@react-navigation/stack": "6.3.20", "@redux-saga/testing-utils": "^1.1.3", - "@xstate/react": "^3.0.1", - "@xstate/react5": "npm:@xstate/react@4.1.1", + "@xstate/react": "npm:@xstate/react@4", + "@xstate4/react": "npm:@xstate/react@3.0.1", "async-mutex": "^0.1.3", "buffer": "^4.9.1", "color": "^3.0.0", @@ -218,8 +218,8 @@ "vision-camera-code-scanner": "^0.2.0", "xml2js": "^0.5.0", "xss": "1.0.10", - "xstate": "^4.33.6", - "xstate5": "npm:xstate@5.11.0" + "xstate": "^5", + "xstate4": "npm:xstate@4.33.6" }, "devDependencies": { "@babel/core": "^7.18.8", diff --git a/ts/features/idpay/unsubscription/machine/actors.ts b/ts/features/idpay/unsubscription/machine/actors.ts index 2e835b212f7..5511608a796 100644 --- a/ts/features/idpay/unsubscription/machine/actors.ts +++ b/ts/features/idpay/unsubscription/machine/actors.ts @@ -1,7 +1,7 @@ import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; import { flow, pipe } from "fp-ts/lib/function"; -import { fromPromise } from "xstate5"; +import { fromPromise } from "xstate"; import { PreferredLanguage } from "../../../../../definitions/backend/PreferredLanguage"; import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; import { IDPayClient } from "../../common/api/client"; diff --git a/ts/features/idpay/unsubscription/machine/machine.ts b/ts/features/idpay/unsubscription/machine/machine.ts index 8eeb735ee58..1ff1526f4da 100644 --- a/ts/features/idpay/unsubscription/machine/machine.ts +++ b/ts/features/idpay/unsubscription/machine/machine.ts @@ -1,4 +1,4 @@ -import { assertEvent, fromPromise, setup } from "xstate5"; +import { assertEvent, assign, fromPromise, setup } from "xstate"; import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; import { LOADING_TAG, @@ -43,7 +43,8 @@ export const idPayUnsubscriptionMachine = setup({ }, guards: { hasMissingInitiativeData: ({ context }) => - !!context.initiativeName || !!context.initiativeType, + context.initiativeName === undefined || + context.initiativeType === undefined, isSessionExpired: data => { // eslint-disable-next-line no-console console.log(data); @@ -63,20 +64,24 @@ export const idPayUnsubscriptionMachine = setup({ onError: { target: ".UnsubscriptionFailure" }, - onDone: [ - { - guard: "hasMissingInitiativeData", - target: ".LoadingInitiativeInfo" - }, - { - target: ".Idle" - } - ] + onDone: { + actions: assign(event => ({ ...event.event.output })), + target: ".Idle" + } }, initial: "Idle", states: { Idle: { - tags: [LOADING_TAG] + tags: [LOADING_TAG], + always: [ + { + guard: "hasMissingInitiativeData", + target: "LoadingInitiativeInfo" + }, + { + target: "WaitingConfirmation" + } + ] }, LoadingInitiativeInfo: { tags: [LOADING_TAG], @@ -86,14 +91,19 @@ export const idPayUnsubscriptionMachine = setup({ onError: [ { guard: "isSessionExpired", - target: ".SessionExpired" + target: "SessionExpired" }, { - target: ".UnsubscriptionFailure" + target: "UnsubscriptionFailure" } ], onDone: { - target: ".WaitingConfirmation" + actions: assign(({ event }) => ({ + initiativeId: event.output.initiativeId, + initiativeName: event.output.initiativeName, + initiativeType: event.output.initiativeRewardType + })), + target: "WaitingConfirmation" } } }, @@ -101,7 +111,10 @@ export const idPayUnsubscriptionMachine = setup({ tags: [WAITING_USER_INPUT_TAG], on: { "confirm-unsubscription": { - target: ".Unsubscribing" + target: "Unsubscribing" + }, + exit: { + actions: "exitUnsubscription" } } }, @@ -113,14 +126,14 @@ export const idPayUnsubscriptionMachine = setup({ onError: [ { guard: "isSessionExpired", - target: ".SessionExpired" + target: "SessionExpired" }, { - target: ".UnsubscriptionFailure" + target: "UnsubscriptionFailure" } ], onDone: { - target: ".UnsubscriptionSuccess" + target: "UnsubscriptionSuccess" } } }, diff --git a/ts/features/idpay/unsubscription/machine/provider.tsx b/ts/features/idpay/unsubscription/machine/provider.tsx index 7630c14ed03..55312099941 100644 --- a/ts/features/idpay/unsubscription/machine/provider.tsx +++ b/ts/features/idpay/unsubscription/machine/provider.tsx @@ -1,4 +1,4 @@ -import { createActorContext } from "@xstate/react5"; +import { createActorContext } from "@xstate/react"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; @@ -33,7 +33,12 @@ type Props = { initiativeType?: InitiativeRewardTypeEnum; }; -export const IDPayUnsubscriptionMachineProvider = ({ children }: Props) => { +export const IDPayUnsubscriptionMachineProvider = ({ + children, + initiativeId, + initiativeName, + initiativeType +}: Props) => { const navigation = useIONavigation(); const dispatch = useIODispatch(); @@ -66,7 +71,10 @@ export const IDPayUnsubscriptionMachineProvider = ({ children }: Props) => { }); return ( - + {children} ); diff --git a/ts/features/idpay/unsubscription/machine/selectors.ts b/ts/features/idpay/unsubscription/machine/selectors.ts index 030309434cc..3f0fb1e792c 100644 --- a/ts/features/idpay/unsubscription/machine/selectors.ts +++ b/ts/features/idpay/unsubscription/machine/selectors.ts @@ -1,7 +1,7 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; -import { StateFrom } from "xstate5"; +import { StateFrom } from "xstate"; import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import I18n from "../../../../i18n"; import { LOADING_TAG } from "../../../../xstate/utils"; diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx index be57bde0b43..1738f85a1d1 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx @@ -27,6 +27,7 @@ import { const UnsubscriptionConfirmationScreen = () => { const { useActorRef, useSelector } = IdPayUnsubscriptionMachineContext; + const machine = useActorRef(); const isLoading = useSelector(isLoadingSelector); diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx index 1b98dacfead..9b1ea82de82 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx @@ -41,7 +41,7 @@ const UnsubscriptionResultScreen = () => { buttonLabel: I18n.t("idpay.unsubscription.success.button") }; - const handleButtonPress = () => machine.send({ type: "EXIT" }); + const handleButtonPress = () => machine.send({ type: "exit" }); return ( diff --git a/yarn.lock b/yarn.lock index cbe07922d33..1eb3290866a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4545,7 +4545,7 @@ resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.7.1.tgz#157d5083db3f116b7ae28b5b3aef8f457f052491" integrity sha512-dQEt6enmHXtD93vDcMefhb5bh1zh0mLCRT8CvYJjCpTjaTth7sXqlU6ri1qP0HDR6IbU9s2/WVNw7Oy7O/Sqfg== -"@xstate/react5@npm:@xstate/react@4.1.1": +"@xstate/react@npm:@xstate/react@4": version "4.1.1" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa" integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA== @@ -4553,14 +4553,6 @@ use-isomorphic-layout-effect "^1.1.2" use-sync-external-store "^1.2.0" -"@xstate/react@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095" - integrity sha512-/tq/gg92P9ke8J+yDNDBv5/PAxBvXJf2cYyGDByzgtl5wKaxKxzDT82Gj3eWlCJXkrBg4J5/V47//gRJuVH2fA== - dependencies: - use-isomorphic-layout-effect "^1.0.0" - use-sync-external-store "^1.0.0" - "@xstate/tools-shared@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@xstate/tools-shared/-/tools-shared-1.2.3.tgz#a2e19119a7a273bbbdd35adaf6ea52cb80add064" @@ -4568,6 +4560,14 @@ dependencies: "@xstate/machine-extractor" "0.7.1" +"@xstate4/react@npm:@xstate/react@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095" + integrity sha512-/tq/gg92P9ke8J+yDNDBv5/PAxBvXJf2cYyGDByzgtl5wKaxKxzDT82Gj3eWlCJXkrBg4J5/V47//gRJuVH2fA== + dependencies: + use-isomorphic-layout-effect "^1.0.0" + use-sync-external-store "^1.0.0" + "@yarnpkg/lockfile@^1.0.0", "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -17363,16 +17363,21 @@ xss@1.0.10: commander "^2.20.3" cssfilter "0.0.10" -"xstate5@npm:xstate@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.11.0.tgz#2155f750d9ff6c4d24b3a9aee620e7e6661dae0c" - integrity sha512-0MqTLpc7dr/hXFHY25oN4sdnO3Ey6MYy9WkWxOgiwjPV0S6rWwLb5nZlRlPDSku2GEV4/y6AR8bX+GNCOxnEwA== +"xstate4@npm:xstate@4.33.6": + version "4.33.6" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.33.6.tgz#9e23f78879af106f1de853aba7acb2bc3b1eb950" + integrity sha512-A5R4fsVKADWogK2a43ssu8Fz1AF077SfrKP1ZNyDBD8lNa/l4zfR//Luofp5GSWehOQr36Jp0k2z7b+sH2ivyg== -xstate@^4.29.0, xstate@^4.33.6: +xstate@^4.29.0: version "4.35.4" resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.35.4.tgz#87b2a45b6c7e84d820f56378408c6531ca5c4662" integrity sha512-mqRBYHhljP1xIItI4xnSQNHEv6CKslSM1cOGmvhmxeoDPAZgNbhSUYAL5N6DZIxRfpYY+M+bSm3mUFHD63iuvg== +xstate@^5: + version "5.11.0" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.11.0.tgz#2155f750d9ff6c4d24b3a9aee620e7e6661dae0c" + integrity sha512-0MqTLpc7dr/hXFHY25oN4sdnO3Ey6MYy9WkWxOgiwjPV0S6rWwLb5nZlRlPDSku2GEV4/y6AR8bX+GNCOxnEwA== + xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From 8f35ddacad2be27d8ec221a82d24a25964e15e93 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 29 Apr 2024 09:22:06 +0200 Subject: [PATCH 04/31] chore: onboarding wip --- .../idpay/common/navigation/linking.ts | 6 +- .../components/BeneficiaryDetailsContent.tsx | 13 +- .../idpay/onboarding/machine/actions.ts | 93 ++++ .../{xstate/services.ts => machine/actors.ts} | 185 +++---- .../idpay/onboarding/machine/context.ts | 28 + .../idpay/onboarding/machine/events.ts | 38 ++ ts/features/idpay/onboarding/machine/input.ts | 8 + .../idpay/onboarding/machine/machine.ts | 367 +++++++++++++ .../idpay/onboarding/machine/provider.tsx | 73 +++ .../{xstate => machine}/selectors.ts | 48 +- .../idpay/onboarding/navigation/navigator.tsx | 127 ++--- .../idpay/onboarding/navigation/params.ts | 15 + .../idpay/onboarding/navigation/routes.ts | 10 + .../screens/BoolValuePrerequisitesScreen.tsx | 4 +- .../onboarding/screens/CompletionScreen.tsx | 4 +- .../onboarding/screens/FailureScreen.tsx | 4 +- .../screens/InitiativeDetailsScreen.tsx | 14 +- .../screens/MultiValuePrerequisitesScreen.tsx | 4 +- .../screens/PDNDPrerequisitesScreen.tsx | 4 +- .../onboarding/xstate/__mocks__/actions.ts | 11 - .../onboarding/xstate/__mocks__/services.ts | 7 - .../xstate/__tests__/actions.test.ts | 215 -------- .../xstate/__tests__/machine.test.ts | 377 ------------- .../xstate/__tests__/services.test.ts | 509 ------------------ .../idpay/onboarding/xstate/actions.ts | 110 ---- ts/features/idpay/onboarding/xstate/events.ts | 50 -- .../idpay/onboarding/xstate/machine.ts | 497 ----------------- .../idpay/onboarding/xstate/provider.tsx | 102 ---- .../machine/__tests__/machine.test.ts | 202 ------- .../machine/__tests__/services.test.ts | 122 ----- .../idpay/unsubscription/machine/machine.ts | 26 +- .../idpay/unsubscription/machine/provider.tsx | 30 +- .../unsubscription/navigation/navigator.tsx | 50 +- .../idpay/unsubscription/navigation/params.ts | 14 + .../idpay/unsubscription/navigation/routes.ts | 5 + ts/navigation/AuthenticatedStackNavigator.tsx | 4 +- ts/navigation/params/AppParamsList.ts | 25 +- ts/xstate/helpers/guardedNavigationAction.ts | 4 +- 38 files changed, 887 insertions(+), 2518 deletions(-) create mode 100644 ts/features/idpay/onboarding/machine/actions.ts rename ts/features/idpay/onboarding/{xstate/services.ts => machine/actors.ts} (65%) create mode 100644 ts/features/idpay/onboarding/machine/context.ts create mode 100644 ts/features/idpay/onboarding/machine/events.ts create mode 100644 ts/features/idpay/onboarding/machine/input.ts create mode 100644 ts/features/idpay/onboarding/machine/machine.ts create mode 100644 ts/features/idpay/onboarding/machine/provider.tsx rename ts/features/idpay/onboarding/{xstate => machine}/selectors.ts (77%) create mode 100644 ts/features/idpay/onboarding/navigation/params.ts create mode 100644 ts/features/idpay/onboarding/navigation/routes.ts delete mode 100644 ts/features/idpay/onboarding/xstate/__mocks__/actions.ts delete mode 100644 ts/features/idpay/onboarding/xstate/__mocks__/services.ts delete mode 100644 ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts delete mode 100644 ts/features/idpay/onboarding/xstate/__tests__/machine.test.ts delete mode 100644 ts/features/idpay/onboarding/xstate/__tests__/services.test.ts delete mode 100644 ts/features/idpay/onboarding/xstate/actions.ts delete mode 100644 ts/features/idpay/onboarding/xstate/events.ts delete mode 100644 ts/features/idpay/onboarding/xstate/machine.ts delete mode 100644 ts/features/idpay/onboarding/xstate/provider.tsx delete mode 100644 ts/features/idpay/unsubscription/machine/__tests__/machine.test.ts delete mode 100644 ts/features/idpay/unsubscription/machine/__tests__/services.test.ts create mode 100644 ts/features/idpay/unsubscription/navigation/params.ts create mode 100644 ts/features/idpay/unsubscription/navigation/routes.ts diff --git a/ts/features/idpay/common/navigation/linking.ts b/ts/features/idpay/common/navigation/linking.ts index ebb1aae9e16..1e163de482c 100644 --- a/ts/features/idpay/common/navigation/linking.ts +++ b/ts/features/idpay/common/navigation/linking.ts @@ -1,20 +1,20 @@ import { PathConfigMap } from "@react-navigation/native"; import { IDPayDetailsRoutes } from "../../details/navigation"; -import { IDPayOnboardingRoutes } from "../../onboarding/navigation/navigator"; import { IDPayPaymentRoutes } from "../../payment/navigation/navigator"; import { AppParamsList } from "../../../../navigation/params/AppParamsList"; +import { IdPayOnboardingRoutes } from "../../onboarding/navigation/routes"; export const idPayLinkingOptions: PathConfigMap = { /** * IDPay initiative onboarding */ - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN]: { + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: { path: "idpay/onboarding", screens: { /** * Handles ioit://idpay/onboarding/{initiativeId} */ - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: "/:serviceId" + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: "/:serviceId" } }, /** diff --git a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx index 133ab7e90ca..f4cdf6305a2 100644 --- a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx +++ b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx @@ -25,10 +25,10 @@ import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { format } from "../../../../utils/dates"; +import { SERVICES_ROUTES } from "../../../services/common/navigation/routes"; import { Table, TableRow } from "../../common/components/Table"; import { formatNumberCurrencyOrDefault } from "../../common/utils/strings"; -import { IDPayUnsubscriptionRoutes } from "../../unsubscription/navigation/navigator"; -import { SERVICES_ROUTES } from "../../../services/common/navigation/routes"; +import { IdPayUnsubscriptionRoutes } from "../../unsubscription/navigation/routes"; import { InitiativeRulesInfoBox, InitiativeRulesInfoBoxSkeleton @@ -184,11 +184,10 @@ const BeneficiaryDetailsContent = (props: BeneficiaryDetailsProps) => { ); const handleUnsubscribePress = () => - navigation.navigate(IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN, { - initiativeId, - initiativeName, - initiativeType - }); + navigation.navigate( + IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR, + { initiativeId, initiativeName, initiativeType } + ); return ( <> diff --git a/ts/features/idpay/onboarding/machine/actions.ts b/ts/features/idpay/onboarding/machine/actions.ts new file mode 100644 index 00000000000..b9d36578db5 --- /dev/null +++ b/ts/features/idpay/onboarding/machine/actions.ts @@ -0,0 +1,93 @@ +import * as O from "fp-ts/lib/Option"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch } from "../../../../store/hooks"; +import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; +import { IDPayDetailsRoutes } from "../../details/navigation"; +import { IdPayOnboardingRoutes } from "../navigation/routes"; +import * as Context from "./context"; + +const createActionsImplementation = ( + navigation: ReturnType, + dispatch: ReturnType +) => { + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + + const navigateToInitiativeDetailsScreen = guardedNavigationAction(() => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS + }) + ); + + const navigateToPDNDCriteriaScreen = guardedNavigationAction(() => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE + }) + ); + + const navigateToBoolSelfDeclarationsScreen = guardedNavigationAction(() => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS + }) + ); + + const navigateToMultiSelfDeclarationsScreen = guardedNavigationAction( + (context: Context.Context) => + navigation.navigate({ + name: IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, + params: { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS + }, + key: String(context.multiConsentsPage) + }) + ); + + const navigateToCompletionScreen = () => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION + }); + + const navigateToFailureScreen = () => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE + }); + + const navigateToInitiativeMonitoringScreen = (context: Context.Context) => { + if (O.isNone(context.initiative)) { + throw new Error("Initiative is undefined"); + } + + navigation.replace(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { + screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, + params: { + initiativeId: context.initiative.value.initiativeId + } + }); + }; + + const closeOnboarding = () => { + navigation.popToTop(); + }; + + return { + handleSessionExpired, + navigateToInitiativeDetailsScreen, + navigateToPDNDCriteriaScreen, + navigateToBoolSelfDeclarationsScreen, + navigateToMultiSelfDeclarationsScreen, + navigateToCompletionScreen, + navigateToFailureScreen, + navigateToInitiativeMonitoringScreen, + closeOnboarding + }; +}; + +export { createActionsImplementation }; diff --git a/ts/features/idpay/onboarding/xstate/services.ts b/ts/features/idpay/onboarding/machine/actors.ts similarity index 65% rename from ts/features/idpay/onboarding/xstate/services.ts rename to ts/features/idpay/onboarding/machine/actors.ts index a37eb18345b..b2a3eb8f615 100644 --- a/ts/features/idpay/onboarding/xstate/services.ts +++ b/ts/features/idpay/onboarding/machine/actors.ts @@ -2,6 +2,7 @@ import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; +import { fromPromise } from "xstate"; import { PreferredLanguage } from "../../../../../definitions/backend/PreferredLanguage"; import { InitiativeDataDTO } from "../../../../../definitions/idpay/InitiativeDataDTO"; import { CodeEnum as OnboardingErrorCodeEnum } from "../../../../../definitions/idpay/OnboardingErrorDTO"; @@ -13,7 +14,7 @@ import { OnboardingFailure, OnboardingFailureEnum } from "../types/OnboardingFailure"; -import { Context } from "./machine"; +import * as Context from "./context"; import { getBoolRequiredCriteriaFromContext } from "./selectors"; /** @@ -69,7 +70,7 @@ const mapErrorCodeToFailure = ( } }; -const createServicesImplementation = ( +const createActorsImplementation = ( client: IDPayClient, token: string, language: PreferredLanguage @@ -79,44 +80,45 @@ const createServicesImplementation = ( "Accept-Language": language }; - const loadInitiative = async (context: Context) => { - if (context.serviceId === undefined) { - return Promise.reject(OnboardingFailureEnum.GENERIC); - } - - const dataResponse = await client.getInitiativeData({ - ...clientOptions, - serviceId: context.serviceId - }); + const getInitiativeInfo = fromPromise( + async params => { + const dataResponse = await client.getInitiativeData({ + ...clientOptions, + serviceId: params.input + }); - const data: Promise = pipe( - dataResponse, - E.fold( - _ => Promise.reject(OnboardingFailureEnum.GENERIC), - ({ status, value }) => { - switch (status) { - case 200: - return Promise.resolve(value); - case 401: - return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); - default: - return Promise.reject(OnboardingFailureEnum.GENERIC); + const data: Promise = pipe( + dataResponse, + E.fold( + _ => Promise.reject(OnboardingFailureEnum.GENERIC), + ({ status, value }) => { + switch (status) { + case 200: + return Promise.resolve(value); + case 401: + return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); + default: + return Promise.reject(OnboardingFailureEnum.GENERIC); + } } - } - ) - ); + ) + ); - return data; - }; + return data; + } + ); - const loadInitiativeStatus = async (context: Context) => { - if (context.initiative === undefined) { - return Promise.reject(OnboardingFailureEnum.GENERIC); + const getOnboardingStatus = fromPromise< + O.Option, + O.Option + >(async params => { + if (O.isNone(params.input)) { + throw new Error("Initiative ID was not provided"); } const statusResponse = await client.onboardingStatus({ ...clientOptions, - initiativeId: context.initiative.initiativeId + initiativeId: params.input.value }); const data: Promise> = pipe( @@ -148,17 +150,17 @@ const createServicesImplementation = ( ); return data; - }; + }); - const acceptTos = async (context: Context) => { - if (context.initiative === undefined) { - return Promise.reject(OnboardingFailureEnum.GENERIC); + const acceptTos = fromPromise>(async params => { + if (O.isNone(params.input)) { + throw new Error("Initiative ID was not provided"); } const response = await client.onboardingCitizen({ ...clientOptions, body: { - initiativeId: context.initiative.initiativeId + initiativeId: params.input.value } }); @@ -182,17 +184,16 @@ const createServicesImplementation = ( ); return dataPromise; - }; - - const loadRequiredCriteria = async (context: Context) => { - if (context.initiative === undefined) { - return Promise.reject(OnboardingFailureEnum.GENERIC); - } + }); + const getRequiredCriteria = fromPromise< + O.Option, + string + >(async params => { const response = await client.checkPrerequisites({ ...clientOptions, body: { - initiativeId: context.initiative.initiativeId + initiativeId: params.input } }); @@ -218,63 +219,67 @@ const createServicesImplementation = ( ); return dataPromise; - }; + }); - const acceptRequiredCriteria = async (context: Context) => { - const { initiative, requiredCriteria, multiConsentsAnswers } = context; + const acceptRequiredCriteria = fromPromise( + async params => { + const { initiative, requiredCriteria, multiConsentsAnswers } = + params.input; - if (initiative === undefined || requiredCriteria === undefined) { - return Promise.reject(OnboardingFailureEnum.GENERIC); - } + if (O.isNone(initiative) || O.isNone(requiredCriteria)) { + return Promise.reject(OnboardingFailureEnum.GENERIC); + } - if (O.isNone(requiredCriteria)) { - return Promise.reject(OnboardingFailureEnum.GENERIC); - } + if (requiredCriteria === undefined) { + return Promise.reject(OnboardingFailureEnum.GENERIC); + } - const consentsArray = [ - ...getBoolRequiredCriteriaFromContext(context).map(_ => ({ - _type: _._type, - code: _.code, - accepted: true - })), - ...Object.values(multiConsentsAnswers) - ] as Array; + const consentsArray = [ + ...getBoolRequiredCriteriaFromContext(params.input).map(_ => ({ + _type: _._type, + code: _.code, + accepted: true + })), + ...Object.values(multiConsentsAnswers) + ] as Array; - const response = await client.consentOnboarding({ - ...clientOptions, - body: { - initiativeId: initiative.initiativeId, - pdndAccept: true, - selfDeclarationList: consentsArray - } - }); + const response = await client.consentOnboarding({ + ...clientOptions, + body: { + initiativeId: initiative.value.initiativeId, + pdndAccept: true, + selfDeclarationList: consentsArray + } + }); - const dataPromise: Promise = pipe( - response, - E.fold( - _ => Promise.reject(OnboardingFailureEnum.GENERIC), - ({ status }) => { - switch (status) { - case 202: - return Promise.resolve(undefined); - case 401: - return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); - default: - return Promise.reject(OnboardingFailureEnum.GENERIC); + const dataPromise: Promise = pipe( + response, + E.fold( + _ => Promise.reject(OnboardingFailureEnum.GENERIC), + ({ status }) => { + switch (status) { + case 202: + return Promise.resolve(undefined); + case 401: + return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); + default: + return Promise.reject(OnboardingFailureEnum.GENERIC); + } } - } - ) - ); + ) + ); + + return dataPromise; + } + ); - return dataPromise; - }; return { - loadInitiative, - loadInitiativeStatus, + getInitiativeInfo, + getOnboardingStatus, acceptTos, - loadRequiredCriteria, + getRequiredCriteria, acceptRequiredCriteria }; }; -export { createServicesImplementation }; +export { createActorsImplementation }; diff --git a/ts/features/idpay/onboarding/machine/context.ts b/ts/features/idpay/onboarding/machine/context.ts new file mode 100644 index 00000000000..3c007805ad7 --- /dev/null +++ b/ts/features/idpay/onboarding/machine/context.ts @@ -0,0 +1,28 @@ +import * as O from "fp-ts/lib/Option"; +import { InitiativeDataDTO } from "../../../../../definitions/idpay/InitiativeDataDTO"; +import { StatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; +import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; +import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; +import { OnboardingFailure } from "../types/OnboardingFailure"; + +export interface Context { + readonly serviceId: string; + readonly initiative: O.Option; + readonly onboardingStatus: O.Option; + readonly requiredCriteria: O.Option; + readonly selfDeclarationsMultiPage: number; + readonly selfDeclarationsMultiAnwsers: Record; + readonly selfDeclarationsBoolAnswers: Record; + readonly failure: O.Option; +} + +export const Context: Context = { + serviceId: "", + initiative: O.none, + onboardingStatus: O.none, + requiredCriteria: O.none, + selfDeclarationsMultiPage: 0, + selfDeclarationsMultiAnwsers: {}, + selfDeclarationsBoolAnswers: {}, + failure: O.none +}; diff --git a/ts/features/idpay/onboarding/machine/events.ts b/ts/features/idpay/onboarding/machine/events.ts new file mode 100644 index 00000000000..49f7df1df7c --- /dev/null +++ b/ts/features/idpay/onboarding/machine/events.ts @@ -0,0 +1,38 @@ +import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; +import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; +import * as Input from "./input"; + +export interface AutoInit { + readonly type: "xstate.init"; + readonly input: Input.Input; +} + +export interface ToggleBoolCriteria { + readonly type: "toggle-bool-criteria"; + readonly criteria: SelfDeclarationBoolDTO; +} + +export interface SelectMultiConsent { + readonly type: "select-multi-consent"; + readonly data: SelfConsentMultiDTO; +} + +export interface Next { + readonly type: "next"; +} + +export interface Back { + readonly type: "back"; +} + +export interface Close { + readonly type: "close"; +} + +export type Events = + | AutoInit + | SelectMultiConsent + | ToggleBoolCriteria + | Back + | Next + | Close; diff --git a/ts/features/idpay/onboarding/machine/input.ts b/ts/features/idpay/onboarding/machine/input.ts new file mode 100644 index 00000000000..bc66c18a570 --- /dev/null +++ b/ts/features/idpay/onboarding/machine/input.ts @@ -0,0 +1,8 @@ +import * as Context from "./context"; + +export interface Input { + readonly serviceId: string; +} + +export const Input = (input: Input): Promise => + Promise.resolve({ ...Context.Context, ...input }); diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts new file mode 100644 index 00000000000..9722fb00309 --- /dev/null +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -0,0 +1,367 @@ +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { assertEvent, assign, fromPromise, setup } from "xstate"; +import { InitiativeDataDTO } from "../../../../../definitions/idpay/InitiativeDataDTO"; +import { StatusEnum as OnboardingStatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; +import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; +import { + LOADING_TAG, + WAITING_USER_INPUT_TAG, + notImplementedStub +} from "../../../../xstate/utils"; +import * as Context from "./context"; +import * as Events from "./events"; +import * as Input from "./input"; +import { + getBooleanSelfDeclarationListFromContext, + getMultiSelfDeclarationListFromContext +} from "./selectors"; + +export const idPayOnboardingMachine = setup({ + types: { + input: {} as Input.Input, + context: {} as Context.Context, + events: {} as Events.Events + }, + actions: { + navigateToInitiativeDetailsScreen: notImplementedStub, + navigateToPdndCriteriaScreen: notImplementedStub, + navigateToBoolSelfDeclarationListScreen: notImplementedStub, + navigateToMultiSelfDeclarationListScreen: notImplementedStub, + navigateToCompletionScreen: notImplementedStub, + navigateToFailureScreen: notImplementedStub, + navigateToInitiativeMonitoringScreen: notImplementedStub, + handleSessionExpired: notImplementedStub, + closeOnboarding: notImplementedStub + }, + actors: { + onInit: fromPromise(({ input }) => + Input.Input(input) + ), + getInitiativeInfo: fromPromise( + notImplementedStub + ), + getOnboardingStatus: fromPromise< + O.Option, + O.Option + >(notImplementedStub), + acceptTos: fromPromise>(notImplementedStub), + getRequiredCriteria: fromPromise< + O.Option, + O.Option + >(notImplementedStub), + acceptRequiredCriteria: fromPromise( + notImplementedStub + ) + }, + guards: { + isSessionExpired: () => false, + hasPdndCriteria: ({ context }) => + pipe( + context.requiredCriteria, + O.map(({ pdndCriteria }) => pdndCriteria.length > 0), + O.getOrElse(() => false) + ), + hasSelfDecalrationList: ({ context }) => + pipe( + context.requiredCriteria, + O.map(({ selfDeclarationList }) => selfDeclarationList.length > 0), + O.getOrElse(() => false) + ), + hasBooleanSelfDeclarationList: ({ context }) => + getBooleanSelfDeclarationListFromContext(context).length > 0, + hasMultiSelfDeclarationList: ({ context }) => + getMultiSelfDeclarationListFromContext(context).length > 0, + isLastMultiConsent: ({ context }) => + context.selfDeclarationsMultiPage >= + getMultiSelfDeclarationListFromContext(context).length - 1 + } +}).createMachine({ + id: "idpay-onboarding", + context: Context.Context, + invoke: { + src: "onInit", + input: ({ event }) => { + assertEvent(event, "xstate.init"); + return event.input; + }, + onError: { + target: ".OnboardingFailure" + }, + onDone: { + actions: assign(event => ({ ...event.event.output })), + target: ".LoadingInitiativeInfo" + } + }, + initial: "LoadingInitiative", + on: { + close: { + actions: "closeOnboarding" + } + }, + states: { + LoadingInitiative: { + tags: [LOADING_TAG], + entry: "navigateToInitiativeDetailsScreen", + states: { + LoadingInitiativeInfo: { + invoke: { + src: "getInitiativeInfo", + input: ({ context }) => context.serviceId, + onDone: { + actions: assign(({ event }) => ({ + initiative: O.some(event.output) + })), + target: ".LoadingInitiative.LoadingOnboardingStatus" + } + } + }, + + LoadingOnboardingStatus: { + invoke: { + src: "getOnboardingStatus", + input: ({ context }) => selectInitiativeId(context), + onDone: { + actions: assign(({ event }) => ({ + onboardingStatus: event.output + })) + } + } + } + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + target: "OnboardingFailure" + } + ], + onDone: { + target: "DisplayingInitiativeInfo" + } + }, + + DisplayingInitiativeInfo: { + tags: [WAITING_USER_INPUT_TAG], + on: { + next: { + target: "AcceptingTos" + } + } + }, + + AcceptingTos: { + tags: [LOADING_TAG], + invoke: { + src: "acceptTos", + input: ({ context }) => selectInitiativeId(context), + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + target: "OnboardingFailure" + } + ], + onDone: { + target: "LoadingCriteria" + } + } + }, + + LoadingCriteria: { + tags: [LOADING_TAG], + invoke: { + src: "getRequiredCriteria", + input: ({ context }) => selectInitiativeId(context), + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + target: "OnboardingFailure" + } + ], + onDone: { + actions: assign(({ event }) => ({ + requiredCriteria: event.output + })), + target: "EvaluatingRequiredCriteria" + } + } + }, + + EvaluatingRequiredCriteria: { + always: [ + { + guard: "hasPdndCriteria", + target: "DisplayingPdndCriteria" + }, + { + guard: "hasSelfDecalrationList", + target: "DisplayingSelfDeclarationList" + }, + { + target: "OnboardingCompleted" + } + ] + }, + + DisplayingPdndCriteria: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToPdndCriteriaScreen", + on: { + next: [ + { + guard: "hasSelfDecalrationList", + target: "DisplayingSelfDeclarationList" + }, + { + target: "AcceptingCriteria" + } + ], + back: { + target: "DisplayingInitiativeInfo" + } + } + }, + + DisplayingSelfDeclarationList: { + tags: [WAITING_USER_INPUT_TAG], + states: { + Evaluating: { + tags: [LOADING_TAG], + always: [ + { + guard: "hasBooleanSelfDeclarationList", + target: "DisplayingBooleanSelfDeclarationList" + }, + { + guard: "hasMultiSelfDeclarationList", + target: "DisplayingMultiSelfDeclarationList" + } + ] + }, + + DisplayingBooleanSelfDeclarationList: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToBoolSelfDeclarationListScreen", + on: { + back: [ + { + guard: "hasPdndCriteria", + target: "DisplayingPdndCriteria" + }, + { + target: "DisplayingInitiativeInfo" + } + ], + "toggle-bool-criteria": { + actions: assign(({ context, event }) => ({ + selfDeclarationsBoolAnswers: { + ...context.selfDeclarationsBoolAnswers, + [event.criteria.code]: event.criteria.value + } + })) + }, + next: [ + { + guard: "hasMultiSelfDeclarationList", + target: "DisplayingMultiSelfDeclarationList" + }, + { + target: "AcceptingCriteria" + } + ] + } + }, + + DisplayingMultiSelfDeclarationList: { + tags: [WAITING_USER_INPUT_TAG], + states: { + DisplayingMultiSelfDeclarationItem: { + entry: "navigateToMultiSelfDeclarationListScreen", + on: { + "select-multi-consent": [ + { + actions: assign(({ context, event }) => ({ + selfDeclarationsMultiAnwsers: { + ...context.selfDeclarationsMultiAnwsers, + [context.selfDeclarationsMultiPage]: event.data + } + })) + } + ] + } + }, + EvaluatingMultiSelfDeclarationList: { + always: [ + { + guard: "isLastMultiConsent", + target: "AcceptingCriteria" + }, + { + actions: assign(({ context }) => ({ + selfDeclarationsMultiPage: + +context.selfDeclarationsMultiPage + 1 + })), + target: "DisplayingMultiSelfDeclarationItem" + } + ] + } + } + } + } + }, + + AcceptingCriteria: { + tags: [LOADING_TAG], + invoke: { + src: "acceptRequiredCriteria", + input: ({ context }) => context, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + target: "OnboardingFailure" + } + ], + onDone: { + target: "OnboardingCompleted" + } + } + }, + + OnboardingCompleted: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToCompletionScreen" + }, + + OnboardingFailure: { + entry: "navigateToFailureScreen", + on: { + next: { + actions: "navigateToInitiativeMonitoringScreen" + } + } + }, + + SessionExpired: { + entry: ["handleSessionExpired", "closeOnboarding"] + } + } +}); + +const selectInitiativeId = (context: Context.Context) => + pipe( + context.initiative, + O.map(initiative => initiative.initiativeId) + ); + +export type IdPayOnboardingMachine = typeof idPayOnboardingMachine; diff --git a/ts/features/idpay/onboarding/machine/provider.tsx b/ts/features/idpay/onboarding/machine/provider.tsx new file mode 100644 index 00000000000..d4e03255b9a --- /dev/null +++ b/ts/features/idpay/onboarding/machine/provider.tsx @@ -0,0 +1,73 @@ +import { createActorContext } from "@xstate/react"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import React from "react"; +import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; +import { + idPayApiBaseUrl, + idPayApiUatBaseUrl, + idPayTestToken +} from "../../../../config"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { sessionInfoSelector } from "../../../../store/reducers/authentication"; +import { + isPagoPATestEnabledSelector, + preferredLanguageSelector +} from "../../../../store/reducers/persistedPreferences"; +import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; +import { createIDPayClient } from "../../common/api/client"; +import { createActionsImplementation } from "./actions"; +import { createActorsImplementation } from "./actors"; +import * as Input from "./input"; +import { idPayOnboardingMachine } from "./machine"; + +type Props = { + children: React.ReactNode; + input: Input.Input; +}; + +export const IdPayOnboardingMachineContext = createActorContext( + idPayOnboardingMachine +); + +export const IdPayOnboardingMachineProvider = ({ children, input }: Props) => { + const dispatch = useIODispatch(); + const rootNavigation = useIONavigation(); + + const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); + const preferredLanguageOption = useIOSelector(preferredLanguageSelector); + + const language = pipe( + preferredLanguageOption, + O.map(fromLocaleToPreferredLanguage), + O.getOrElse(() => PreferredLanguageEnum.it_IT) + ); + + const sessionInfo = useIOSelector(sessionInfoSelector); + + if (O.isNone(sessionInfo)) { + throw new Error("Session info is undefined"); + } + + const { bpdToken } = sessionInfo.value; + + const token = idPayTestToken !== undefined ? idPayTestToken : bpdToken; + const client = createIDPayClient( + isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl + ); + + const actors = createActorsImplementation(client, token, language); + const actions = createActionsImplementation(rootNavigation, dispatch); + + const machine = idPayOnboardingMachine.provide({ + actors, + actions + }); + + return ( + + {children} + + ); +}; diff --git a/ts/features/idpay/onboarding/xstate/selectors.ts b/ts/features/idpay/onboarding/machine/selectors.ts similarity index 77% rename from ts/features/idpay/onboarding/xstate/selectors.ts rename to ts/features/idpay/onboarding/machine/selectors.ts index 69d7692eaf8..bbe13c6a7a9 100644 --- a/ts/features/idpay/onboarding/xstate/selectors.ts +++ b/ts/features/idpay/onboarding/machine/selectors.ts @@ -8,12 +8,13 @@ import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDec import { SelfDeclarationDTO } from "../../../../../definitions/idpay/SelfDeclarationDTO"; import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDeclarationMultiDTO"; import { LOADING_TAG, UPSERTING_TAG } from "../../../../xstate/utils"; -import { Context, IDPayOnboardingMachineType } from "./machine"; +import { IdPayOnboardingMachine } from "./machine"; +import * as Context from "./context"; -type StateWithContext = StateFrom; +type StateWithContext = StateFrom; const selectInitiativeStatus = (state: StateWithContext) => - state.context.initiativeStatus; + state.context.onboardingStatus; const selectOnboardingFailure = (state: StateWithContext) => state.context.failure; @@ -38,13 +39,11 @@ export const selectInitiative = (state: StateWithContext) => const selectServiceId = (state: StateWithContext) => state.context.serviceId; const filterCriteria = ( - criteria: O.Option | undefined, + criteria: O.Option, filterFunc: typeof SelfDeclarationMultiDTO | typeof SelfDeclarationBoolDTO ) => pipe( criteria, - O.fromNullable, - O.flatten, O.fold( () => [], some => some.selfDeclarationList.filter(filterFunc.is) @@ -80,8 +79,6 @@ const pdndCriteriaSelector = createSelector( requiredCriteria => pipe( requiredCriteria, - O.fromNullable, - O.flatten, O.fold( () => [], some => some.pdndCriteria @@ -106,18 +103,25 @@ const isUpsertingSelector = createSelector(selectTags, tags => tags.has(UPSERTING_TAG) ); -const initiativeIDSelector = createSelector( - selectInitiative, - initiative => initiative?.initiativeId ?? undefined +const initiativeIDSelector = createSelector(selectInitiative, initiative => + pipe( + initiative, + O.map(initiative => initiative.initiativeId), + O.toUndefined + ) ); -const getMultiRequiredCriteriaFromContext = (context: Context) => +export const getMultiSelfDeclarationListFromContext = ( + context: Context.Context +) => filterCriteria( context.requiredCriteria, SelfDeclarationMultiDTO ); -const getBoolRequiredCriteriaFromContext = (context: Context) => +export const getBooleanSelfDeclarationListFromContext = ( + context: Context.Context +) => filterCriteria( context.requiredCriteria, SelfDeclarationBoolDTO @@ -130,21 +134,3 @@ const areAllSelfDeclarationsToggledSelector = createSelector( boolSelfDeclarations.length === Object.values(answers).filter(answer => answer).length ); - -export { - selectServiceId, - selectInitiativeStatus, - selectOnboardingFailure, - isUpsertingSelector, - multiRequiredCriteriaSelector, - boolRequiredCriteriaSelector, - getMultiRequiredCriteriaFromContext, - getBoolRequiredCriteriaFromContext, - criteriaToDisplaySelector, - pdndCriteriaSelector, - prerequisiteAnswerIndexSelector, - isLoadingSelector, - initiativeIDSelector, - selectSelfDeclarationBoolAnswers, - areAllSelfDeclarationsToggledSelector -}; diff --git a/ts/features/idpay/onboarding/navigation/navigator.tsx b/ts/features/idpay/onboarding/navigation/navigator.tsx index 40c72636c7b..bdf6a3559ff 100644 --- a/ts/features/idpay/onboarding/navigation/navigator.tsx +++ b/ts/features/idpay/onboarding/navigation/navigator.tsx @@ -1,88 +1,63 @@ -import { ParamListBase, RouteProp } from "@react-navigation/native"; -import { - createStackNavigator, - StackNavigationProp -} from "@react-navigation/stack"; +import { RouteProp, useRoute } from "@react-navigation/native"; +import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; import { isGestureEnabled } from "../../../../utils/navigation"; +import { IdPayOnboardingMachineProvider } from "../machine/provider"; import BoolValuePrerequisitesScreen from "../screens/BoolValuePrerequisitesScreen"; import CompletionScreen from "../screens/CompletionScreen"; import FailureScreen from "../screens/FailureScreen"; -import InitiativeDetailsScreen, { - InitiativeDetailsScreenRouteParams -} from "../screens/InitiativeDetailsScreen"; +import InitiativeDetailsScreen from "../screens/InitiativeDetailsScreen"; import MultiValuePrerequisitesScreen from "../screens/MultiValuePrerequisitesScreen"; import PDNDPrerequisitesScreen from "../screens/PDNDPrerequisitesScreen"; -import { IDPayOnboardingMachineProvider } from "../xstate/provider"; +import { IdPayOnboardingParamsList } from "./params"; +import { IdPayOnboardingRoutes } from "./routes"; -export const IDPayOnboardingRoutes = { - IDPAY_ONBOARDING_MAIN: "IDPAY_ONBOARDING_MAIN", - IDPAY_ONBOARDING_INITIATIVE_DETAILS: "IDPAY_ONBOARDING_INITIATIVE_DETAILS", - IDPAY_ONBOARDING_PDNDACCEPTANCE: "IDPAY_ONBOARDING_PDNDACCEPTANCE", - IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS: "IDPAY_ONBOARDING_SELF_DECLARATIONS", - IDPAY_ONBOARDING_COMPLETION: "IDPAY_ONBOARDING_COMPLETION", - IDPAY_ONBOARDING_FAILURE: "IDPAY_ONBOARDING_FAILURE", - IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS: - "IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS" -} as const; +const Stack = createStackNavigator(); -export type IDPayOnboardingParamsList = { - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: InitiativeDetailsScreenRouteParams; - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS]: undefined; - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE]: undefined; - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION]: undefined; - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE]: undefined; - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS]: undefined; -}; +type IdPayOnboardingRouteProps = RouteProp< + IdPayOnboardingParamsList, + "IDPAY_ONBOARDING_NAVIGATOR" +>; -const Stack = createStackNavigator(); +export const IdPayOnboardingNavigator = () => { + const { params } = useRoute(); + const { serviceId } = params; -export const IDPayOnboardingNavigator = () => ( - - - - - - - - - - -); -export type IDPayOnboardingStackNavigationRouteProps< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = { - navigation: IDPayOnboardingStackNavigationProp; - route: RouteProp; + return ( + + + + + + + + + + + ); }; - -export type IDPayOnboardingStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; diff --git a/ts/features/idpay/onboarding/navigation/params.ts b/ts/features/idpay/onboarding/navigation/params.ts new file mode 100644 index 00000000000..3fb0cfa452a --- /dev/null +++ b/ts/features/idpay/onboarding/navigation/params.ts @@ -0,0 +1,15 @@ +import { IdPayOnboardingRoutes } from "./routes"; + +export type IdPayOnboardingNavigatorParams = { + serviceId: string; +}; + +export type IdPayOnboardingParamsList = { + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: IdPayOnboardingNavigatorParams; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: undefined; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS]: undefined; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE]: undefined; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION]: undefined; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE]: undefined; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS]: undefined; +}; diff --git a/ts/features/idpay/onboarding/navigation/routes.ts b/ts/features/idpay/onboarding/navigation/routes.ts new file mode 100644 index 00000000000..e66411a932f --- /dev/null +++ b/ts/features/idpay/onboarding/navigation/routes.ts @@ -0,0 +1,10 @@ +export const IdPayOnboardingRoutes = { + IDPAY_ONBOARDING_NAVIGATOR: "IDPAY_ONBOARDING_NAVIGATOR", + IDPAY_ONBOARDING_INITIATIVE_DETAILS: "IDPAY_ONBOARDING_INITIATIVE_DETAILS", + IDPAY_ONBOARDING_PDNDACCEPTANCE: "IDPAY_ONBOARDING_PDNDACCEPTANCE", + IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS: "IDPAY_ONBOARDING_SELF_DECLARATIONS", + IDPAY_ONBOARDING_COMPLETION: "IDPAY_ONBOARDING_COMPLETION", + IDPAY_ONBOARDING_FAILURE: "IDPAY_ONBOARDING_FAILURE", + IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS: + "IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS" +} as const; diff --git a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx index 504a3a5b552..ad71b71ddb4 100644 --- a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx @@ -15,13 +15,13 @@ import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { useOnboardingMachineService } from "../xstate/provider"; +import { useOnboardingMachineService } from "../machine/provider"; import { areAllSelfDeclarationsToggledSelector, boolRequiredCriteriaSelector, isLoadingSelector, selectSelfDeclarationBoolAnswers -} from "../xstate/selectors"; +} from "../machine/selectors"; import { openWebUrl } from "../../../../utils/url"; import { dpr28Dec2000Url } from "../../../../urls"; diff --git a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx index 5b0433632da..d8485e23afd 100644 --- a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx +++ b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx @@ -9,8 +9,8 @@ import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay" import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import I18n from "../../../../i18n"; -import { useOnboardingMachineService } from "../xstate/provider"; -import { isUpsertingSelector } from "../xstate/selectors"; +import { useOnboardingMachineService } from "../machine/provider"; +import { isUpsertingSelector } from "../machine/selectors"; import themeVariables from "../../../../theme/variables"; const CompletionScreen = () => { diff --git a/ts/features/idpay/onboarding/screens/FailureScreen.tsx b/ts/features/idpay/onboarding/screens/FailureScreen.tsx index 5d46a80a7b0..cf8534252d7 100644 --- a/ts/features/idpay/onboarding/screens/FailureScreen.tsx +++ b/ts/features/idpay/onboarding/screens/FailureScreen.tsx @@ -7,8 +7,8 @@ import { OperationResultScreenContentProps } from "../../../../components/screens/OperationResultScreenContent"; import { OnboardingFailureEnum } from "../types/OnboardingFailure"; -import { useOnboardingMachineService } from "../xstate/provider"; -import { selectOnboardingFailure } from "../xstate/selectors"; +import { useOnboardingMachineService } from "../machine/provider"; +import { selectOnboardingFailure } from "../machine/selectors"; import I18n from "../../../../i18n"; const FailureScreen = () => { diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 0cf7ad2b32f..2b07237f5b1 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -18,21 +18,9 @@ import { } from "../components/OnboardingDescriptionMarkdown"; import { OnboardingPrivacyAdvice } from "../components/OnboardingPrivacyAdvice"; import { OnboardingServiceHeader } from "../components/OnboardingServiceHeader"; -import { IDPayOnboardingParamsList } from "../navigation/navigator"; -import { useOnboardingMachineService } from "../xstate/provider"; -import { isUpsertingSelector, selectInitiative } from "../xstate/selectors"; - -type InitiativeDetailsScreenRouteParams = { - serviceId: string; -}; - -type InitiativeDetailsRouteProps = RouteProp< - IDPayOnboardingParamsList, - "IDPAY_ONBOARDING_INITIATIVE_DETAILS" ->; +import { isUpsertingSelector, selectInitiative } from "../machine/selectors"; const InitiativeDetailsScreen = () => { - const route = useRoute(); const machine = useOnboardingMachineService(); const { serviceId } = route.params; diff --git a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx index 14247309ec7..c2c1db45620 100644 --- a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx @@ -16,11 +16,11 @@ import BaseScreenComponent from "../../../../components/screens/BaseScreenCompon import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; -import { useOnboardingMachineService } from "../xstate/provider"; +import { useOnboardingMachineService } from "../machine/provider"; import { criteriaToDisplaySelector, prerequisiteAnswerIndexSelector -} from "../xstate/selectors"; +} from "../machine/selectors"; import { Link } from "../../../../components/core/typography/Link"; type ListItemProps = { diff --git a/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx index 959fef79119..76ea6de9bed 100644 --- a/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx @@ -25,8 +25,8 @@ import { serviceByIdPotSelector } from "../../../services/details/store/reducers import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; import { getPDNDCriteriaDescription } from "../utils/strings"; -import { useOnboardingMachineService } from "../xstate/provider"; -import { pdndCriteriaSelector, selectServiceId } from "../xstate/selectors"; +import { useOnboardingMachineService } from "../machine/provider"; +import { pdndCriteriaSelector, selectServiceId } from "../machine/selectors"; const secondaryButtonProps = { block: true, diff --git a/ts/features/idpay/onboarding/xstate/__mocks__/actions.ts b/ts/features/idpay/onboarding/xstate/__mocks__/actions.ts deleted file mode 100644 index f0f1a86e176..00000000000 --- a/ts/features/idpay/onboarding/xstate/__mocks__/actions.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const mockActions = { - handleSessionExpired: jest.fn(), - navigateToInitiativeDetailsScreen: jest.fn(), - navigateToPDNDCriteriaScreen: jest.fn(), - navigateToBoolSelfDeclarationsScreen: jest.fn(), - navigateToMultiSelfDeclarationsScreen: jest.fn(), - navigateToCompletionScreen: jest.fn(), - navigateToFailureScreen: jest.fn(), - navigateToInitiativeMonitoringScreen: jest.fn(), - exitOnboarding: jest.fn() -}; diff --git a/ts/features/idpay/onboarding/xstate/__mocks__/services.ts b/ts/features/idpay/onboarding/xstate/__mocks__/services.ts deleted file mode 100644 index ee20d481217..00000000000 --- a/ts/features/idpay/onboarding/xstate/__mocks__/services.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const mockServices = { - loadInitiative: jest.fn(), - loadInitiativeStatus: jest.fn(), - acceptTos: jest.fn(), - loadRequiredCriteria: jest.fn(), - acceptRequiredCriteria: jest.fn() -}; diff --git a/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts b/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts deleted file mode 100644 index 40698389315..00000000000 --- a/ts/features/idpay/onboarding/xstate/__tests__/actions.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import * as O from "fp-ts/lib/Option"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../../navigation/params/AppParamsList"; -import { - IDPayOnboardingParamsList, - IDPayOnboardingRoutes, - IDPayOnboardingStackNavigationProp -} from "../../navigation/navigator"; -import { createActionsImplementation } from "../actions"; -import { Context } from "../machine"; -import { InitiativeDataDTO } from "../../../../../../definitions/idpay/InitiativeDataDTO"; -import { refreshSessionToken } from "../../../../fastLogin/store/actions/tokenRefreshActions"; - -const rootNavigation: Partial> = { - navigate: jest.fn(), - replace: jest.fn() -}; - -const onboardingNavigation: Partial< - IDPayOnboardingStackNavigationProp -> = { - navigate: jest.fn(), - pop: jest.fn() -}; - -const T_CONTEXT: Context = { - failure: O.none, - initiativeStatus: O.none, - multiConsentsAnswers: {}, - multiConsentsPage: 0, - selfDeclarationBoolAnswers: {} -}; - -const T_SERVICE_ID = "efg456"; - -const T_NO_EVENT = { type: "" }; -const T_BACK_EVENT = { type: "BACK", skipNavigation: true }; - -const dispatch = jest.fn(); - -const T_INITIATIVE_INFO_DTO: InitiativeDataDTO = { - initiativeId: "1234", - description: "", - initiativeName: "", - organizationId: "", - organizationName: "", - privacyLink: "", - tcLink: "", - logoURL: "" -}; - -describe("IDPay Onboarding machine actions", () => { - const actions = createActionsImplementation( - rootNavigation as IOStackNavigationProp, - onboardingNavigation as IDPayOnboardingStackNavigationProp< - IDPayOnboardingParamsList, - keyof IDPayOnboardingParamsList - >, - dispatch - ); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("handleSessionExpired", () => { - it("should call dispatch with sessionExpired", async () => { - actions.handleSessionExpired(); - expect(dispatch).toHaveBeenCalledWith( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }); - }); - - describe("navigateToInitiativeDetailsScreen", () => { - it("should throw error if serviceId is not provided in context", async () => { - expect(() => { - actions.navigateToInitiativeDetailsScreen(T_CONTEXT, { type: "" }); - }).toThrow("serviceId is undefined"); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - - it("should navigate to screen", async () => { - actions.navigateToInitiativeDetailsScreen( - { ...T_CONTEXT, serviceId: T_SERVICE_ID }, - T_NO_EVENT - ); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledWith( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, - { - serviceId: T_SERVICE_ID - } - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToInitiativeDetailsScreen( - { ...T_CONTEXT, serviceId: T_SERVICE_ID }, - T_BACK_EVENT - ); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToPDNDCriteriaScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToPDNDCriteriaScreen(T_CONTEXT, T_NO_EVENT); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledWith( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToPDNDCriteriaScreen(T_CONTEXT, T_BACK_EVENT); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToBoolSelfDeclarationsScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToBoolSelfDeclarationsScreen(T_CONTEXT, T_NO_EVENT); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledWith( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToBoolSelfDeclarationsScreen(T_CONTEXT, T_BACK_EVENT); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToMultiSelfDeclarationsScreen", () => { - it("should navigate to screen", async () => { - const T_PAGE = 7; - - actions.navigateToMultiSelfDeclarationsScreen( - { ...T_CONTEXT, multiConsentsPage: T_PAGE }, - T_NO_EVENT - ); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledWith({ - name: IDPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS, - key: String(T_PAGE) - }); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToMultiSelfDeclarationsScreen(T_CONTEXT, T_BACK_EVENT); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToCompletionScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToCompletionScreen(); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledWith( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION - ); - }); - }); - - describe("navigateToFailureScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToFailureScreen(); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledWith( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE - ); - }); - }); - - describe("navigateToInitiativeMonitoringScreen", () => { - it("should throw error if initiative is not provided in context", async () => { - expect(() => { - actions.navigateToInitiativeMonitoringScreen(T_CONTEXT); - }).toThrow("initiative is undefined"); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - - it("should navigate to screen", async () => { - actions.navigateToInitiativeMonitoringScreen({ - ...T_CONTEXT, - initiative: T_INITIATIVE_INFO_DTO - }); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("exitOnboarding", () => { - it("should navigate to screen", async () => { - actions.exitOnboarding(); - expect(rootNavigation.navigate).toHaveBeenCalledTimes(0); - expect(onboardingNavigation.pop).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/ts/features/idpay/onboarding/xstate/__tests__/machine.test.ts b/ts/features/idpay/onboarding/xstate/__tests__/machine.test.ts deleted file mode 100644 index f40cbb5740a..00000000000 --- a/ts/features/idpay/onboarding/xstate/__tests__/machine.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -/* eslint-disable sonarjs/no-identical-functions */ -/* eslint-disable functional/no-let */ -import * as O from "fp-ts/lib/Option"; -import { interpret } from "xstate"; -import { waitFor } from "@testing-library/react-native"; -import { createIDPayOnboardingMachine } from "../machine"; -import { RequiredCriteriaDTO } from "../../../../../../definitions/idpay/RequiredCriteriaDTO"; -import { - AuthorityEnum, - CodeEnum, - PDNDCriteriaDTO -} from "../../../../../../definitions/idpay/PDNDCriteriaDTO"; -import { - SelfDeclarationBoolDTO, - _typeEnum as SelfDeclarationBoolDTOType -} from "../../../../../../definitions/idpay/SelfDeclarationBoolDTO"; -import { - SelfDeclarationMultiDTO, - _typeEnum as SelfDeclarationMultiDTOType -} from "../../../../../../definitions/idpay/SelfDeclarationMultiDTO"; -import { SelfConsentMultiDTO } from "../../../../../../definitions/idpay/SelfConsentMultiDTO"; -import { OnboardingFailureEnum } from "../../types/OnboardingFailure"; -import { mockActions } from "../__mocks__/actions"; -import { mockServices } from "../__mocks__/services"; - -const T_SERVICE_ID = "T_SERVICE_ID"; -const T_INITIATIVE_ID = "T_INITIATIVE_ID"; - -const T_REQUIRED_PDND_CRITERIA: PDNDCriteriaDTO = { - code: CodeEnum.BIRTHDATE, - description: "T_DESCRIPTION", - authority: AuthorityEnum.AGID, - value: "T_VALUE" -}; - -const T_REQUIRED_SELF_CRITERIA_BOOL: SelfDeclarationBoolDTO = { - _type: SelfDeclarationBoolDTOType.boolean, - code: "T_CODE_SELF_BOOL", - description: "T_DESCRIPTION", - value: true -}; - -const T_SELF_CONSENT_MULTI: SelfConsentMultiDTO = { - _type: SelfDeclarationMultiDTOType.multi, - code: "T_CODE_SELF_MULTI", - value: "T_VALUE_1" -}; - -const T_REQUIRED_SELF_CRITERIA_MULTI: SelfDeclarationMultiDTO = { - _type: SelfDeclarationMultiDTOType.multi, - code: "T_CODE_SELF_MULTI", - description: "T_DESCRIPTION", - value: ["T_VALUE_1", "T_VALUE_2"] -}; - -const T_REQUIRED_CRITERIA: RequiredCriteriaDTO = { - pdndCriteria: [T_REQUIRED_PDND_CRITERIA], - selfDeclarationList: [ - T_REQUIRED_SELF_CRITERIA_BOOL, - T_REQUIRED_SELF_CRITERIA_MULTI - ] -}; - -describe("IDPay Onboarding machine", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should have a default state of WAITING_INITIATIVE_SELECTION", () => { - const machine = createIDPayOnboardingMachine(); - expect(machine.initialState.value).toEqual("WAITING_INITIATIVE_SELECTION"); - }); - - it("should allow the citizen to complete onboarding on happy path", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve({ - initiativeId: T_INITIATIVE_ID - }) - ); - - mockServices.loadInitiativeStatus.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.acceptTos.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadRequiredCriteria.mockImplementation(async () => - Promise.resolve(O.some(T_REQUIRED_CRITERIA)) - ); - - mockServices.acceptRequiredCriteria.mockImplementation(async () => - Promise.resolve(undefined) - ); - - const machine = createIDPayOnboardingMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - service.send({ - type: "SELECT_INITIATIVE", - serviceId: T_SERVICE_ID - }); - - expect(currentState.value).toMatch("LOADING_INITIATIVE"); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledWith( - expect.objectContaining({ - serviceId: T_SERVICE_ID - }), - expect.anything(), - expect.anything() - ) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeStatus).toHaveBeenCalledWith( - expect.objectContaining({ - initiative: { - initiativeId: T_INITIATIVE_ID - }, - serviceId: T_SERVICE_ID - }), - expect.anything(), - expect.anything() - ) - ); - - expect(currentState.value).toMatch("DISPLAYING_INITIATIVE"); - - service.send({ - type: "ACCEPT_TOS" - }); - - expect(currentState.value).toMatch("ACCEPTING_TOS"); - - await waitFor(() => - expect(mockServices.acceptTos).toHaveBeenCalledWith( - expect.objectContaining({ - serviceId: T_SERVICE_ID - }), - expect.anything(), - expect.anything() - ) - ); - - await waitFor(() => - expect(mockServices.loadRequiredCriteria).toHaveBeenCalledWith( - expect.objectContaining({ - serviceId: T_SERVICE_ID, - initiative: { - initiativeId: T_INITIATIVE_ID - } - }), - expect.anything(), - expect.anything() - ) - ); - - await waitFor(() => - expect(mockActions.navigateToPDNDCriteriaScreen).toHaveBeenCalled() - ); - - expect(currentState.value).toMatch("DISPLAYING_REQUIRED_PDND_CRITERIA"); - - service.send({ - type: "ACCEPT_REQUIRED_PDND_CRITERIA" - }); - - await waitFor(() => - expect( - mockActions.navigateToBoolSelfDeclarationsScreen - ).toHaveBeenCalled() - ); - - expect(currentState.value).toMatchObject({ - DISPLAYING_REQUIRED_SELF_CRITERIA: "DISPLAYING_BOOL_CRITERIA" - }); - - service.send({ - type: "ACCEPT_REQUIRED_BOOL_CRITERIA" - }); - - await waitFor(() => - expect( - mockActions.navigateToMultiSelfDeclarationsScreen - ).toHaveBeenCalled() - ); - - expect(currentState.value).toMatchObject({ - DISPLAYING_REQUIRED_SELF_CRITERIA: "DISPLAYING_MULTI_CRITERIA" - }); - - service.send({ - type: "SELECT_MULTI_CONSENT", - data: T_SELF_CONSENT_MULTI - }); - - await waitFor(() => - expect(mockActions.navigateToCompletionScreen).toHaveBeenCalled() - ); - - await waitFor(() => - expect(mockServices.acceptRequiredCriteria).toHaveBeenCalledWith( - expect.objectContaining({ - serviceId: T_SERVICE_ID, - initiative: { - initiativeId: T_INITIATIVE_ID - }, - requiredCriteria: O.some(T_REQUIRED_CRITERIA) - }), - expect.anything(), - expect.anything() - ) - ); - - expect(currentState.value).toMatch("DISPLAYING_ONBOARDING_COMPLETED"); - }); - - it("should allow the citizen to exit onboarding from any state", async () => { - const machine = createIDPayOnboardingMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - const service = interpret(machine); - - service.start(); - - service.send({ - type: "QUIT_ONBOARDING" - }); - - await waitFor(() => expect(mockActions.exitOnboarding).toHaveBeenCalled()); - }); - - it("should not allow the citizen to complete the onboarding if initiative fails to load", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.reject(OnboardingFailureEnum.GENERIC) - ); - - const machine = createIDPayOnboardingMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("WAITING_INITIATIVE_SELECTION"); - - service.send({ - type: "SELECT_INITIATIVE", - serviceId: T_SERVICE_ID - }); - - await waitFor(() => expect(mockServices.loadInitiative).toHaveBeenCalled()); - await waitFor(() => - expect(mockServices.loadInitiativeStatus).toHaveBeenCalledTimes(0) - ); - - expect(currentState.value).toMatch("DISPLAYING_ONBOARDING_FAILURE"); - }); - - it("should not allow the citizen to complete the onboarding if initiative status is not valid", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve({ - initiativeId: T_INITIATIVE_ID - }) - ); - - mockServices.loadInitiativeStatus.mockImplementation(async () => - Promise.reject(OnboardingFailureEnum.USER_ONBOARDED) - ); - - const machine = createIDPayOnboardingMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("WAITING_INITIATIVE_SELECTION"); - - service.send({ - type: "SELECT_INITIATIVE", - serviceId: T_SERVICE_ID - }); - - await waitFor(() => expect(mockServices.loadInitiative).toHaveBeenCalled()); - await waitFor(() => - expect(mockServices.loadInitiativeStatus).toHaveBeenCalled() - ); - - expect(currentState.value).toMatch("DISPLAYING_ONBOARDING_FAILURE"); - }); - - it("should not allow the citizen to complete the onboarding if prerequesites check fails", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve({ - initiativeId: T_INITIATIVE_ID - }) - ); - - mockServices.loadInitiativeStatus.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.acceptTos.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadRequiredCriteria.mockImplementation(async () => - Promise.reject(OnboardingFailureEnum.GENERIC) - ); - - const machine = createIDPayOnboardingMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("WAITING_INITIATIVE_SELECTION"); - - service.send({ - type: "SELECT_INITIATIVE", - serviceId: T_SERVICE_ID - }); - - await waitFor(() => expect(mockServices.loadInitiative).toHaveBeenCalled()); - await waitFor(() => - expect(mockServices.loadInitiativeStatus).toHaveBeenCalled() - ); - - expect(currentState.value).toMatch("DISPLAYING_INITIATIVE"); - - service.send({ - type: "ACCEPT_TOS" - }); - - await waitFor(() => expect(mockServices.acceptTos).toHaveBeenCalled()); - await waitFor(() => - expect(mockServices.loadRequiredCriteria).toHaveBeenCalled() - ); - - expect(currentState.value).toMatch("DISPLAYING_ONBOARDING_FAILURE"); - }); -}); diff --git a/ts/features/idpay/onboarding/xstate/__tests__/services.test.ts b/ts/features/idpay/onboarding/xstate/__tests__/services.test.ts deleted file mode 100644 index 5980c7ececf..00000000000 --- a/ts/features/idpay/onboarding/xstate/__tests__/services.test.ts +++ /dev/null @@ -1,509 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; -import { PreferredLanguageEnum } from "../../../../../../definitions/backend/PreferredLanguage"; -import { ErrorDTO } from "../../../../../../definitions/idpay/ErrorDTO"; -import { - OnboardingStatusDTO, - StatusEnum -} from "../../../../../../definitions/idpay/OnboardingStatusDTO"; -import { RequiredCriteriaDTO } from "../../../../../../definitions/idpay/RequiredCriteriaDTO"; -import { _typeEnum as BoolTypeEnum } from "../../../../../../definitions/idpay/SelfConsentBoolDTO"; -import { SelfConsentDTO } from "../../../../../../definitions/idpay/SelfConsentDTO"; -import { - SelfConsentMultiDTO, - _typeEnum as MultiTypeEnum -} from "../../../../../../definitions/idpay/SelfConsentMultiDTO"; -import { mockIDPayClient } from "../../../common/api/__mocks__/client"; -import { OnboardingFailureEnum } from "../../types/OnboardingFailure"; -import { Context } from "../machine"; -import { createServicesImplementation } from "../services"; -import { InitiativeDataDTO } from "../../../../../../definitions/idpay/InitiativeDataDTO"; -import { - AuthorityEnum, - CodeEnum -} from "../../../../../../definitions/idpay/PDNDCriteriaDTO"; - -const T_PREFERRED_LANGUAGE = PreferredLanguageEnum.it_IT; -const T_AUTH_TOKEN = "abc123"; - -const T_CONTEXT: Context = { - failure: O.none, - initiativeStatus: O.none, - multiConsentsAnswers: {}, - multiConsentsPage: 0, - selfDeclarationBoolAnswers: {} -}; - -const T_SERVICE_ID = "efg456"; - -const T_INITIATIVE_DATA_DTO: InitiativeDataDTO = { - initiativeId: "1234", - description: "", - initiativeName: "abc", - organizationId: "123", - organizationName: "abc", - privacyLink: "abc", - tcLink: "abc" -}; - -const T_REQUIRED_CRITERIA_DTO: RequiredCriteriaDTO = { - pdndCriteria: [ - { - authority: AuthorityEnum.AGID, - code: CodeEnum.BIRTHDATE, - description: "c", - value: "d" - } - ], - selfDeclarationList: [ - { - _type: MultiTypeEnum.multi, - code: "1", - description: "a", - value: ["A", "B", "C"] - }, - { - _type: MultiTypeEnum.multi, - code: "2", - description: "b", - value: ["D", "E", "F"] - }, - { - _type: BoolTypeEnum.boolean, - code: "3", - description: "c", - value: false - } - ] -}; - -const T_MULTI_CONSENTS_ANSWERS: Record = { - 1: { - _type: MultiTypeEnum.multi, - code: "1", - value: "A" - }, - 2: { - _type: MultiTypeEnum.multi, - code: "2", - value: "D" - } -}; - -const T_ACCEPTED_SELF_DECLARATION_LIST: Array = [ - { - _type: BoolTypeEnum.boolean, - code: "3", - accepted: true - }, - ...Object.values(T_MULTI_CONSENTS_ANSWERS) -] as Array; - -describe("IDPay Onboarding machine services", () => { - const services = createServicesImplementation( - mockIDPayClient, - T_AUTH_TOKEN, - T_PREFERRED_LANGUAGE - ); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("loadInitiative", () => { - it("should fail if not service id is provided in context", async () => { - await expect(services.loadInitiative(T_CONTEXT)).rejects.toMatch( - OnboardingFailureEnum.GENERIC - ); - - expect(mockIDPayClient.getInitiativeData).toHaveBeenCalledTimes(0); - }); - - it("should fail if response status code != 200", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.getInitiativeData.mockImplementation(() => response); - - await expect( - services.loadInitiative({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID - }) - ).rejects.toMatch(OnboardingFailureEnum.GENERIC); - - expect(mockIDPayClient.getInitiativeData).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - serviceId: T_SERVICE_ID - }) - ); - }); - - it("should get initiative data", async () => { - const response: E.Either< - Error, - { status: number; value?: InitiativeDataDTO } - > = E.right({ status: 200, value: T_INITIATIVE_DATA_DTO }); - - mockIDPayClient.getInitiativeData.mockImplementation(() => response); - - await expect( - services.loadInitiative({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID - }) - ).resolves.toMatchObject(T_INITIATIVE_DATA_DTO); - - expect(mockIDPayClient.getInitiativeData).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - serviceId: T_SERVICE_ID - }) - ); - }); - }); - - describe("loadInitiativeStatus", () => { - it("should fail if initiative is not provided with context", async () => { - await expect(services.loadInitiativeStatus(T_CONTEXT)).rejects.toMatch( - OnboardingFailureEnum.GENERIC - ); - - expect(mockIDPayClient.onboardingStatus).toHaveBeenCalledTimes(0); - }); - - it("should fail if response status code != 200", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.onboardingStatus.mockImplementation(() => response); - - await expect( - services.loadInitiativeStatus({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).rejects.toMatch(OnboardingFailureEnum.GENERIC); - - expect(mockIDPayClient.onboardingStatus).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - }) - ); - }); - - it("should return none if response status code == 404", async () => { - const response: E.Either = - E.right({ status: 404, value: { code: 400, message: "" } }); - - mockIDPayClient.onboardingStatus.mockImplementation(() => response); - - await expect( - services.loadInitiativeStatus({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).resolves.toMatchObject(O.none); - - expect(mockIDPayClient.onboardingStatus).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - }) - ); - }); - - const statusFailures: ReadonlyArray< - [status: StatusEnum, failure: OnboardingFailureEnum] - > = [ - [StatusEnum.ELIGIBLE_KO, OnboardingFailureEnum.NOT_ELIGIBLE], - [StatusEnum.ONBOARDING_KO, OnboardingFailureEnum.GENERIC], - [StatusEnum.ONBOARDING_OK, OnboardingFailureEnum.USER_ONBOARDED], - [StatusEnum.UNSUBSCRIBED, OnboardingFailureEnum.USER_UNSUBSCRIBED], - [StatusEnum.ON_EVALUATION, OnboardingFailureEnum.ON_EVALUATION] - ]; - - test.each(statusFailures)( - "if initiative status is %p, should get failure %p", - async (status, failure) => { - const response: E.Either< - Error, - { status: number; value?: OnboardingStatusDTO } - > = E.right({ status: 200, value: { status, statusDate: new Date() } }); - - mockIDPayClient.onboardingStatus.mockImplementation(() => response); - - await expect( - services.loadInitiativeStatus({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).rejects.toMatch(failure); - } - ); - - const allowedStatuses: ReadonlyArray<[status: StatusEnum]> = [ - [StatusEnum.ACCEPTED_TC], - [StatusEnum.INVITED] - ]; - - test.each(allowedStatuses)( - "if initiative status is %p, should succeed and return it", - async status => { - const response: E.Either< - Error, - { status: number; value?: OnboardingStatusDTO } - > = E.right({ status: 200, value: { status, statusDate: new Date() } }); - - mockIDPayClient.onboardingStatus.mockImplementation(() => response); - - await expect( - services.loadInitiativeStatus({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).resolves.toMatchObject(O.some(status)); - } - ); - }); - - describe("acceptTos", () => { - it("should fail if initiative is not provided with context", async () => { - await expect(services.acceptTos(T_CONTEXT)).rejects.toMatch( - OnboardingFailureEnum.GENERIC - ); - - expect(mockIDPayClient.onboardingCitizen).toHaveBeenCalledTimes(0); - }); - - it("should fail if response status code != 204", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.onboardingCitizen.mockImplementation(() => response); - - await expect( - services.acceptTos({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).rejects.toMatch(OnboardingFailureEnum.GENERIC); - - expect(mockIDPayClient.onboardingCitizen).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - } - }) - ); - }); - - it("should return undefined if response status code = 204", async () => { - const response: E.Either = - E.right({ status: 204, value: undefined }); - - mockIDPayClient.onboardingCitizen.mockImplementation(() => response); - - await expect( - services.acceptTos({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).resolves.toBeUndefined(); - - expect(mockIDPayClient.onboardingCitizen).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - } - }) - ); - }); - }); - - describe("loadRequiredCriteria", () => { - it("should fail if initiative is not provided with context", async () => { - await expect(services.loadRequiredCriteria(T_CONTEXT)).rejects.toMatch( - OnboardingFailureEnum.GENERIC - ); - - expect(mockIDPayClient.checkPrerequisites).toHaveBeenCalledTimes(0); - }); - - it("should fail if response status code != 200 or 202", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.checkPrerequisites.mockImplementation(() => response); - - await expect( - services.loadRequiredCriteria({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).rejects.toMatch(OnboardingFailureEnum.GENERIC); - - expect(mockIDPayClient.checkPrerequisites).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - } - }) - ); - }); - - it("should return O.none if response status code is 202", async () => { - const response: E.Either = - E.right({ status: 202, value: undefined }); - - mockIDPayClient.checkPrerequisites.mockImplementation(() => response); - - await expect( - services.loadRequiredCriteria({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).resolves.toMatchObject(O.none); - - expect(mockIDPayClient.checkPrerequisites).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - } - }) - ); - }); - - it("should return citizen's required criteria if request is success", async () => { - const response: E.Either< - Error, - { status: number; value?: RequiredCriteriaDTO } - > = E.right({ - status: 200, - value: T_REQUIRED_CRITERIA_DTO - }); - - mockIDPayClient.checkPrerequisites.mockImplementation(() => response); - - await expect( - services.loadRequiredCriteria({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO - }) - ).resolves.toMatchObject(O.some(T_REQUIRED_CRITERIA_DTO)); - - expect(mockIDPayClient.checkPrerequisites).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId - } - }) - ); - }); - }); - - describe("acceptRequiredCriteria", () => { - it("should fail if initiative or required criterias are not provided with context", async () => { - await expect(services.acceptRequiredCriteria(T_CONTEXT)).rejects.toMatch( - OnboardingFailureEnum.GENERIC - ); - - expect(mockIDPayClient.consentOnboarding).toHaveBeenCalledTimes(0); - }); - - it("should fail if required criteria in context is none", async () => { - await expect( - services.acceptRequiredCriteria({ - ...T_CONTEXT, - requiredCriteria: O.none - }) - ).rejects.toMatch(OnboardingFailureEnum.GENERIC); - - expect(mockIDPayClient.consentOnboarding).toHaveBeenCalledTimes(0); - }); - - it("should fail if response status code != 202", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.consentOnboarding.mockImplementation(() => response); - - await expect( - services.acceptRequiredCriteria({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO, - requiredCriteria: O.some(T_REQUIRED_CRITERIA_DTO), - multiConsentsAnswers: T_MULTI_CONSENTS_ANSWERS - }) - ).rejects.toMatch(OnboardingFailureEnum.GENERIC); - - expect(mockIDPayClient.consentOnboarding).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId, - pdndAccept: true, - selfDeclarationList: T_ACCEPTED_SELF_DECLARATION_LIST - } - }) - ); - }); - - it("should return undefined if success", async () => { - const response: E.Either = - E.right({ status: 202, value: undefined }); - - mockIDPayClient.consentOnboarding.mockImplementation(() => response); - - await expect( - services.acceptRequiredCriteria({ - ...T_CONTEXT, - serviceId: T_SERVICE_ID, - initiative: T_INITIATIVE_DATA_DTO, - requiredCriteria: O.some(T_REQUIRED_CRITERIA_DTO), - multiConsentsAnswers: T_MULTI_CONSENTS_ANSWERS - }) - ).resolves.toBeUndefined(); - - expect(mockIDPayClient.consentOnboarding).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - body: { - initiativeId: T_INITIATIVE_DATA_DTO.initiativeId, - pdndAccept: true, - selfDeclarationList: T_ACCEPTED_SELF_DECLARATION_LIST - } - }) - ); - }); - }); -}); diff --git a/ts/features/idpay/onboarding/xstate/actions.ts b/ts/features/idpay/onboarding/xstate/actions.ts deleted file mode 100644 index 2942009e3cb..00000000000 --- a/ts/features/idpay/onboarding/xstate/actions.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; -import { useIODispatch } from "../../../../store/hooks"; -import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; -import { IDPayDetailsRoutes } from "../../details/navigation"; -import { - IDPayOnboardingParamsList, - IDPayOnboardingRoutes, - IDPayOnboardingStackNavigationProp -} from "../navigation/navigator"; -import { Context } from "./machine"; - -const createActionsImplementation = ( - rootNavigation: IOStackNavigationProp, - onboardingNavigation: IDPayOnboardingStackNavigationProp< - IDPayOnboardingParamsList, - keyof IDPayOnboardingParamsList - >, - dispatch: ReturnType -) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - - const navigateToInitiativeDetailsScreen = guardedNavigationAction( - (context: Context) => { - if (context.serviceId === undefined) { - throw new Error("serviceId is undefined"); - } - onboardingNavigation.navigate( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, - { - serviceId: context.serviceId - } - ); - } - ); - - const navigateToPDNDCriteriaScreen = guardedNavigationAction(() => - onboardingNavigation.navigate( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE - ) - ); - - const navigateToBoolSelfDeclarationsScreen = guardedNavigationAction(() => - onboardingNavigation.navigate( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS - ) - ); - - const navigateToMultiSelfDeclarationsScreen = guardedNavigationAction( - (context: Context) => - onboardingNavigation.navigate({ - name: IDPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS, - key: String(context.multiConsentsPage) - }) - ); - - const navigateToCompletionScreen = () => { - onboardingNavigation.navigate( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION - ); - }; - - const navigateToFailureScreen = () => { - onboardingNavigation.navigate( - IDPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE - ); - }; - - const navigateToInitiativeMonitoringScreen = (context: Context) => { - if (context.initiative === undefined) { - throw new Error("initiative is undefined"); - } - - rootNavigation.replace(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { - screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, - params: { - initiativeId: context.initiative.initiativeId - } - }); - }; - - const exitOnboarding = () => { - onboardingNavigation.pop(); - }; - - return { - handleSessionExpired, - navigateToInitiativeDetailsScreen, - navigateToPDNDCriteriaScreen, - navigateToBoolSelfDeclarationsScreen, - navigateToMultiSelfDeclarationsScreen, - navigateToCompletionScreen, - navigateToFailureScreen, - navigateToInitiativeMonitoringScreen, - exitOnboarding - }; -}; - -export { createActionsImplementation }; diff --git a/ts/features/idpay/onboarding/xstate/events.ts b/ts/features/idpay/onboarding/xstate/events.ts deleted file mode 100644 index 2ceafd65e12..00000000000 --- a/ts/features/idpay/onboarding/xstate/events.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; -import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; -import { E_BACK } from "../../../../xstate/types/events"; - -// Events types -type E_SELECT_INITIATIVE = { - type: "SELECT_INITIATIVE"; - serviceId: string; -}; - -type E_ACCEPT_TOS = { - type: "ACCEPT_TOS"; -}; - -type E_ACCEPT_REQUIRED_PDND_CRITERIA = { - type: "ACCEPT_REQUIRED_PDND_CRITERIA"; -}; - -type E_TOGGLE_BOOL_CRITERIA = { - type: "TOGGLE_BOOL_CRITERIA"; - criteria: SelfDeclarationBoolDTO; -}; - -type E_ACCEPT_REQUIRED_BOOL_CRITERIA = { - type: "ACCEPT_REQUIRED_BOOL_CRITERIA"; -}; - -type E_QUIT_ONBOARDING = { - type: "QUIT_ONBOARDING"; -}; - -type E_SHOW_INITIATIVE_DETAILS = { - type: "SHOW_INITIATIVE_DETAILS"; -}; - -type E_SELECT_MULTI_CONSENT = { - type: "SELECT_MULTI_CONSENT"; - data: SelfConsentMultiDTO; -}; - -export type Events = - | E_SELECT_INITIATIVE - | E_ACCEPT_TOS - | E_ACCEPT_REQUIRED_PDND_CRITERIA - | E_QUIT_ONBOARDING - | E_SHOW_INITIATIVE_DETAILS - | E_BACK - | E_SELECT_MULTI_CONSENT - | E_TOGGLE_BOOL_CRITERIA - | E_ACCEPT_REQUIRED_BOOL_CRITERIA; diff --git a/ts/features/idpay/onboarding/xstate/machine.ts b/ts/features/idpay/onboarding/xstate/machine.ts deleted file mode 100644 index fe26c50c96c..00000000000 --- a/ts/features/idpay/onboarding/xstate/machine.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { pipe } from "fp-ts/lib/function"; -/* eslint-disable no-underscore-dangle */ -import * as O from "fp-ts/lib/Option"; -import { assign, createMachine } from "xstate"; -import { InitiativeDataDTO } from "../../../../../definitions/idpay/InitiativeDataDTO"; -import { StatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; -import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; -import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; -import { - LOADING_TAG, - UPSERTING_TAG, - WAITING_USER_INPUT_TAG -} from "../../../../xstate/utils"; -import { - OnboardingFailure, - OnboardingFailureEnum -} from "../types/OnboardingFailure"; -import { Events } from "./events"; -import { - getBoolRequiredCriteriaFromContext, - getMultiRequiredCriteriaFromContext -} from "./selectors"; - -// Context types -export type Context = { - serviceId?: string; - initiative?: InitiativeDataDTO; - initiativeStatus: O.Option; - requiredCriteria?: O.Option; - multiConsentsPage: number; - multiConsentsAnswers: Record; - selfDeclarationBoolAnswers: Record; - failure: O.Option; -}; - -const INITIAL_CONTEXT: Context = { - initiativeStatus: O.none, - multiConsentsPage: 0, - multiConsentsAnswers: {}, - selfDeclarationBoolAnswers: {}, - failure: O.none -}; - -// Services types -type Services = { - loadInitiative: { - data: InitiativeDataDTO; - }; - loadInitiativeStatus: { - data: O.Option; - }; - acceptTos: { - data: undefined; - }; - loadRequiredCriteria: { - data: O.Option; - }; - acceptRequiredCriteria: { - data: undefined; - }; -}; - -const hasPDNDRequiredCriteria = (context: Context) => { - const requiredCriteria = context.requiredCriteria; - if (requiredCriteria !== undefined && O.isSome(requiredCriteria)) { - return requiredCriteria.value.pdndCriteria.length > 0; - } - - return false; -}; - -const hasSelfRequiredCriteria = (context: Context) => { - const requiredCriteria = context.requiredCriteria; - if (requiredCriteria !== undefined && O.isSome(requiredCriteria)) { - return requiredCriteria.value.selfDeclarationList.length > 0; - } - - return false; -}; - -const hasBoolRequiredCriteria = (context: Context) => - getBoolRequiredCriteriaFromContext(context).length > 0; - -const hasMultiRequiredCriteria = (context: Context) => - getMultiRequiredCriteriaFromContext(context).length > 0; - -const isLastMultiConsent = (context: Context) => - context.multiConsentsPage >= - getMultiRequiredCriteriaFromContext(context).length - 1; - -const shouldRedoUndefinedPrerequisites = (context: Context) => { - const isLast = isLastMultiConsent(context); - const lengthIsDifferent = - // since the guard is checked before the action that pushes the last page, we need to add 1 to the length - Object.entries(context.multiConsentsAnswers).length + 1 !== - getMultiRequiredCriteriaFromContext(context).length; - return isLast && lengthIsDifferent; -}; - -const isNotFirstMultiConsent = (context: Context) => - context.multiConsentsPage > 0; - -const createIDPayOnboardingMachine = () => - /** @xstate-layout N4IgpgJg5mDOIC5QEkAiAFAggTQPoHkA5AIX0wCVVlCBxAYgEUBVZAFQJLMupoG0AGALqJQABwD2sAJYAXKeIB2IkAA9EARgDsmgGwA6dQBZN-bTvU6AHJf46ANCACeGgEwBfNw7RY8RUhSpaPQB1TDYeXGpwzFZkADUAUVwAZQSAGQSAYViiOlSM7MjCaNjEgWEkEAlpOUVlNQQAZkN1PUtGzQBWAE5DW35GxssXRodnBE6XfUNuxqMXTuNZ5o8vDBwOf24gtLJAmiKS+IS6CEUwPSkFADdxAGsLgBtxAEMIZAVZKRe5a7By5TVL51SoNSyGTp6XRTBb8dSNbo6bouMaIRYuPQubSddSWXQDHQ41YgbwbPxcfZ6XaYfaHWIxY50MAAJ2Z4mZelEjx+ADN2QBbPTPN4fL4-KR-AGVIG1JSgxDaVEIFyWZGYwwuTUtSw9FxaYmk3ycAI8Kl7CJRemlJLJVgxJjJU7nS43e5PV7vT5ycV-ZIyH4AV1gUrEkmBctADQhrRGRm6iM6nUaC0WSpc-Dxeh6yZ0U1smu6nQN6yNW0p1NpluQDMSKTtrAdTNZ7M53JkfOZguFnrFvzAfsDwaEgLDsvqiGjmLmMwTSZThiVuM6-D0hM6XSmk3aPWLPk2FNNCTimDSTAZtDp1etdftjpDVVH8gjqgVmiVcy6bUMOu-OkJXSMXcyWNbYaD0I8TzPWILyrGsbXrRteHUCpQxqJ9xwQbpVT0QZtEsKx+A1TRETTAscMRZNuiMfg9W6TQgNLA8ggg09zwOWDr1tW86F4FwUIfNCQUjRAsO6HCOk0fCbCIkinA0eMMQhVUjFVLFl3ozwSRLfcTSCKhknQNIcAtYorUZTBMkyBJ0HYVh8GSe8ZXQ+Umi0FcdA6cEdH4QsOmMRdNBVPQqJxOiLEkwxDEaBidNAvQLKsmyIjsx0zgUC4rluB49BeABjXKwFEGRWEkRzHyEl9MLfOTlSsfRJnhLC-28zcYvJXSwIS6zoIOFKmzZDkuV5AUcvywritK4dpXK58wWq8YsXjVdGk6VU6P6ZE2pA8tzQvcgEmYZB9tQXBMnINgEnOzAnXSl0svdN5yDAABHAMpGZSBMmZWQWW+MrBNmjR5o0dQMVzZdCPUQsjHUTodC2stTQrCJ9sO47TvO1hLurfqWyG9sRu7J7Xvez7vpkX6Xn+8MML-SFEVVRoMzmEYVrTHRjBwkYIZ6fDiIRpiwJYqCUYOlh0bOi6rp4qbUJplzGisPQBlByKtEkzRBnsGqXF6HC+jhFbkSmfnNMNWLKWFtjcFR8WEhOyWselpD+KciqGkVGrYd1zFNEi-CVvTbQBY68Dj1YnqbbFo77YxqWcd412ZowujF34OE2naXQ-y6CEiTN7T2ri-TDOMvbo-R9BUEIB3Mex66upsqO0djqua7jp3q2pscXIhTRldmXOVRxXXCUXFbISkhYEXBP2opD4vkAMozsFFluTrb2v44byzuubu2N+rrfO8wF2RwBjDIv7nyOnRHVQYTRdQZXQPQoWQsBm6BfKRLle14P3Am8O71zoDQfAuBiAWQANLd2csJBAV8B63w1PfUenRFx9H0EzdchgObeSikWAue4i4-yXqXVe5d14pHSAAMWAdLNKFxYD+gpnoc2JDTS-zLgcW2McTr5DoY7eusD3aICZtoAw1g4RGGaLgqYSoPL93jHCcw645je2-pwshf9KEAIEfQnGYCIHQJEYDBB64kFD1QY-L22C9BRVMKony-AkzwyIcBRGeltHcP3nw6haRBF12lkYyBmQYHIXPvLeB79GgGF6CrdMSZNDqAwbMZW3lVS4K1jgzRXjl4+N4ejfRQirph0gtbYpQSE6ywElEyqsNCL2OaCYOGsxwoLhqnDDEcNLBQyGKDKwkxclgS4RQnhFdY6VO3mUiOEQpknx4hE6aF8XINMME04wLikRzB0H7JULTM62ELFRGicNhl6FGf-Px8z656EyNgTIGRcAAFkmBpFiAYhuqB+G0NOkQVIhBWCmIwlMawU5ekqgAtgpUvSVymFxJRXMi03FrGIdtLR+Sxm+KKb8kp1Y7kPKea895yBPl0BpD8gJfzCAAqBUsuWPdondFsMFawIx4wal2YMJUSSoQuEirrXoyY-bRXcYxUOlzdHXNxVUzAFzvFYtIPgNIZLG7sEKbHJVKq8WnxqW7MxtEYzZiwitCECIeVDExPyyKGp4wWF1uctVVyJaypuhlV02U8oFSKsTN6H0IBfR+t9Kmerk4K3UAMKEcNsw2sIvhceisDDUWoh5cGorNIKHEBAOAyh2HotoJExllUAC0cw0zrN6PGXoxEbAWGTOc0I4QYKmSvMcfxWQciEELXAyq-lOkzCzFYIw34dm9HOcjZtRxEjdtERMCEOE4bJltYmJElgeULFZbYZcKDh5fzFRbJGu12ItrgjeBsyQZ1mMCmmeFmIKKEV6Phdw+6OHMXDiLSdZlaxcXPZe2mzLM5xgcYrfC2sFoRrErDPEvQPJM3UIBF9+aRkKpMlOhIf6XIqj6FCeJQ6qLflTl7A5NhrCSW8kmHElhHW7ySheFKGH4GDCZsFLogwegzGXGzIjLi2gZjxPhFxK1cTjqPdi2OOqGOVSmGJeMQxhWqMClFdmK02jUUCnMZYLjzlW0jhq4+9dJNRmBsqfCfLb7G1xOnZ9qKPGC3lZi51rcj6fMMyJFEtjNRxJMHBuYzQ6LnMleMqhNyrquYQPB7CiZmj8oisiJUfQMR4jVu0XZHLRU2fFYvBzUqcVUp1TMj9BwQvVjC6DDoWYkwamMOCOLNUkT9yREKxasMTCGACyhnLkyZXTPuY8pIxKPkSeWXUhoCxCSHJ8vy2RXKFEtEzrspMsNES53a9loLejusn3s+QiIWqXPDaLQ0BFsTCKTFzH0XQVEeXeTvUK3BUMrD1sQ54zqNHdMTP06Fg7PbRsmH7pJWG2SFua0XCqMScJklRURNDwkq2dsXlfQcTI+BnmGQSFjVAYW2MVs8kidcRhEwdPGA-LBzKBiLAGCgqjz27OBYPReGhYRTz7TC8y2JalNTwjx7rInYiVStG0H5XWkx07U48EAA */ - createMachine( - { - context: INITIAL_CONTEXT, - tsTypes: {} as import("./machine.typegen").Typegen0, - schema: { - context: {} as Context, - events: {} as Events, - services: {} as Services - }, - predictableActionArguments: true, - id: "IDPAY_ONBOARDING", - initial: "WAITING_INITIATIVE_SELECTION", - on: { - QUIT_ONBOARDING: { - actions: "exitOnboarding" - } - }, - states: { - WAITING_INITIATIVE_SELECTION: { - tags: [LOADING_TAG], - on: { - SELECT_INITIATIVE: { - target: "LOADING_INITIATIVE", - actions: "selectInitiative" - } - } - }, - - LOADING_INITIATIVE: { - tags: [LOADING_TAG], - invoke: { - src: "loadInitiative", - id: "loadInitiative", - onDone: [ - { - target: "LOADING_INITIATIVE_STATUS", - actions: "loadInitiativeSuccess" - } - ], - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - target: "DISPLAYING_ONBOARDING_FAILURE" - } - ] - } - }, - - LOADING_INITIATIVE_STATUS: { - tags: [LOADING_TAG], - invoke: { - src: "loadInitiativeStatus", - id: "loadInitiativeStatus", - onDone: [ - { - target: "DISPLAYING_INITIATIVE", - actions: "loadInitiativeStatusSuccess" - } - ], - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - target: "DISPLAYING_ONBOARDING_FAILURE" - } - ] - } - }, - - DISPLAYING_INITIATIVE: { - entry: "navigateToInitiativeDetailsScreen", - on: { - ACCEPT_TOS: { - target: "ACCEPTING_TOS" - } - } - }, - - ACCEPTING_TOS: { - tags: [UPSERTING_TAG], - invoke: { - id: "acceptTos", - src: "acceptTos", - onDone: [ - { - target: "LOADING_REQUIRED_CRITERIA" - } - ], - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - target: "DISPLAYING_ONBOARDING_FAILURE" - } - ] - } - }, - - LOADING_REQUIRED_CRITERIA: { - tags: [LOADING_TAG], - invoke: { - id: "loadRequiredCriteria", - src: "loadRequiredCriteria", - onDone: [ - { - target: "EVALUATING_REQUIRED_CRITERIA", - actions: "loadRequiredCriteriaSuccess" - } - ], - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - target: "DISPLAYING_ONBOARDING_FAILURE" - } - ] - } - }, - - // Self transition node to evaluate required criteria - EVALUATING_REQUIRED_CRITERIA: { - tags: [LOADING_TAG], - always: [ - { - target: "DISPLAYING_REQUIRED_PDND_CRITERIA", - cond: "hasPDNDRequiredCriteria" - }, - { - target: "DISPLAYING_REQUIRED_SELF_CRITERIA", - cond: "hasSelfRequiredCriteria" - }, - { - target: "DISPLAYING_ONBOARDING_COMPLETED" - } - ] - }, - - DISPLAYING_REQUIRED_PDND_CRITERIA: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToPDNDCriteriaScreen", - on: { - ACCEPT_REQUIRED_PDND_CRITERIA: [ - { - target: "DISPLAYING_REQUIRED_SELF_CRITERIA", - cond: "hasSelfRequiredCriteria" - }, - { - target: "ACCEPTING_REQUIRED_CRITERIA" - } - ], - BACK: [ - { - target: "DISPLAYING_INITIATIVE" - } - ] - } - }, - - DISPLAYING_REQUIRED_SELF_CRITERIA: { - tags: [WAITING_USER_INPUT_TAG], - initial: "EVALUATING_SELF_CRITERIA", - onDone: { - target: "ACCEPTING_REQUIRED_CRITERIA" - // triggered by the 'final' substate - }, - states: { - EVALUATING_SELF_CRITERIA: { - tags: [LOADING_TAG], - always: [ - { - target: "DISPLAYING_BOOL_CRITERIA", - cond: "hasBoolRequiredCriteria" - }, - { - target: "DISPLAYING_MULTI_CRITERIA", - cond: "hasMultiRequiredCriteria" - } - ] - }, - DISPLAYING_BOOL_CRITERIA: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToBoolSelfDeclarationsScreen", - on: { - BACK: [ - { - target: - "#IDPAY_ONBOARDING.DISPLAYING_REQUIRED_PDND_CRITERIA", - cond: "hasPDNDRequiredCriteria" - }, - { - target: "#IDPAY_ONBOARDING.DISPLAYING_INITIATIVE" - } - ], - TOGGLE_BOOL_CRITERIA: { - actions: "toggleBoolCriteria" - }, - ACCEPT_REQUIRED_BOOL_CRITERIA: [ - { - target: "DISPLAYING_MULTI_CRITERIA", - cond: "hasMultiRequiredCriteria" - }, - { - target: "ALL_PREREQUISITES_ANSWERED" - } - ] - } - }, - DISPLAYING_MULTI_CRITERIA: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToMultiSelfDeclarationsScreen", - on: { - SELECT_MULTI_CONSENT: [ - { - // this is an edge case where for some reason - // an entry in the consents record is missing - // at the end of the flow - cond: "shouldRedoUndefinedPrerequisites", - actions: [ - "addMultiConsent", - "redoUndefinedPrerequisites", - "navigateToMultiSelfDeclarationsScreen" - ] - }, - { - cond: "isLastMultiConsent", - actions: "addMultiConsent", - target: "ALL_PREREQUISITES_ANSWERED" - }, - { - actions: [ - "addMultiConsent", - "increaseMultiConsentIndex", - "navigateToMultiSelfDeclarationsScreen" - ] - } - ], - BACK: [ - { - actions: [ - "decreaseMultiConsentIndex", - "navigateToMultiSelfDeclarationsScreen" - ], - cond: "isNotFirstMultiConsent" - }, - { - target: "DISPLAYING_BOOL_CRITERIA", - cond: "hasBoolRequiredCriteria" - }, - { - target: - "#IDPAY_ONBOARDING.DISPLAYING_REQUIRED_PDND_CRITERIA", - cond: "hasPDNDRequiredCriteria" - }, - { - target: "#IDPAY_ONBOARDING.DISPLAYING_INITIATIVE" - } - ] - } - }, - - ALL_PREREQUISITES_ANSWERED: { - type: "final" - } - } - }, - - ACCEPTING_REQUIRED_CRITERIA: { - tags: [UPSERTING_TAG], - entry: "navigateToCompletionScreen", - invoke: { - src: "acceptRequiredCriteria", - id: "acceptRequiredCriteria", - onDone: { - target: "DISPLAYING_ONBOARDING_COMPLETED" - }, - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - target: "DISPLAYING_ONBOARDING_FAILURE" - } - ] - } - }, - - DISPLAYING_ONBOARDING_COMPLETED: { - entry: "navigateToCompletionScreen" - }, - - DISPLAYING_ONBOARDING_FAILURE: { - entry: "navigateToFailureScreen", - on: { - SHOW_INITIATIVE_DETAILS: { - actions: "navigateToInitiativeMonitoringScreen" - } - } - }, - - SESSION_EXPIRED: { - entry: ["handleSessionExpired", "exitOnboarding"] - } - } - }, - { - actions: { - selectInitiative: assign((_, event) => ({ - serviceId: event.serviceId - })), - loadInitiativeSuccess: assign((_, event) => ({ - initiative: event.data - })), - loadInitiativeStatusSuccess: assign((_, event) => ({ - initiativeStatus: event.data - })), - loadRequiredCriteriaSuccess: assign((_, event) => ({ - requiredCriteria: event.data - })), - toggleBoolCriteria: assign((context, event) => ({ - selfDeclarationBoolAnswers: { - ...context.selfDeclarationBoolAnswers, - [event.criteria.code]: event.criteria.value - } - })), - addMultiConsent: assign((context, event) => ({ - multiConsentsAnswers: { - ...context.multiConsentsAnswers, - [context.multiConsentsPage]: event.data - } - })), - redoUndefinedPrerequisites: assign((context, _) => { - const firstUndefinedPage = Object.keys( - context.multiConsentsAnswers - ).findIndex(item => item === undefined); - // converts the record into an array, so that we can use the findIndex method - return { - multiConsentsPage: - firstUndefinedPage === -1 ? 0 : firstUndefinedPage - }; - }), - increaseMultiConsentIndex: assign((context, _) => ({ - multiConsentsPage: context.multiConsentsPage + 1 - })), - decreaseMultiConsentIndex: assign((context, _) => ({ - multiConsentsPage: context.multiConsentsPage - 1 - })), - setFailure: assign((_, event) => ({ - failure: pipe(O.of(event.data), O.filter(OnboardingFailure.is)) - })) - }, - guards: { - isSessionExpired: (_, event) => - pipe( - event.data, - OnboardingFailure.decode, - O.fromEither, - O.filter( - failure => failure === OnboardingFailureEnum.SESSION_EXPIRED - ), - O.isSome - ), - hasPDNDRequiredCriteria, - hasSelfRequiredCriteria, - hasBoolRequiredCriteria, - hasMultiRequiredCriteria, - shouldRedoUndefinedPrerequisites, - isLastMultiConsent, - isNotFirstMultiConsent - } - } - ); - -type IDPayOnboardingMachineType = ReturnType< - typeof createIDPayOnboardingMachine ->; - -export { createIDPayOnboardingMachine }; -export type { IDPayOnboardingMachineType }; diff --git a/ts/features/idpay/onboarding/xstate/provider.tsx b/ts/features/idpay/onboarding/xstate/provider.tsx deleted file mode 100644 index 6f42814cdea..00000000000 --- a/ts/features/idpay/onboarding/xstate/provider.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useNavigation } from "@react-navigation/native"; -import { useInterpret } from "@xstate/react"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import React from "react"; -import { InterpreterFrom } from "xstate"; -import { - idPayApiBaseUrl, - idPayApiUatBaseUrl, - idPayTestToken -} from "../../../../config"; -import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; -import { - isPagoPATestEnabledSelector, - preferredLanguageSelector -} from "../../../../store/reducers/persistedPreferences"; -import { - fromLocaleToPreferredLanguage, - getLocalePrimaryWithFallback -} from "../../../../utils/locale"; -import { useXStateMachine } from "../../../../xstate/hooks/useXStateMachine"; -import { createIDPayClient } from "../../common/api/client"; -import { - IDPayOnboardingParamsList, - IDPayOnboardingStackNavigationProp -} from "../navigation/navigator"; -import { createActionsImplementation } from "./actions"; -import { - IDPayOnboardingMachineType, - createIDPayOnboardingMachine -} from "./machine"; -import { createServicesImplementation } from "./services"; - -type OnboardingMachineContext = InterpreterFrom; - -const OnboardingMachineContext = React.createContext( - {} as OnboardingMachineContext -); - -type Props = { - children: React.ReactNode; -}; - -const IDPayOnboardingMachineProvider = (props: Props) => { - const dispatch = useIODispatch(); - const [machine] = useXStateMachine(createIDPayOnboardingMachine); - const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - const baseUrl = isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl; - - const sessionInfo = useIOSelector(sessionInfoSelector); - - const rootNavigation = useIONavigation(); - const onboardingNavigation = - useNavigation< - IDPayOnboardingStackNavigationProp< - IDPayOnboardingParamsList, - keyof IDPayOnboardingParamsList - > - >(); - - if (O.isNone(sessionInfo)) { - throw new Error("Session info is undefined"); - } - - const { bpdToken } = sessionInfo.value; - - const token = idPayTestToken !== undefined ? idPayTestToken : bpdToken; - - const language = pipe( - useIOSelector(preferredLanguageSelector), - O.getOrElse(getLocalePrimaryWithFallback), - fromLocaleToPreferredLanguage - ); - - const client = createIDPayClient(baseUrl); - - const services = createServicesImplementation(client, token, language); - - const actions = createActionsImplementation( - rootNavigation, - onboardingNavigation, - dispatch - ); - - const machineService = useInterpret(machine, { - services, - actions - }); - - return ( - - {props.children} - - ); -}; - -const useOnboardingMachineService = () => - React.useContext(OnboardingMachineContext); - -export { IDPayOnboardingMachineProvider, useOnboardingMachineService }; diff --git a/ts/features/idpay/unsubscription/machine/__tests__/machine.test.ts b/ts/features/idpay/unsubscription/machine/__tests__/machine.test.ts deleted file mode 100644 index 83e537c0860..00000000000 --- a/ts/features/idpay/unsubscription/machine/__tests__/machine.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* eslint-disable sonarjs/no-identical-functions */ -/* eslint-disable functional/no-let */ -import { waitFor } from "@testing-library/react-native"; -import { interpret } from "xstate"; -import { - InitiativeDTO, - StatusEnum -} from "../../../../../../definitions/idpay/InitiativeDTO"; -import { createIDPayUnsubscriptionMachine } from "../machine"; - -const T_INITIATIVE_ID = "T_INITIATIVE_ID"; -const T_INITIATIVE_NAME = "T_INITIATIVE_ID"; - -const T_INITIATIVE_DTO: InitiativeDTO = { - initiativeId: T_INITIATIVE_ID, - status: StatusEnum.NOT_REFUNDABLE, - endDate: new Date("2023-01-25T13:00:25.477Z"), - nInstr: 1 -}; - -describe("IDPay Unsubscription machine", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should transition to AWAITING_CONFIRMATION after start", () => { - // NOTE: initial state is START_UNSUBSCRIPTION but since it has an "always" transitions the transition occurs immediately - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID, - initiativeName: T_INITIATIVE_NAME - }); - - expect(machine.initialState.value).toEqual("AWAITING_CONFIRMATION"); - }); - - it("should transition to LOADING_INITIATIVE_INFO after start", () => { - // NOTE: initial state is START_UNSUBSCRIPTION but since it has an "always" transitions the transition occurs immediately - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID - }); - - expect(machine.initialState.value).toEqual("LOADING_INITIATIVE_INFO"); - }); - - it("should get initiative info if something is missing", async () => { - const mockGetInitiativeInfo = jest.fn(async () => - Promise.resolve(T_INITIATIVE_DTO) - ); - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID - }).withConfig({ - actions: { - navigateToConfirmationScreen: jest.fn(), - navigateToResultScreen: jest.fn(), - exitToWallet: jest.fn(), - exitUnsubscription: jest.fn(), - handleSessionExpired: jest.fn() - }, - services: { - getInitiativeInfo: mockGetInitiativeInfo, - unsubscribeFromInitiative: jest.fn() - } - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - await waitFor(() => expect(mockGetInitiativeInfo).toHaveBeenCalled()); - - expect(currentState.value).toEqual("AWAITING_CONFIRMATION"); - }); - - it("should allow the citizen to complete the unsubscription on happy path", async () => { - const mockUnsubscribeFromInitiative = jest.fn(async () => - Promise.resolve(undefined) - ); - - const mockNavigateToConfirmationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockExitToWallet = jest.fn(); - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID, - initiativeName: T_INITIATIVE_NAME - }).withConfig({ - actions: { - navigateToConfirmationScreen: mockNavigateToConfirmationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - exitToWallet: mockExitToWallet, - exitUnsubscription: jest.fn(), - handleSessionExpired: jest.fn() - }, - services: { - getInitiativeInfo: jest.fn(), - unsubscribeFromInitiative: mockUnsubscribeFromInitiative - } - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - await waitFor(() => - expect(mockNavigateToConfirmationScreen).toHaveBeenCalled() - ); - - expect(currentState.value).toEqual("AWAITING_CONFIRMATION"); - - service.send({ - type: "CONFIRM_UNSUBSCRIPTION" - }); - - expect(currentState.value).toEqual("UNSUBSCRIBING"); - - await waitFor(() => - expect(mockUnsubscribeFromInitiative).toHaveBeenCalled() - ); - - await waitFor(() => expect(mockNavigateToResultScreen).toHaveBeenCalled()); - - expect(currentState.value).toEqual("UNSUBSCRIPTION_SUCCESS"); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitToWallet).toHaveBeenCalled()); - }); - - it("should show failure if unsubscription fails", async () => { - const mockUnsubscribeFromInitiative = jest.fn(async () => - Promise.reject(undefined) - ); - - const mockNavigateToConfirmationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockExitUnsubscription = jest.fn(); - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID, - initiativeName: T_INITIATIVE_NAME - }).withConfig({ - actions: { - navigateToConfirmationScreen: mockNavigateToConfirmationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - exitToWallet: jest.fn(), - exitUnsubscription: mockExitUnsubscription, - handleSessionExpired: jest.fn() - }, - services: { - getInitiativeInfo: jest.fn(), - unsubscribeFromInitiative: mockUnsubscribeFromInitiative - } - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - await waitFor(() => - expect(mockNavigateToConfirmationScreen).toHaveBeenCalled() - ); - - expect(currentState.value).toEqual("AWAITING_CONFIRMATION"); - - service.send({ - type: "CONFIRM_UNSUBSCRIPTION" - }); - - expect(currentState.value).toEqual("UNSUBSCRIBING"); - - await waitFor(() => - expect(mockUnsubscribeFromInitiative).toHaveBeenCalled() - ); - - await waitFor(() => expect(mockNavigateToResultScreen).toHaveBeenCalled()); - - expect(currentState.value).toEqual("UNSUBSCRIPTION_FAILURE"); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitUnsubscription).toHaveBeenCalled()); - }); -}); diff --git a/ts/features/idpay/unsubscription/machine/__tests__/services.test.ts b/ts/features/idpay/unsubscription/machine/__tests__/services.test.ts deleted file mode 100644 index 859549504aa..00000000000 --- a/ts/features/idpay/unsubscription/machine/__tests__/services.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as E from "fp-ts/lib/Either"; -import { PreferredLanguageEnum } from "../../../../../../definitions/backend/PreferredLanguage"; -import { - InitiativeDTO, - StatusEnum -} from "../../../../../../definitions/idpay/InitiativeDTO"; -import { mockIDPayClient } from "../../../common/api/__mocks__/client"; -import { Context } from "../context"; - -import { createServicesImplementation } from "../actors"; -import { ErrorDTO } from "../../../../../../definitions/idpay/ErrorDTO"; -import { UnsubscriptionFailureEnum } from "../../types/failure"; - -const T_PREFERRED_LANGUAGE = PreferredLanguageEnum.it_IT; -const T_AUTH_TOKEN = "abc123"; - -const T_INITIATIVE_ID = "efg456"; - -const T_INITIATIVE_DTO: InitiativeDTO = { - initiativeId: T_INITIATIVE_ID, - status: StatusEnum.NOT_REFUNDABLE, - endDate: new Date("2023-01-25T13:00:25.477Z"), - nInstr: 1 -}; - -const T_CONTEXT: Context = { - initiativeId: T_INITIATIVE_ID -}; - -describe("IDPay Unsubscription machine services", () => { - const services = createServicesImplementation( - mockIDPayClient, - T_AUTH_TOKEN, - T_PREFERRED_LANGUAGE - ); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("loadInitiative", () => { - it("should fail if response status code != 200", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.getWalletDetail.mockImplementation(() => response); - - await expect(services.getInitiativeInfo(T_CONTEXT)).rejects.toBe( - UnsubscriptionFailureEnum.GENERIC - ); - - expect(mockIDPayClient.getWalletDetail).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - initiativeId: T_INITIATIVE_ID - }) - ); - }); - - it("should get initiative data", async () => { - const response: E.Either< - Error, - { status: number; value?: InitiativeDTO } - > = E.right({ status: 200, value: T_INITIATIVE_DTO }); - - mockIDPayClient.getWalletDetail.mockImplementation(() => response); - - await expect( - services.getInitiativeInfo(T_CONTEXT) - ).resolves.toMatchObject(T_INITIATIVE_DTO); - - expect(mockIDPayClient.getWalletDetail).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - initiativeId: T_INITIATIVE_ID - }) - ); - }); - }); - - describe("unsubscribeFromInitiative", () => { - it("should fail if response status code != 200", async () => { - const response: E.Either = - E.right({ status: 400, value: { code: 400, message: "" } }); - - mockIDPayClient.unsubscribe.mockImplementation(() => response); - - await expect(services.unsubscribeFromInitiative(T_CONTEXT)).rejects.toBe( - UnsubscriptionFailureEnum.GENERIC - ); - - expect(mockIDPayClient.unsubscribe).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - initiativeId: T_INITIATIVE_ID - }) - ); - }); - - it("should unsubscribe from initiative", async () => { - const response: E.Either = - E.right({ status: 204, value: undefined }); - - mockIDPayClient.unsubscribe.mockImplementation(() => response); - - await expect( - services.unsubscribeFromInitiative(T_CONTEXT) - ).resolves.toBeUndefined(); - - expect(mockIDPayClient.unsubscribe).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - "Accept-Language": T_PREFERRED_LANGUAGE, - initiativeId: T_INITIATIVE_ID - }) - ); - }); - }); -}); diff --git a/ts/features/idpay/unsubscription/machine/machine.ts b/ts/features/idpay/unsubscription/machine/machine.ts index 1ff1526f4da..f5649784f33 100644 --- a/ts/features/idpay/unsubscription/machine/machine.ts +++ b/ts/features/idpay/unsubscription/machine/machine.ts @@ -16,21 +16,11 @@ export const idPayUnsubscriptionMachine = setup({ events: {} as Events.Events }, actions: { - navigateToConfirmationScreen: () => { - throw new Error("Not implemented"); - }, - navigateToResultScreen: () => { - throw new Error("Not implemented"); - }, - exitToWallet: () => { - throw new Error("Not implemented"); - }, - exitUnsubscription: () => { - throw new Error("Not implemented"); - }, - handleSessionExpired: () => { - throw new Error("Not implemented"); - } + navigateToConfirmationScreen: notImplementedStub, + navigateToResultScreen: notImplementedStub, + exitToWallet: notImplementedStub, + exitUnsubscription: notImplementedStub, + handleSessionExpired: notImplementedStub }, actors: { onInit: fromPromise(({ input }) => @@ -45,11 +35,7 @@ export const idPayUnsubscriptionMachine = setup({ hasMissingInitiativeData: ({ context }) => context.initiativeName === undefined || context.initiativeType === undefined, - isSessionExpired: data => { - // eslint-disable-next-line no-console - console.log(data); - return false; - } + isSessionExpired: () => false } }).createMachine({ id: "idpay-unsubscription", diff --git a/ts/features/idpay/unsubscription/machine/provider.tsx b/ts/features/idpay/unsubscription/machine/provider.tsx index 55312099941..375b74c9964 100644 --- a/ts/features/idpay/unsubscription/machine/provider.tsx +++ b/ts/features/idpay/unsubscription/machine/provider.tsx @@ -3,7 +3,6 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import { idPayApiBaseUrl, idPayApiUatBaseUrl, @@ -20,24 +19,21 @@ import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; import { createActorsImplementation } from "./actors"; +import * as Input from "./input"; import { idPayUnsubscriptionMachine } from "./machine"; -export const IdPayUnsubscriptionMachineContext = createActorContext( - idPayUnsubscriptionMachine -); - type Props = { children: React.ReactNode; - initiativeId: string; - initiativeName?: string; - initiativeType?: InitiativeRewardTypeEnum; + input: Input.Input; }; -export const IDPayUnsubscriptionMachineProvider = ({ +export const IdPayUnsubscriptionMachineContext = createActorContext( + idPayUnsubscriptionMachine +); + +export const IdPayUnsubscriptionMachineProvider = ({ children, - initiativeId, - initiativeName, - initiativeType + input }: Props) => { const navigation = useIONavigation(); const dispatch = useIODispatch(); @@ -55,15 +51,17 @@ export const IDPayUnsubscriptionMachineProvider = ({ if (O.isNone(sessionInfo)) { throw new Error("Session info is undefined"); } - const { bpdToken } = sessionInfo.value; - const idPayToken = idPayTestToken ?? bpdToken; const idPayClient = createIDPayClient( isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); - const actors = createActorsImplementation(idPayClient, idPayToken, language); + const actors = createActorsImplementation( + idPayClient, + idPayTestToken ?? bpdToken, + language + ); const actions = createActionsImplementation(navigation, dispatch); const machine = idPayUnsubscriptionMachine.provide({ actors, @@ -73,7 +71,7 @@ export const IDPayUnsubscriptionMachineProvider = ({ return ( {children} diff --git a/ts/features/idpay/unsubscription/navigation/navigator.tsx b/ts/features/idpay/unsubscription/navigation/navigator.tsx index ab42e28b8ba..54d73f5a3f9 100644 --- a/ts/features/idpay/unsubscription/navigation/navigator.tsx +++ b/ts/features/idpay/unsubscription/navigation/navigator.tsx @@ -1,62 +1,42 @@ import { RouteProp, useRoute } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; +import { IdPayUnsubscriptionMachineProvider } from "../machine/provider"; import UnsubscriptionConfirmationScreen from "../screens/UnsubscriptionConfirmationScreen"; import UnsubscriptionResultScreen from "../screens/UnsubscriptionResultScreen"; -import { IDPayUnsubscriptionMachineProvider } from "../machine/provider"; -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; +import { IdPayUnsubscriptionParamsList } from "./params"; +import { IdPayUnsubscriptionRoutes } from "./routes"; -export const IDPayUnsubscriptionRoutes = { - IDPAY_UNSUBSCRIPTION_MAIN: "IDPAY_UNSUBSCRIPTION_MAIN", - IDPAY_UNSUBSCRIPTION_CONFIRMATION: "IDPAY_UNSUBSCRIPTION_CONFIRMATION", - IDPAY_UNSUBSCRIPTION_RESULT: "IDPAY_UNSUBSCRIPTION_RESULT" -} as const; +const Stack = createStackNavigator(); -export type IDPayUnsubscriptionParamsList = { - [IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN]: IDPayUnsubscriptionNavigatorParams; - [IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION]: undefined; - [IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT]: undefined; -}; - -const Stack = createStackNavigator(); - -export type IDPayUnsubscriptionNavigatorParams = { - initiativeId: string; - initiativeName?: string; - initiativeType?: InitiativeRewardTypeEnum; -}; - -type IDPayUnsubscriptionScreenRouteProps = RouteProp< - IDPayUnsubscriptionParamsList, - "IDPAY_UNSUBSCRIPTION_MAIN" +type IdPayUnsubscriptionScreenRouteProps = RouteProp< + IdPayUnsubscriptionParamsList, + "IDPAY_UNSUBSCRIPTION_NAVIGATOR" >; export const IDPayUnsubscriptionNavigator = () => { - const route = useRoute(); - - const { initiativeId, initiativeName, initiativeType } = route.params; + const { params } = useRoute(); + const { initiativeId, initiativeName, initiativeType } = params; return ( - - + ); }; diff --git a/ts/features/idpay/unsubscription/navigation/params.ts b/ts/features/idpay/unsubscription/navigation/params.ts new file mode 100644 index 00000000000..5ef8547beb1 --- /dev/null +++ b/ts/features/idpay/unsubscription/navigation/params.ts @@ -0,0 +1,14 @@ +import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; +import { IdPayUnsubscriptionRoutes } from "./routes"; + +export type IdPayUnsubscriptionNavigatorParams = { + initiativeId: string; + initiativeName?: string; + initiativeType?: InitiativeRewardTypeEnum; +}; + +export type IdPayUnsubscriptionParamsList = { + [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR]: IdPayUnsubscriptionNavigatorParams; + [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION]: undefined; + [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT]: undefined; +}; diff --git a/ts/features/idpay/unsubscription/navigation/routes.ts b/ts/features/idpay/unsubscription/navigation/routes.ts new file mode 100644 index 00000000000..a688af9915f --- /dev/null +++ b/ts/features/idpay/unsubscription/navigation/routes.ts @@ -0,0 +1,5 @@ +export const IdPayUnsubscriptionRoutes = { + IDPAY_UNSUBSCRIPTION_NAVIGATOR: "IDPAY_UNSUBSCRIPTION_NAVIGATOR", + IDPAY_UNSUBSCRIPTION_CONFIRMATION: "IDPAY_UNSUBSCRIPTION_CONFIRMATION", + IDPAY_UNSUBSCRIPTION_RESULT: "IDPAY_UNSUBSCRIPTION_RESULT" +} as const; diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index a13bd475ece..f3af526cb26 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -31,7 +31,7 @@ import { IDPayDetailsRoutes } from "../features/idpay/details/navigation"; import { - IDPayOnboardingNavigator, + IdPayOnboardingNavigator, IDPayOnboardingRoutes } from "../features/idpay/onboarding/navigation/navigator"; import { @@ -249,7 +249,7 @@ const AuthenticatedStackNavigator = () => { <> ; [FCI_ROUTES.MAIN]: NavigatorScreenParams; - [IDPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN]: NavigatorScreenParams; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: + | IdPayOnboardingNavigatorParams + | NavigatorScreenParams; [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN]: NavigatorScreenParams; [IDPayDetailsRoutes.IDPAY_DETAILS_MAIN]: NavigatorScreenParams; - [IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN]: - | NavigatorScreenParams - | IDPayUnsubscriptionNavigatorParams; + [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR]: + | IdPayUnsubscriptionNavigatorParams + | NavigatorScreenParams; [IDPayPaymentRoutes.IDPAY_PAYMENT_CODE_SCAN]: undefined; // FIXME IOBP-383: remove after react-navigation 6.x upgrade. This should be insde IDPAY_PAYMENT_MAIN [IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: NavigatorScreenParams; [IdPayCodeRoutes.IDPAY_CODE_MAIN]: NavigatorScreenParams; diff --git a/ts/xstate/helpers/guardedNavigationAction.ts b/ts/xstate/helpers/guardedNavigationAction.ts index aa24bed3c12..786293889af 100644 --- a/ts/xstate/helpers/guardedNavigationAction.ts +++ b/ts/xstate/helpers/guardedNavigationAction.ts @@ -1,6 +1,6 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { DefaultContext } from "xstate"; +import { MachineContext } from "xstate"; import { E_BACK } from "../types/events"; type WrappedAction = (context: TContext, event: any) => void; @@ -27,7 +27,7 @@ const skipNavigation = (event: E_BACK) => event.skipNavigation || false; * and either skips the action or calls the original action based on the event. */ export const guardedNavigationAction = - (action: WrappedAction) => + (action: WrappedAction) => (context: TContext, event: any) => pipe( event, From 5bf9dedb18b95f15404ae37d0173fb9fc5e95ab4 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Tue, 30 Apr 2024 17:23:07 +0200 Subject: [PATCH 05/31] chore: refactor onboarding machine --- package.json | 2 +- .../idpay/configuration/xstate/machine.ts | 134 ++---------------- .../components/OnboardingServiceHeader.tsx | 3 +- .../idpay/onboarding/machine/actions.ts | 50 ++++--- .../idpay/onboarding/machine/actors.ts | 16 ++- .../idpay/onboarding/machine/events.ts | 17 +-- .../idpay/onboarding/machine/machine.ts | 15 +- .../idpay/onboarding/machine/selectors.ts | 45 +++--- .../idpay/onboarding/navigation/navigator.tsx | 2 +- .../screens/BoolValuePrerequisitesScreen.tsx | 34 ++--- .../onboarding/screens/CompletionScreen.tsx | 24 ++-- .../onboarding/screens/FailureScreen.tsx | 17 +-- .../screens/InitiativeDetailsScreen.tsx | 42 ++---- .../screens/MultiValuePrerequisitesScreen.tsx | 26 ++-- .../screens/PDNDPrerequisitesScreen.tsx | 34 ++--- ts/features/idpay/payment/xstate/machine.ts | 2 +- ts/xstate/helpers/guardedNavigationAction.ts | 19 +-- ts/xstate/types/events.ts | 17 ++- yarn.lock | 2 +- 19 files changed, 182 insertions(+), 319 deletions(-) diff --git a/package.json b/package.json index 76506f0a513..be63fd10c83 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@react-navigation/native": "6.1.9", "@react-navigation/stack": "6.3.20", "@redux-saga/testing-utils": "^1.1.3", - "@xstate/react": "npm:@xstate/react@4", + "@xstate/react": "^4.0.3", "@xstate4/react": "npm:@xstate/react@3.0.1", "async-mutex": "^0.1.3", "buffer": "^4.9.1", diff --git a/ts/features/idpay/configuration/xstate/machine.ts b/ts/features/idpay/configuration/xstate/machine.ts index ea119620718..d9f387411a0 100644 --- a/ts/features/idpay/configuration/xstate/machine.ts +++ b/ts/features/idpay/configuration/xstate/machine.ts @@ -2,7 +2,7 @@ import * as p from "@pagopa/ts-commons/lib/pot"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { assign, createMachine, forwardTo } from "xstate"; +import { assign, createMachine, forwardTo } from "xstate4"; import { IbanListDTO } from "../../../../../definitions/idpay/IbanListDTO"; import { InitiativeDTO, @@ -810,42 +810,14 @@ const createIDPayInitiativeConfigurationMachine = () => }, { actions: { - startConfiguration: assign((_, event) => ({ - initiativeId: event.initiativeId, - mode: event.mode - })), - loadInitiativeSuccess: assign((_, event) => ({ - initiative: p.some(event.data), - failure: undefined - })), - loadIbanListSuccess: assign((_, event) => ({ - ibanList: p.some(event.data.ibanList), - failure: undefined - })), - selectIban: assign((_, event) => ({ - selectedIban: event.iban, - failure: undefined - })), - enrollIbanSuccess: assign(context => ({ - initiative: p.map(context.initiative, initiative => ({ - ...initiative, - iban: context.selectedIban?.iban - })), - selectedIban: undefined, - failure: undefined - })), - confirmIbanOnboarding: assign((_, event) => ({ - ibanBody: event.ibanBody, - failure: undefined - })), - loadWalletInstrumentsSuccess: assign((_, event) => ({ - walletInstruments: event.data, - failure: undefined - })), - loadInitiativeInstrumentsSuccess: assign((_, event) => ({ - initiativeInstruments: event.data, - failure: undefined - })), + startConfiguration: assign((_, event) => ({})), + loadInitiativeSuccess: assign((_, event) => ({})), + loadIbanListSuccess: assign((_, event) => ({})), + selectIban: assign((_, event) => ({})), + enrollIbanSuccess: assign(context => ({})), + confirmIbanOnboarding: assign((_, event) => ({})), + loadWalletInstrumentsSuccess: assign((_, event) => ({})), + loadInitiativeInstrumentsSuccess: assign((_, event) => ({})), updateInstrumentStatuses: assign((context, _) => { const updatedStatuses = context.initiativeInstruments.reduce( @@ -876,88 +848,12 @@ const createIDPayInitiativeConfigurationMachine = () => forwardToInstrumentsEnrollmentService: forwardTo( "instrumentsEnrollmentService" ), - updateInstrumentEnrollStatus: assign((context, event) => ({ - instrumentStatuses: { - ...context.instrumentStatuses, - [event.walletId]: p.noneLoading - } - })), - updateInstrumentEnrollStatusSuccess: assign((context, event) => { - const currentEnrollStatus = - context.instrumentStatuses[event.walletId]; - - if (p.isSome(currentEnrollStatus)) { - // No need to update instrument status - return {}; - } - - return { - instrumentStatuses: { - ...context.instrumentStatuses, - [event.walletId]: p.some( - InstrumentStatusEnum.PENDING_ENROLLMENT_REQUEST - ) - } - }; - }), - updateInstrumentEnrollStatusFailure: assign((context, event) => { - if (event.walletId === undefined) { - return {}; - } - - const { [event.walletId]: _removedStatus, ...updatedStatuses } = - context.instrumentStatuses; - - return { - instrumentStatuses: updatedStatuses - }; - }), - updateInstrumentDeleteStatus: assign((context, event) => { - if (event.walletId === undefined) { - return {}; - } - - return { - instrumentStatuses: { - ...context.instrumentStatuses, - [event.walletId]: p.noneLoading - } - }; - }), - updateInstrumentDeleteStatusSuccess: assign((context, event) => { - if (event.walletId === undefined) { - return {}; - } - - const currentDeleteStatus = - context.instrumentStatuses[event.walletId]; - - if (p.isSome(currentDeleteStatus)) { - // No need to update instrument status - return {}; - } - - return { - instrumentStatuses: { - ...context.instrumentStatuses, - [event.walletId]: p.some( - InstrumentStatusEnum.PENDING_DEACTIVATION_REQUEST - ) - } - }; - }), - updateInstrumentDeleteStatusFailure: assign((context, event) => { - if (event.walletId === undefined) { - return {}; - } - - return { - instrumentStatuses: { - ...context.instrumentStatuses, - [event.walletId]: p.some(InstrumentStatusEnum.ACTIVE) - } - }; - }), + updateInstrumentEnrollStatus: assign((context, event) => ({})), + updateInstrumentEnrollStatusSuccess: assign((context, event) => ({})), + updateInstrumentEnrollStatusFailure: assign((context, event) => ({})), + updateInstrumentDeleteStatus: assign((context, event) => ({})), + updateInstrumentDeleteStatusSuccess: assign((context, event) => ({})), + updateInstrumentDeleteStatusFailure: assign((context, event) => ({})), skipInstruments: assign((_, __) => ({ areInstrumentsSkipped: true })), diff --git a/ts/features/idpay/onboarding/components/OnboardingServiceHeader.tsx b/ts/features/idpay/onboarding/components/OnboardingServiceHeader.tsx index c5e13b46823..9057f86ab36 100644 --- a/ts/features/idpay/onboarding/components/OnboardingServiceHeader.tsx +++ b/ts/features/idpay/onboarding/components/OnboardingServiceHeader.tsx @@ -9,7 +9,7 @@ import { Body } from "../../../../components/core/typography/Body"; import { H2 } from "../../../../components/core/typography/H2"; type Props = { - initiative?: InitiativeDataDTO; + initiative: O.Option; }; const OnboardingServiceHeader = (props: Props) => { @@ -17,7 +17,6 @@ const OnboardingServiceHeader = (props: Props) => { return pipe( initiative, - O.fromNullable, O.map(initiative => ({ organizationName: initiative.organizationName, initiativeName: initiative.initiativeName, diff --git a/ts/features/idpay/onboarding/machine/actions.ts b/ts/features/idpay/onboarding/machine/actions.ts index b9d36578db5..82f24f218ae 100644 --- a/ts/features/idpay/onboarding/machine/actions.ts +++ b/ts/features/idpay/onboarding/machine/actions.ts @@ -11,44 +11,34 @@ const createActionsImplementation = ( navigation: ReturnType, dispatch: ReturnType ) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - const navigateToInitiativeDetailsScreen = guardedNavigationAction(() => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS }) ); - const navigateToPDNDCriteriaScreen = guardedNavigationAction(() => + const navigateToPdndCriteriaScreen = guardedNavigationAction(() => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE }) ); - const navigateToBoolSelfDeclarationsScreen = guardedNavigationAction(() => + const navigateToBoolSelfDeclarationListScreen = guardedNavigationAction(() => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS }) ); - const navigateToMultiSelfDeclarationsScreen = guardedNavigationAction( - (context: Context.Context) => + const navigateToMultiSelfDeclarationListScreen = + guardedNavigationAction(({ context }) => navigation.navigate({ name: IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, params: { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS }, - key: String(context.multiConsentsPage) + key: String(context.selfDeclarationsMultiPage) }) - ); + ); const navigateToCompletionScreen = () => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { @@ -60,32 +50,46 @@ const createActionsImplementation = ( screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE }); - const navigateToInitiativeMonitoringScreen = (context: Context.Context) => { - if (O.isNone(context.initiative)) { + const navigateToInitiativeMonitoringScreen = (args: { + context: Context.Context; + }) => { + if (O.isNone(args.context.initiative)) { throw new Error("Initiative is undefined"); } + const initiativeId = args.context.initiative.value.initiativeId; + navigation.replace(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, params: { - initiativeId: context.initiative.value.initiativeId + initiativeId } }); }; + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + const closeOnboarding = () => { navigation.popToTop(); }; return { - handleSessionExpired, navigateToInitiativeDetailsScreen, - navigateToPDNDCriteriaScreen, - navigateToBoolSelfDeclarationsScreen, - navigateToMultiSelfDeclarationsScreen, + navigateToPdndCriteriaScreen, + navigateToBoolSelfDeclarationListScreen, + navigateToMultiSelfDeclarationListScreen, navigateToCompletionScreen, navigateToFailureScreen, navigateToInitiativeMonitoringScreen, + handleSessionExpired, closeOnboarding }; }; diff --git a/ts/features/idpay/onboarding/machine/actors.ts b/ts/features/idpay/onboarding/machine/actors.ts index b2a3eb8f615..5b821d49842 100644 --- a/ts/features/idpay/onboarding/machine/actors.ts +++ b/ts/features/idpay/onboarding/machine/actors.ts @@ -15,7 +15,7 @@ import { OnboardingFailureEnum } from "../types/OnboardingFailure"; import * as Context from "./context"; -import { getBoolRequiredCriteriaFromContext } from "./selectors"; +import { getBooleanSelfDeclarationListFromContext } from "./selectors"; /** * Maps the status of the initiative to a possibile UI failure state @@ -188,12 +188,16 @@ const createActorsImplementation = ( const getRequiredCriteria = fromPromise< O.Option, - string + O.Option >(async params => { + if (O.isNone(params.input)) { + throw new Error("Initiative ID was not provided"); + } + const response = await client.checkPrerequisites({ ...clientOptions, body: { - initiativeId: params.input + initiativeId: params.input.value } }); @@ -223,7 +227,7 @@ const createActorsImplementation = ( const acceptRequiredCriteria = fromPromise( async params => { - const { initiative, requiredCriteria, multiConsentsAnswers } = + const { initiative, requiredCriteria, selfDeclarationsMultiAnwsers } = params.input; if (O.isNone(initiative) || O.isNone(requiredCriteria)) { @@ -235,12 +239,12 @@ const createActorsImplementation = ( } const consentsArray = [ - ...getBoolRequiredCriteriaFromContext(params.input).map(_ => ({ + ...getBooleanSelfDeclarationListFromContext(params.input).map(_ => ({ _type: _._type, code: _.code, accepted: true })), - ...Object.values(multiConsentsAnswers) + ...Object.values(selfDeclarationsMultiAnwsers) ] as Array; const response = await client.consentOnboarding({ diff --git a/ts/features/idpay/onboarding/machine/events.ts b/ts/features/idpay/onboarding/machine/events.ts index 49f7df1df7c..c449aba6ce9 100644 --- a/ts/features/idpay/onboarding/machine/events.ts +++ b/ts/features/idpay/onboarding/machine/events.ts @@ -1,5 +1,6 @@ import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; +import { GlobalEvents } from "../../../../xstate/types/events"; import * as Input from "./input"; export interface AutoInit { @@ -17,22 +18,8 @@ export interface SelectMultiConsent { readonly data: SelfConsentMultiDTO; } -export interface Next { - readonly type: "next"; -} - -export interface Back { - readonly type: "back"; -} - -export interface Close { - readonly type: "close"; -} - export type Events = | AutoInit | SelectMultiConsent | ToggleBoolCriteria - | Back - | Next - | Close; + | GlobalEvents; diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index 9722fb00309..2825a13c985 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -90,7 +90,7 @@ export const idPayOnboardingMachine = setup({ }, onDone: { actions: assign(event => ({ ...event.event.output })), - target: ".LoadingInitiativeInfo" + target: ".LoadingInitiative" } }, initial: "LoadingInitiative", @@ -103,6 +103,7 @@ export const idPayOnboardingMachine = setup({ LoadingInitiative: { tags: [LOADING_TAG], entry: "navigateToInitiativeDetailsScreen", + initial: "LoadingInitiativeInfo", states: { LoadingInitiativeInfo: { invoke: { @@ -112,7 +113,7 @@ export const idPayOnboardingMachine = setup({ actions: assign(({ event }) => ({ initiative: O.some(event.output) })), - target: ".LoadingInitiative.LoadingOnboardingStatus" + target: "LoadingOnboardingStatus" } } }, @@ -225,7 +226,7 @@ export const idPayOnboardingMachine = setup({ } ], back: { - target: "DisplayingInitiativeInfo" + target: "#idpay-onboarding.DisplayingInitiativeInfo" } } }, @@ -254,10 +255,10 @@ export const idPayOnboardingMachine = setup({ back: [ { guard: "hasPdndCriteria", - target: "DisplayingPdndCriteria" + target: "#idpay-onboarding.DisplayingPdndCriteria" }, { - target: "DisplayingInitiativeInfo" + target: "#idpay-onboarding.DisplayingInitiativeInfo" } ], "toggle-bool-criteria": { @@ -274,7 +275,7 @@ export const idPayOnboardingMachine = setup({ target: "DisplayingMultiSelfDeclarationList" }, { - target: "AcceptingCriteria" + target: "#idpay-onboarding.AcceptingCriteria" } ] } @@ -302,7 +303,7 @@ export const idPayOnboardingMachine = setup({ always: [ { guard: "isLastMultiConsent", - target: "AcceptingCriteria" + target: "#idpay-onboarding.AcceptingCriteria" }, { actions: assign(({ context }) => ({ diff --git a/ts/features/idpay/onboarding/machine/selectors.ts b/ts/features/idpay/onboarding/machine/selectors.ts index bbe13c6a7a9..3da8d688a27 100644 --- a/ts/features/idpay/onboarding/machine/selectors.ts +++ b/ts/features/idpay/onboarding/machine/selectors.ts @@ -1,42 +1,40 @@ /* eslint-disable no-underscore-dangle */ -import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; import { StateFrom } from "xstate"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; import { SelfDeclarationDTO } from "../../../../../definitions/idpay/SelfDeclarationDTO"; import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDeclarationMultiDTO"; -import { LOADING_TAG, UPSERTING_TAG } from "../../../../xstate/utils"; -import { IdPayOnboardingMachine } from "./machine"; +import { LOADING_TAG } from "../../../../xstate/utils"; import * as Context from "./context"; +import { IdPayOnboardingMachine } from "./machine"; type StateWithContext = StateFrom; -const selectInitiativeStatus = (state: StateWithContext) => - state.context.onboardingStatus; - -const selectOnboardingFailure = (state: StateWithContext) => +export const selectOnboardingFailure = (state: StateWithContext) => state.context.failure; const selectRequiredCriteria = (state: StateWithContext) => state.context.requiredCriteria; -const selectSelfDeclarationBoolAnswers = (state: StateWithContext) => - state.context.selfDeclarationBoolAnswers; +export const selectSelfDeclarationBoolAnswers = (state: StateWithContext) => + state.context.selfDeclarationsBoolAnswers; const selectMultiConsents = (state: StateWithContext) => - state.context.multiConsentsAnswers; + state.context.selfDeclarationsMultiAnwsers; const selectCurrentPage = (state: StateWithContext) => - state.context.multiConsentsPage; + state.context.selfDeclarationsMultiPage; const selectTags = (state: StateWithContext) => state.tags; export const selectInitiative = (state: StateWithContext) => state.context.initiative; -const selectServiceId = (state: StateWithContext) => state.context.serviceId; +export const selectServiceId = (state: StateWithContext) => + state.context.serviceId; const filterCriteria = ( criteria: O.Option, @@ -59,7 +57,7 @@ const multiRequiredCriteriaSelector = createSelector( ) ); -const boolRequiredCriteriaSelector = createSelector( +export const boolRequiredCriteriaSelector = createSelector( selectRequiredCriteria, requiredCriteria => filterCriteria( @@ -68,13 +66,13 @@ const boolRequiredCriteriaSelector = createSelector( ) ); -const criteriaToDisplaySelector = createSelector( +export const criteriaToDisplaySelector = createSelector( multiRequiredCriteriaSelector, selectCurrentPage, (criteria, currentPage) => criteria[currentPage] ); -const pdndCriteriaSelector = createSelector( +export const pdndCriteriaSelector = createSelector( selectRequiredCriteria, requiredCriteria => pipe( @@ -86,7 +84,7 @@ const pdndCriteriaSelector = createSelector( ) ); -const prerequisiteAnswerIndexSelector = createSelector( +export const prerequisiteAnswerIndexSelector = createSelector( criteriaToDisplaySelector, selectMultiConsents, selectCurrentPage, @@ -96,20 +94,9 @@ const prerequisiteAnswerIndexSelector = createSelector( : currentCriteria.value.indexOf(multiConsents[currentPage]?.value) ); -const isLoadingSelector = createSelector(selectTags, tags => +export const isLoadingSelector = createSelector(selectTags, tags => tags.has(LOADING_TAG) ); -const isUpsertingSelector = createSelector(selectTags, tags => - tags.has(UPSERTING_TAG) -); - -const initiativeIDSelector = createSelector(selectInitiative, initiative => - pipe( - initiative, - O.map(initiative => initiative.initiativeId), - O.toUndefined - ) -); export const getMultiSelfDeclarationListFromContext = ( context: Context.Context @@ -127,7 +114,7 @@ export const getBooleanSelfDeclarationListFromContext = ( SelfDeclarationBoolDTO ); -const areAllSelfDeclarationsToggledSelector = createSelector( +export const areAllSelfDeclarationsToggledSelector = createSelector( boolRequiredCriteriaSelector, selectSelfDeclarationBoolAnswers, (boolSelfDeclarations, answers) => diff --git a/ts/features/idpay/onboarding/navigation/navigator.tsx b/ts/features/idpay/onboarding/navigation/navigator.tsx index bdf6a3559ff..4721a56da10 100644 --- a/ts/features/idpay/onboarding/navigation/navigator.tsx +++ b/ts/features/idpay/onboarding/navigation/navigator.tsx @@ -6,7 +6,7 @@ import { IdPayOnboardingMachineProvider } from "../machine/provider"; import BoolValuePrerequisitesScreen from "../screens/BoolValuePrerequisitesScreen"; import CompletionScreen from "../screens/CompletionScreen"; import FailureScreen from "../screens/FailureScreen"; -import InitiativeDetailsScreen from "../screens/InitiativeDetailsScreen"; +import { InitiativeDetailsScreen } from "../screens/InitiativeDetailsScreen"; import MultiValuePrerequisitesScreen from "../screens/MultiValuePrerequisitesScreen"; import PDNDPrerequisitesScreen from "../screens/PDNDPrerequisitesScreen"; import { IdPayOnboardingParamsList } from "./params"; diff --git a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx index ad71b71ddb4..7078d5dc9b8 100644 --- a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx @@ -1,54 +1,48 @@ -import { useSelector } from "@xstate/react"; +import { VSpacer } from "@pagopa/io-app-design-system"; import React from "react"; import { SafeAreaView, View } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; -import { VSpacer } from "@pagopa/io-app-design-system"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; +import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import { Body } from "../../../../components/core/typography/Body"; import { H1 } from "../../../../components/core/typography/H1"; import { Link } from "../../../../components/core/typography/Link"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import ListItemComponent from "../../../../components/screens/ListItemComponent"; import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; +import { dpr28Dec2000Url } from "../../../../urls"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { useOnboardingMachineService } from "../machine/provider"; +import { openWebUrl } from "../../../../utils/url"; +import { IdPayOnboardingMachineContext } from "../machine/provider"; import { areAllSelfDeclarationsToggledSelector, boolRequiredCriteriaSelector, isLoadingSelector, selectSelfDeclarationBoolAnswers } from "../machine/selectors"; -import { openWebUrl } from "../../../../utils/url"; -import { dpr28Dec2000Url } from "../../../../urls"; const InitiativeSelfDeclarationsScreen = () => { - const machine = useOnboardingMachineService(); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); - const isLoading = useSelector(machine, isLoadingSelector); + const isLoading = useSelector(isLoadingSelector); - const selfCriteriaBool = useSelector(machine, boolRequiredCriteriaSelector); - const selfCriteriaBoolAnswers = useSelector( - machine, - selectSelfDeclarationBoolAnswers - ); + const selfCriteriaBool = useSelector(boolRequiredCriteriaSelector); + const selfCriteriaBoolAnswers = useSelector(selectSelfDeclarationBoolAnswers); const areAllSelfCriteriaBoolAccepted = useSelector( - machine, areAllSelfDeclarationsToggledSelector ); - const continueOnPress = () => - machine.send({ type: "ACCEPT_REQUIRED_BOOL_CRITERIA" }); - - const goBackOnPress = () => machine.send({ type: "BACK" }); + const continueOnPress = () => machine.send({ type: "next" }); + const goBackOnPress = () => machine.send({ type: "back" }); const toggleCriteria = (criteria: SelfDeclarationBoolDTO) => (value: boolean) => machine.send({ - type: "TOGGLE_BOOL_CRITERIA", + type: "toggle-bool-criteria", criteria: { ...criteria, value } }); @@ -56,7 +50,7 @@ const InitiativeSelfDeclarationsScreen = () => { selfCriteriaBoolAnswers[criteria.code] ?? false; useNavigationSwipeBackListener(() => { - machine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); return ( diff --git a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx index d8485e23afd..e4406111a31 100644 --- a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx +++ b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx @@ -1,30 +1,26 @@ -import { useSelector } from "@xstate/react"; +import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; import React from "react"; -import { SafeAreaView, View, StyleSheet } from "react-native"; -import { VSpacer, Pictogram } from "@pagopa/io-app-design-system"; +import { SafeAreaView, StyleSheet, View } from "react-native"; +import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import { Body } from "../../../../components/core/typography/Body"; import { H3 } from "../../../../components/core/typography/H3"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import I18n from "../../../../i18n"; -import { useOnboardingMachineService } from "../machine/provider"; -import { isUpsertingSelector } from "../machine/selectors"; import themeVariables from "../../../../theme/variables"; +import { IdPayOnboardingMachineContext } from "../machine/provider"; +import { isLoadingSelector } from "../machine/selectors"; const CompletionScreen = () => { - const onboardingMachineService = useOnboardingMachineService(); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); - const isUpserting = useSelector( - onboardingMachineService, - isUpsertingSelector - ); + const isLoading = useSelector(isLoadingSelector); - const handleClosePress = () => - onboardingMachineService.send({ type: "QUIT_ONBOARDING" }); + const handleClosePress = () => machine.send({ type: "close" }); - if (isUpserting) { + if (isLoading) { return ( { - const machine = useOnboardingMachineService(); - const failureOption = useSelector(machine, selectOnboardingFailure); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); + + const failureOption = useSelector(selectOnboardingFailure); const defaultCloseAction = React.useMemo( () => ({ label: I18n.t("global.buttons.close"), accessibilityLabel: I18n.t("global.buttons.close"), - onPress: () => machine.send({ type: "QUIT_ONBOARDING" }) + onPress: () => machine.send({ type: "close" }) }), [machine] ); @@ -30,7 +31,7 @@ const FailureScreen = () => { accessibilityLabel: I18n.t( "idpay.onboarding.failure.button.goToInitiative" ), - onPress: () => machine.send({ type: "SHOW_INITIATIVE_DETAILS" }) + onPress: () => machine.send({ type: "next" }) }), [machine] ); diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 2b07237f5b1..537aea7e92f 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -1,13 +1,11 @@ /* eslint-disable functional/immutable-data */ -import { RouteProp, useRoute } from "@react-navigation/native"; -import { useSelector } from "@xstate/react"; +import { VSpacer } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; import { StyleSheet, View } from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; -import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import { ForceScrollDownView } from "../../../../components/ForceScrollDownView"; +import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import BlockButtons from "../../../../components/ui/BlockButtons"; import I18n from "../../../../i18n"; @@ -18,31 +16,22 @@ import { } from "../components/OnboardingDescriptionMarkdown"; import { OnboardingPrivacyAdvice } from "../components/OnboardingPrivacyAdvice"; import { OnboardingServiceHeader } from "../components/OnboardingServiceHeader"; -import { isUpsertingSelector, selectInitiative } from "../machine/selectors"; - -const InitiativeDetailsScreen = () => { - const machine = useOnboardingMachineService(); +import { IdPayOnboardingMachineContext } from "../machine/provider"; +import { isLoadingSelector, selectInitiative } from "../machine/selectors"; - const { serviceId } = route.params; +export const InitiativeDetailsScreen = () => { + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); - React.useEffect(() => { - machine.send({ - type: "SELECT_INITIATIVE", - serviceId - }); - }, [machine, serviceId]); - - const initiative = useSelector(machine, selectInitiative); - const isUpserting = useSelector(machine, isUpsertingSelector); + const initiative = useSelector(selectInitiative); + const isLoading = useSelector(isLoadingSelector); const [isDescriptionLoaded, setDescriptionLoaded] = React.useState(false); - const handleGoBackPress = () => machine.send({ type: "QUIT_ONBOARDING" }); - - const handleContinuePress = () => machine.send({ type: "ACCEPT_TOS" }); + const handleGoBackPress = () => machine.send({ type: "close" }); + const handleContinuePress = () => machine.send({ type: "next" }); const onboardingPrivacyAdvice = pipe( initiative, - O.fromNullable, O.map(initiative => ({ privacyUrl: initiative.privacyLink, tosUrl: initiative.tcLink @@ -55,7 +44,6 @@ const InitiativeDetailsScreen = () => { const descriptionComponent = pipe( initiative, - O.fromNullable, O.fold( () => , ({ description }) => ( @@ -96,8 +84,8 @@ const InitiativeDetailsScreen = () => { accessibilityLabel: I18n.t("global.buttons.continue"), onPress: handleContinuePress, testID: "IDPayOnboardingContinue", - isLoading: isUpserting, - disabled: isUpserting + isLoading, + disabled: isLoading }} /> @@ -116,7 +104,3 @@ const styles = StyleSheet.create({ paddingHorizontal: 24 } }); - -export type { InitiativeDetailsScreenRouteParams }; - -export default InitiativeDetailsScreen; diff --git a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx index c2c1db45620..6eadd11f62b 100644 --- a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx @@ -1,7 +1,4 @@ /* eslint-disable no-underscore-dangle */ -import { useSelector } from "@xstate/react"; -import React from "react"; -import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import { Body, H1, @@ -11,17 +8,19 @@ import { PressableListItemBase, VSpacer } from "@pagopa/io-app-design-system"; +import React from "react"; +import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import { H4 } from "../../../../components/core/typography/H4"; +import { Link } from "../../../../components/core/typography/Link"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; -import { useOnboardingMachineService } from "../machine/provider"; +import { IdPayOnboardingMachineContext } from "../machine/provider"; import { criteriaToDisplaySelector, prerequisiteAnswerIndexSelector } from "../machine/selectors"; -import { Link } from "../../../../components/core/typography/Link"; type ListItemProps = { text: string; @@ -57,13 +56,11 @@ const buttonProps = { }; const MultiValuePrerequisitesScreen = () => { - const machine = useOnboardingMachineService(); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); - const currentPrerequisite = useSelector(machine, criteriaToDisplaySelector); - const possiblySelectedIndex = useSelector( - machine, - prerequisiteAnswerIndexSelector - ); + const currentPrerequisite = useSelector(criteriaToDisplaySelector); + const possiblySelectedIndex = useSelector(prerequisiteAnswerIndexSelector); const [selectedIndex, setSelectedIndex] = React.useState( possiblySelectedIndex @@ -73,7 +70,8 @@ const MultiValuePrerequisitesScreen = () => { if (selectedIndex === undefined) { return null; } - machine.send("SELECT_MULTI_CONSENT", { + machine.send({ + type: "select-multi-consent", data: { _type: currentPrerequisite._type, value: currentPrerequisite.value[selectedIndex], @@ -83,10 +81,10 @@ const MultiValuePrerequisitesScreen = () => { return null; }; - const goBack = () => machine.send("BACK"); + const goBack = () => machine.send({ type: "back" }); useNavigationSwipeBackListener(() => { - machine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); return ( diff --git a/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx index 76ea6de9bed..862c91fe2d2 100644 --- a/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/PDNDPrerequisitesScreen.tsx @@ -1,16 +1,15 @@ +import { + ButtonExtendedOutline, + ButtonSolid, + ContentWrapper, + VSpacer +} from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useSelector } from "@xstate/react"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; import { ScrollView, StyleSheet, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { - ButtonSolid, - VSpacer, - ContentWrapper, - ButtonExtendedOutline -} from "@pagopa/io-app-design-system"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { Body } from "../../../../components/core/typography/Body"; import { H1 } from "../../../../components/core/typography/H1"; @@ -21,12 +20,12 @@ import LegacyMarkdown from "../../../../components/ui/Markdown/LegacyMarkdown"; import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; -import { serviceByIdPotSelector } from "../../../services/details/store/reducers/servicesById"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; -import { getPDNDCriteriaDescription } from "../utils/strings"; -import { useOnboardingMachineService } from "../machine/provider"; +import { serviceByIdPotSelector } from "../../../services/details/store/reducers/servicesById"; +import { IdPayOnboardingMachineContext } from "../machine/provider"; import { pdndCriteriaSelector, selectServiceId } from "../machine/selectors"; +import { getPDNDCriteriaDescription } from "../utils/strings"; const secondaryButtonProps = { block: true, @@ -46,9 +45,11 @@ const styles = StyleSheet.create({ }); export const PDNDPrerequisitesScreen = () => { - const machine = useOnboardingMachineService(); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); + const [authority, setAuthority] = React.useState(); - const serviceId = useSelector(machine, selectServiceId); + const serviceId = useSelector(selectServiceId); const serviceName = pipe( useIOSelector(state => @@ -61,9 +62,8 @@ export const PDNDPrerequisitesScreen = () => { ) ); - const continueOnPress = () => - machine.send({ type: "ACCEPT_REQUIRED_PDND_CRITERIA" }); - const goBackOnPress = () => machine.send({ type: "BACK" }); + const continueOnPress = () => machine.send({ type: "next" }); + const goBackOnPress = () => machine.send({ type: "back" }); const { present, bottomSheet, dismiss } = useIOBottomSheetAutoresizableModal( { @@ -100,10 +100,10 @@ export const PDNDPrerequisitesScreen = () => { 162 ); - const pdndCriteria = useSelector(machine, pdndCriteriaSelector); + const pdndCriteria = useSelector(pdndCriteriaSelector); useNavigationSwipeBackListener(() => { - machine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); return ( diff --git a/ts/features/idpay/payment/xstate/machine.ts b/ts/features/idpay/payment/xstate/machine.ts index 39e10a005fb..b3f30e93fce 100644 --- a/ts/features/idpay/payment/xstate/machine.ts +++ b/ts/features/idpay/payment/xstate/machine.ts @@ -1,6 +1,6 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { assign, createMachine } from "xstate"; +import { assign, createMachine } from "xstate4"; import { LOADING_TAG, WAITING_USER_INPUT_TAG } from "../../../../xstate/utils"; import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; import { Context, INITIAL_CONTEXT } from "./context"; diff --git a/ts/xstate/helpers/guardedNavigationAction.ts b/ts/xstate/helpers/guardedNavigationAction.ts index 786293889af..5737cfb795f 100644 --- a/ts/xstate/helpers/guardedNavigationAction.ts +++ b/ts/xstate/helpers/guardedNavigationAction.ts @@ -1,24 +1,27 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { MachineContext } from "xstate"; -import { E_BACK } from "../types/events"; +import { Back } from "../types/events"; -type WrappedAction = (context: TContext, event: any) => void; -type Events = { type: string } | E_BACK; +type WrappedAction = (args: { + context: TContext; + event: any; +}) => void; +type Events = { type: string } | Back; /** * Checks if the event is of type E_BACK * @param event The event object to check. * @returns True if the event is of type E_BACK, false otherwise. */ -const isBack = (event: Events): event is E_BACK => event.type === "BACK"; +const isBack = (event: Events): event is Back => event.type === "BACK"; /** * Checks if an E_BACK event should skip the navigation action. * @param event The event object to check. * @returns True if the event has a skipNavigation property set to true; otherwise, false. */ -const skipNavigation = (event: E_BACK) => event.skipNavigation || false; +const skipNavigation = (event: Back) => event.skipNavigation || false; /** * Wrap an action function with a guard clause that checks whether the event should be skipped. @@ -28,9 +31,9 @@ const skipNavigation = (event: E_BACK) => event.skipNavigation || false; */ export const guardedNavigationAction = (action: WrappedAction) => - (context: TContext, event: any) => + (args: { context: TContext; event: any }) => pipe( - event, + args.event, O.of, O.filter(isBack), O.filter(skipNavigation), @@ -39,7 +42,7 @@ export const guardedNavigationAction = * The event is not of type E_BACK and/or does not contain the skipNavigation property. * WrappedAction should be executed. */ - () => action(context, event), + () => action(args), /** * The event is of type E_BACK and contains the skipNavigation property. * No actions should be executed. diff --git a/ts/xstate/types/events.ts b/ts/xstate/types/events.ts index bbcf3362310..a7ef155f203 100644 --- a/ts/xstate/types/events.ts +++ b/ts/xstate/types/events.ts @@ -1,4 +1,13 @@ -export type E_BACK = { - type: "BACK"; - skipNavigation?: boolean; -}; +export interface Next { + readonly type: "next"; +} +export interface Back { + readonly type: "back"; + readonly skipNavigation?: boolean; +} + +export interface Close { + readonly type: "close"; +} + +export type GlobalEvents = Next | Back | Close; diff --git a/yarn.lock b/yarn.lock index 1eb3290866a..94c005953a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4545,7 +4545,7 @@ resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.7.1.tgz#157d5083db3f116b7ae28b5b3aef8f457f052491" integrity sha512-dQEt6enmHXtD93vDcMefhb5bh1zh0mLCRT8CvYJjCpTjaTth7sXqlU6ri1qP0HDR6IbU9s2/WVNw7Oy7O/Sqfg== -"@xstate/react@npm:@xstate/react@4": +"@xstate/react@^4.0.3": version "4.1.1" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa" integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA== From e31a41bd375b7b9410b651f4dd423f3e71fe5d4e Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Wed, 1 May 2024 15:08:05 +0200 Subject: [PATCH 06/31] chore: idpay payment machine --- package.json | 4 +- .../idpay/common/navigation/linking.ts | 14 +- .../idpay/configuration/xstate/actions.ts | 8 +- .../idpay/configuration/xstate/events.ts | 4 +- .../idpay/configuration/xstate/machine.ts | 174 ++----- .../idpay/configuration/xstate/provider.tsx | 48 +- .../idpay/onboarding/machine/events.ts | 6 +- .../payment/{xstate => machine}/actions.ts | 17 +- .../{xstate/services.ts => machine/actors.ts} | 250 +++++----- .../payment/{xstate => machine}/context.ts | 14 +- ts/features/idpay/payment/machine/events.ts | 8 + ts/features/idpay/payment/machine/input.ts | 8 + ts/features/idpay/payment/machine/machine.ts | 182 ++++++++ .../payment/{xstate => machine}/provider.tsx | 48 +- .../idpay/payment/machine/selectors.ts | 19 + .../idpay/payment/navigation/navigator.tsx | 56 +-- .../idpay/payment/navigation/params.ts | 9 + .../idpay/payment/navigation/routes.ts | 7 + .../IDPayPaymentAuthorizationScreen.tsx | 79 ++-- .../screens/IDPayPaymentCodeInputScreen.tsx | 13 +- .../screens/IDPayPaymentCodeScanScreen.tsx | 8 +- .../screens/IDPayPaymentResultScreen.tsx | 30 +- .../IDPayPaymentResultScreen.test.tsx | 8 +- .../payment/xstate/__tests__/machine.test.ts | 437 ------------------ .../payment/xstate/__tests__/services.test.ts | 236 ---------- ts/features/idpay/payment/xstate/events.ts | 22 - ts/features/idpay/payment/xstate/machine.ts | 195 -------- ts/features/idpay/payment/xstate/selectors.ts | 35 -- .../idpay/unsubscription/machine/events.ts | 7 +- .../idpay/unsubscription/machine/machine.ts | 8 +- .../idpay/unsubscription/machine/selectors.ts | 25 +- .../UnsubscriptionConfirmationScreen.tsx | 5 +- .../screens/UnsubscriptionResultScreen.tsx | 2 +- ts/navigation/params/AppParamsList.ts | 15 +- ts/xstate/selectors/index.ts | 15 + yarn.lock | 15 +- 36 files changed, 552 insertions(+), 1479 deletions(-) rename ts/features/idpay/payment/{xstate => machine}/actions.ts (76%) rename ts/features/idpay/payment/{xstate/services.ts => machine/actors.ts} (51%) rename ts/features/idpay/payment/{xstate => machine}/context.ts (53%) create mode 100644 ts/features/idpay/payment/machine/events.ts create mode 100644 ts/features/idpay/payment/machine/input.ts create mode 100644 ts/features/idpay/payment/machine/machine.ts rename ts/features/idpay/payment/{xstate => machine}/provider.tsx (53%) create mode 100644 ts/features/idpay/payment/machine/selectors.ts create mode 100644 ts/features/idpay/payment/navigation/params.ts create mode 100644 ts/features/idpay/payment/navigation/routes.ts delete mode 100644 ts/features/idpay/payment/xstate/__tests__/machine.test.ts delete mode 100644 ts/features/idpay/payment/xstate/__tests__/services.test.ts delete mode 100644 ts/features/idpay/payment/xstate/events.ts delete mode 100644 ts/features/idpay/payment/xstate/machine.ts delete mode 100644 ts/features/idpay/payment/xstate/selectors.ts create mode 100644 ts/xstate/selectors/index.ts diff --git a/package.json b/package.json index be63fd10c83..4e47aaa0b6e 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,6 @@ "@react-navigation/stack": "6.3.20", "@redux-saga/testing-utils": "^1.1.3", "@xstate/react": "^4.0.3", - "@xstate4/react": "npm:@xstate/react@3.0.1", "async-mutex": "^0.1.3", "buffer": "^4.9.1", "color": "^3.0.0", @@ -218,8 +217,7 @@ "vision-camera-code-scanner": "^0.2.0", "xml2js": "^0.5.0", "xss": "1.0.10", - "xstate": "^5", - "xstate4": "npm:xstate@4.33.6" + "xstate": "^5" }, "devDependencies": { "@babel/core": "^7.18.8", diff --git a/ts/features/idpay/common/navigation/linking.ts b/ts/features/idpay/common/navigation/linking.ts index 1e163de482c..a32ef367349 100644 --- a/ts/features/idpay/common/navigation/linking.ts +++ b/ts/features/idpay/common/navigation/linking.ts @@ -1,6 +1,6 @@ import { PathConfigMap } from "@react-navigation/native"; import { IDPayDetailsRoutes } from "../../details/navigation"; -import { IDPayPaymentRoutes } from "../../payment/navigation/navigator"; +import { IdPayPaymentRoutes } from "../../payment/navigation/routes"; import { AppParamsList } from "../../../../navigation/params/AppParamsList"; import { IdPayOnboardingRoutes } from "../../onboarding/navigation/routes"; @@ -9,13 +9,7 @@ export const idPayLinkingOptions: PathConfigMap = { * IDPay initiative onboarding */ [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: { - path: "idpay/onboarding", - screens: { - /** - * Handles ioit://idpay/onboarding/{initiativeId} - */ - [IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: "/:serviceId" - } + path: "idpay/onboarding/:serviceId" }, /** * IDPay initiative details @@ -32,13 +26,13 @@ export const idPayLinkingOptions: PathConfigMap = { /** * IDPay payment authorization */ - [IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: { + [IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: { path: "idpay/auth", screens: { /** * Handles ioit://idpay/auth/{trxCode} */ - [IDPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION]: "/:trxCode" + [IdPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION]: "/:trxCode" } } }; diff --git a/ts/features/idpay/configuration/xstate/actions.ts b/ts/features/idpay/configuration/xstate/actions.ts index c13a055d15c..c4ae50f451e 100644 --- a/ts/features/idpay/configuration/xstate/actions.ts +++ b/ts/features/idpay/configuration/xstate/actions.ts @@ -30,16 +30,16 @@ const createActionsImplementation = ( ); }; - const navigateToConfigurationIntro = guardedNavigationAction( - (context: Context) => { - if (context.initiativeId === undefined) { + const navigateToConfigurationIntro = guardedNavigationAction( + (args: { context: Context }) => { + if (args.context.initiativeId === undefined) { throw new Error("initiativeId is undefined"); } navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, params: { - initiativeId: context.initiativeId + initiativeId: args.context.initiativeId } }); } diff --git a/ts/features/idpay/configuration/xstate/events.ts b/ts/features/idpay/configuration/xstate/events.ts index ff29020fb18..8c37649de97 100644 --- a/ts/features/idpay/configuration/xstate/events.ts +++ b/ts/features/idpay/configuration/xstate/events.ts @@ -1,6 +1,6 @@ import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; -import { E_BACK } from "../../../../xstate/types/events"; +import { Back } from "../../../../xstate/types/events"; import { ConfigurationMode } from "./context"; type E_START_CONFIGURATION = { @@ -91,5 +91,5 @@ export type Events = | E_COMPLETE_CONFIGURATION | E_SKIP | E_NEXT - | E_BACK + | Back | E_QUIT; diff --git a/ts/features/idpay/configuration/xstate/machine.ts b/ts/features/idpay/configuration/xstate/machine.ts index d9f387411a0..785494589c7 100644 --- a/ts/features/idpay/configuration/xstate/machine.ts +++ b/ts/features/idpay/configuration/xstate/machine.ts @@ -1,64 +1,18 @@ -import * as p from "@pagopa/ts-commons/lib/pot"; -import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { assign, createMachine, forwardTo } from "xstate4"; -import { IbanListDTO } from "../../../../../definitions/idpay/IbanListDTO"; -import { - InitiativeDTO, - StatusEnum as InitiativeStatusEnum -} from "../../../../../definitions/idpay/InitiativeDTO"; -import { - InstrumentDTO, - StatusEnum as InstrumentStatusEnum -} from "../../../../../definitions/idpay/InstrumentDTO"; -import { Wallet } from "../../../../types/pagopa"; +import { assign, createMachine, forwardTo } from "xstate"; import { LOADING_TAG, UPSERTING_TAG, WAITING_USER_INPUT_TAG } from "../../../../xstate/utils"; -import { - ConfigurationMode, - Context, - INITIAL_CONTEXT, - InstrumentStatusByIdWallet -} from "./context"; -import { Events } from "./events"; -import { InitiativeFailure, InitiativeFailureType } from "./failure"; - -type Services = { - loadInitiative: { - data: InitiativeDTO; - }; - loadIbanList: { - data: IbanListDTO; - }; - enrollIban: { - data: undefined; - }; - loadWalletInstruments: { - data: ReadonlyArray; - }; - loadInitiativeInstruments: { - data: ReadonlyArray; - }; -}; +import { INITIAL_CONTEXT } from "./context"; /** PLEASE DO NO USE AUTO-LAYOUT WHEN USING VISUAL EDITOR */ -const createIDPayInitiativeConfigurationMachine = () => +export const idPayInitiativeConfigurationMachine = /** @xstate-layout N4IgpgJg5mDOIC5QCUDyqAqBiAigVQEkMBtABgF1FQAHAe1gEsAXB2gOypAA9EBWXgIy8AdAHYALPwCc4qaSlSBAZgA0IAJ6JRvKcNKiAbKPkGDADgNKlBgL421aTMIDqAQSIEAcgHEA+gGUMV2RsQOCMXwBhVE8AMQJvPGRXDAIYskokEDpGFnZOHgQAJjNDYVNSAXEDIqV+USK1TWKzXXkFXlEhUiLSAyq7B3QMYQAZVFcAES8-Lw8UggA1AFEsCHYwYQY2ADdaAGtNgBtaAEMIAjZmBlOWHbAMzhzr-KzC3lKi4SVapVkFJTaJqIARmL68UiQqS8IrGGRFKSDECOEbjKYzXxzVILFZYMAAJ3xtHxwmoR1uADNiQBbYQnc6Xa63Bj3R5ZZ55DhvPifb6-f5SQG8YEIJSkcTCMwCwSAqSfJEo4TLRauUZ4BY+TGeeapFZRGLxRLJVIxLBsmj0F5c0CFAydESVCy8ao6CxSEVFASkPSGZ28O2iVoGUhmBXDJUqtUa2ba7G65b6uIJJILU3EASZC25VjW7gg-QigRyESlXh-KSiaFyu1hpzK1Xq1KarEEHEJ6JJo2pzxmoqZ7KWzkFRC1QXCBTBj6g4xVAyFgQSH2WST8cziIpFWsjetRpsxnVLdsG5PGtI94hKfscnPDhABh2gu0u6vujSIcTiURLmqGSxFAziFKW7CNM-gAAqjK4ACaGJeBgaBYJ4ywABokBQTyDje3IIJ0ShmJKSgyH0oLiICqhvggAgIuChh4XaOgwpYwEdoaSSwQAQq4PbrGwmywEwtybIqLEnhxXHmgO2avDafCCCIEjSLI7TKCKgLBsIXr6JIsignhzHHkaYncRswj8YJwjCQZbHNpxnjplemHSXmOH-l8RSSF64ghtYliFuIVF8gBPxynI1iiPpnbWbMtljBM0w2VxvijAQgRrCZ2x7IcdJnBcABGpxsKMDD8RJ15OYUPyQsIOi0ZCzrmCpFHudUBEwr6C5eUx9jIuGImGQlnixeiA1JSl2AEkSJJkpSNLZQy+WFcVTClY5ubvII3oKToSkKI1zQ-P6wjVGYZj6DIEJyJu3WWZFyBGUN8XRYlyWpRNxKkuSTBUvitL0nlBVFSVGYYVJa2yUIYgrjIci7eRzT-oCehmB87Q-P5iLXb1Vl3QNEYNtGmK2aNqUraDt7WEoAiQzV-nmPojRNZ+BjjoG-COpT4WY04fVRYTXF47uRnE9g9kg1a5OWFTCkVrTp0NB6VgShuUqfvofysxFrE409g2gRB0FCzE7ETMgj2IShaEOWT2H+XhBFSOYgaAaCZgK+u1UlNUG7mDoAia6JuN65BMEjUbJtm5xkQANKk+L2H8BDW3Q8pcP5lVnR2pUa7+nK-v9TrIEpfrIc674YfBBHrjR6L7KrbeCfyVDO2KKnxQfMzHydAuauEUBXMjDz2t87rRfB4bnjGxXGKxKgyAALJYCJ8-D7HQ421U3qeoB-nt1R4gekWIj2gYcqnTIAi2P3wiD-dQcG6HE-h9Ps8L5HMfobX1syQgn529o1gVn4HUT0HoSib2hHhB2gFvZ515gQGKd8S7DzLo-KemoZ7zywG-GuWY47f1tvhMwhFHZSjMC7UBUgvg9CISfLy0JjCXyGNzbG90l5zyMmlXiWxdgHE2AAY3YBSBgP0CALVXlhfByhCHENKKQ8hFEhCEUCpTMUXkqiVlgUPeB-M2EcLelNT631aQCLYEIkRYiP64LXpIu2RCHayOdidD0+gRAX0FJCIQp1SyaNvqPe+pcXrYGwZYySeDnIN2pttGGLdCykEEEuBEgI6p9B8YHPxSDtGeGFlgquMdgafzCetROTdol7RHGWZmIZ-TuS3oCUQnMmEDxYWk8CY8RqBPNs4ZB5dTYzHEeVd8cthCwlED8ZGYpAzIwVmKMQ0JoEQgaNURhPVmG3V8a0-xyCOnLE8GgUYowV4hLKmDH+rQvh2lqFKHoXQ8KFhkEoPQCIajyB6MFXgqSC47L2clAanDNgZV4cIMAbAiRHCOKIgq-STm-3wv-E+2gywwgEM4pR8zKF-FXEQj5w8lS7NQPsjhPF-k8KysC0F4KxH5KsRI5yMKxAVMAYikBCi7T4QznUTuC4JBXUadfZpny8UEt+foj6M0fpApBbQMFEK2BQtvHSuFjLgHIqakIXQJ1WjRPMA7ZZN0tawU8IEZAeA547IwP4P5pkBJMCEljNZzZDXwRNWa-wcrsKjgeROOJZDSiVGqKpaEVNkbyEBFKf8phsVeCNc6zw5rLVmRtRZO1+qHXRtNbG11VLQnWOch68cDtvXTj9XOCiakqbBi0ooeQDQL6Rsdca9N5qHoGrTS6+N1rbWrJTTGVtGbm2pqdY211Ry67YQEIo4Z64ijOmgRIMEHo4T5s9BuAtsh3lXxvgOhtLr+09sHW2olVrzJ6oDnu7dfa0SPS1L281ODs00sKOOssk6NwzrBHOhm8NTBfFkL0MigIaniDrTe-wu7r37oze249ybT3gfPU2y9LaIO3r7GLHNhQ81eqnL62cqkz4aUhEQ3+CIJDAeQ6BxDW6Y0IbihiNw+zlgRCjeR3dlqAVZT+s4U4YKwBMEuPxfEABXakwKmCwDdd-YMzND51C8V6UwqlehfloUQhxRYMa8s3We6jFHaNUaHWB+joxGNwZ06xkV00vqzU49xo4vH+NMCEyJtgYmJO5sIp6gt2GZz+tLb3coMhVNO3U2R+DunhraYM5RvwRmTPMbC+Zwk71LNGLmhALjPG+NsAE8J0T4ms3HNvJhrzPqfMlvhjoXQnc-gbinTLULZnoumai3p-ccZDzNZ3dFtjJLjg5UZCwZk9wHNOby2594Y5ISQkrP6D8oJP0jlOm5dcIY-UnRPg1lrEXOsXta1qA8ep4uNda3iJLBixW-X61cQbdwwAjdyy58TI6v7ubHFh0rxaPSnWZv+eok4pQNJWU0+1kWut7aO1tq9LY2w7ZoxF07k1RVWfFX9AbNxbv3ec65gro7v7Ff+0W3DjNKwaUDPcnQJgNNA75SD2HoHEFIbC1gMI3gEwQ7NeNkE0gfTrhhOt-0pRQHaDEFUWEYvJbik2zuhn+m21fPxQc9nsbOeUV+2IO0J8tJCGna7JqwZdCSEpsWRQNMpd9pl6DyDkxljGYwGz+t1GVdUQAuUeQbNDAnQrCiqmQo2r0wRGCM3TaLd06wFMSYvgwLQUbb4U1GAAASqBJhO5hJvPCfwPOfgdgfEn-5NLikInKQHJ78508LhsjJDuh05Ors9wpI4PP5oJzh3zzRSjqvDXEowFQeXU602XkPSu43YJxy9jDjf3uE9byOdc+EjAnQWUb2QSgg-0-SYznT5tUJO5oZKGpCgpSdGMLr+GH4vwKCnaOB2DRV-l+Lhv6v-go4EDAjv6we-1wH8kPUkMCt4nOjd0qGUD+AsFv3lyFUt2wEPXY02DJSlXBWy0cwe2Wjr3QxBAnRqWnQ-HfWgQVnMHKDoWhm0C9AdjAMFR+UgIR2S0MVmjgOlUQNGxcydwwKnTfVhFwMZhqD0HhArWnVMCAw3X5QHxt0YwfzNR60yk2AgDADsxtUx1E2YOfUwLYI-QPlhD3zIQrCMHcjLFv2t1tzENjSoPO2R1pGkNkLuwYOQMUIeWUOwPYPnSag3CphKFnDFzFBDGAhD0Hm7ACDwEiEiGWH8AtWiDnggkYyPFum7BVwiSTmbjKVFCGQrSIXHW6DkAEN5W8Oxl8P8H8MCOCLD0mAjyjyghjzj0T2T1QIfUQGsB0HHH8gRE-GRh0lUnMAeSqDIVIHT0-GdF7xLxTBNCyU8EwF8CQmWGt0mEXlQDCNt0iK1miKqIGWKAnxKyn3KxqIUF0DqC9FqGfBIjsG6jYFoGkPgCyBRDQ2qJwhFBkHzU2LuLuIyOpzcA8E1DCBCAuKWPDR+3cQ-FMErASMphoifHMEsGRkDF1XDCa2h3jA+JOX4BKA0jklGUECnSBCalaCXCUl0m70-GAh3EbANQOzmJPG7FhNvGeW9FIgD14KdmcQlCZjLHkA8galDCvkH1jTQDJPjl5B+EVk2KFA9H6F0AYTqkAnFDiUjVsi5Lx2Rjn2DDcI5XlgUQ8j3xqC5RPl6HXU0yEMyTA0yWFmlNzTkGZgqAVPqAWzblKGqjwlIkP3fS6MlP5nxIJn1MCUNIqkL3qOUHHXMCIUqGFE4MIXHQsDzw5msEdJHgr3Hknl6R8HdJqKWw0l6FKDqB8nOgVhDBZlU1kH4Jv0ENp11MHyJh6SvQwTnnjJ-jqQ0gUDiUUEAi6DtAoQlFDTBAWW1wrAjJp3iHniMgrLwlqG+DqlhAhCI33iahDF0BqFHEsAkD6El3zO7RxSLOejGgrI2geRqCZn0G0CrQDNP16FanNKIUYk7PAIoOHgrI-A+ElB6JuX12nTpO9HW2oUDTFD0gXNg11P1J8MGP1BmIiMmDXOrWGVfQviqScRZS6HKGsGnU6EPzqlXzXNbiLFIklG0FIlBEqA5RXw-NLyH3CyhyrxdQrK9BVQqxPmGRdhVm2L9lwrgSIt2223wsM1VFtzpxIq4MPlhGRgvgAkDAtMpnkFJ1BEEBnAUmLxgzwoYrh0IpAxYoYyY2koIpmArM9G9C4tZl4qaIEqLBNJuUkBKAuRSToq0SUr1LMqa1i0UrksvT8ICKCP8ArIsGbLqAdmnXURq0UwRPXFF0fAhC6m1ILIsvB2CqYtjFbHjHYoKTQMolkAXS6JFxhjFH4KEFvyhNCsIqJLL2ixIq9yan+MotlNMEECLDSpCpspCqyuYtstyPsuCKcpdysB0BqBRNGXXC+w3HHBBL-BIysD0PX1lwzRIphAeTarm1plInFAVnqQ0iMD9EqARnDJMsMKbTPJWorMrAeVaB4qLC6LBAvgViIRfS6D6FaHbj0JEL3Fhw2qlGGQdjmQ-Adm2LpIlC0Jmw3i6NhFv3wr-PCLt0AuisuN6CEorBKA3B6EoUUDIobzISOhq3U2DF2IkqcCyKiN-NqvyMcsBqWJgvBBlnRV6AAgEqdld23I-HLHkE0V8OGIiDGImJIq6K-GsD3gsFGW7nWNFC0O+Auj+LBMBCpt-NCL+vGLXJ5P5H5LRP2hkC-GBrkChGDHqQFrPCiHGH8BFuxrhLFr5IBElsQAP3HDz2MCsD6D4qVpiF8FiHcDVGQGWFFoaD5G1sFF1tFEAgN0AlqM9GXE5jsCAA */ createMachine( { context: INITIAL_CONTEXT, - tsTypes: {} as import("./machine.typegen").Typegen0, - schema: { - context: {} as Context, - events: {} as Events, - services: {} as Services - }, id: "ROOT", - predictableActionArguments: true, initial: "WAITING_START", on: { /** @@ -101,7 +55,7 @@ const createIDPayInitiativeConfigurationMachine = () => }, onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "SESSION_EXPIRED" }, { @@ -123,21 +77,21 @@ const createIDPayInitiativeConfigurationMachine = () => /** * Configuration in "INSTRUMENTS" mode */ - cond: "isInstrumentsOnlyMode", + guard: "isInstrumentsOnlyMode", target: "CONFIGURING_INSTRUMENTS" }, { /** * Configuration in "IBAN" mode */ - cond: "isIbanOnlyMode", + guard: "isIbanOnlyMode", target: "CONFIGURING_IBAN" }, { /** * Configuration in "COMPLETE" mode, no iban or instruments already configured */ - cond: "isInitiativeConfigurationNeeded", + guard: "isInitiativeConfigurationNeeded", target: "DISPLAYING_INTRO" }, { @@ -187,7 +141,7 @@ const createIDPayInitiativeConfigurationMachine = () => }, onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "#ROOT.SESSION_EXPIRED" }, { @@ -195,7 +149,7 @@ const createIDPayInitiativeConfigurationMachine = () => * If configuration mode is "IBAN", the machine should set the received failure to the * context and go to the failure state */ - cond: "isIbanOnlyMode", + guard: "isIbanOnlyMode", target: "#ROOT.CONFIGURATION_FAILURE", actions: "setFailure" }, @@ -222,7 +176,7 @@ const createIDPayInitiativeConfigurationMachine = () => * If at least one iban is present, next state should be the iban selection state. */ target: "DISPLAYING_IBAN_LIST", - cond: "hasIbanList" + guard: "hasIbanList" }, { /** @@ -254,7 +208,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If configuration mode is "IBAN", the machine should go the the final state */ - cond: "isIbanOnlyMode", + guard: "isIbanOnlyMode", target: "#ROOT.CONFIGURATION_CLOSED" }, { @@ -290,7 +244,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If the user has at least one IBAN, the machine goes to the display state */ - cond: "hasIbanList", + guard: "hasIbanList", target: "DISPLAYING_IBAN_LIST" }, { @@ -318,7 +272,7 @@ const createIDPayInitiativeConfigurationMachine = () => }, onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "#ROOT.SESSION_EXPIRED" }, { @@ -345,7 +299,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If the configuration mode is "IBAN", the configuration should be closed */ - cond: "isIbanOnlyMode", + guard: "isIbanOnlyMode", target: "#ROOT.CONFIGURATION_CLOSED" }, { @@ -387,7 +341,7 @@ const createIDPayInitiativeConfigurationMachine = () => * If success and configuration mode is "IBAN", the next state is the IBAN list * A success toast is displayed */ - cond: "isIbanOnlyMode", + guard: "isIbanOnlyMode", target: "DISPLAYING_IBAN_LIST", actions: ["enrollIbanSuccess", "showUpdateIbanToast"] }, @@ -401,7 +355,7 @@ const createIDPayInitiativeConfigurationMachine = () => ], onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "#ROOT.SESSION_EXPIRED" }, { @@ -423,7 +377,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If configuration mode is "IBAN", the configuration si completed */ - cond: "isIbanOnlyMode", + guard: "isIbanOnlyMode", target: "CONFIGURATION_COMPLETED" }, { @@ -473,11 +427,11 @@ const createIDPayInitiativeConfigurationMachine = () => }, onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "#ROOT.SESSION_EXPIRED" }, { - cond: "isInstrumentsOnlyMode", + guard: "isInstrumentsOnlyMode", target: "#ROOT.CONFIGURATION_FAILURE", actions: "setFailure" }, @@ -514,11 +468,11 @@ const createIDPayInitiativeConfigurationMachine = () => }, onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "#ROOT.SESSION_EXPIRED" }, { - cond: "isInstrumentsOnlyMode", + guard: "isInstrumentsOnlyMode", target: "#ROOT.CONFIGURATION_FAILURE", actions: "setFailure" }, @@ -540,7 +494,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If the user has PagoPA instruments we go to the state where we show the instrument toggles */ - cond: "hasInstruments", + guard: "hasInstruments", target: "DISPLAYING_INSTRUMENTS" }, { @@ -548,7 +502,7 @@ const createIDPayInitiativeConfigurationMachine = () => * If the configuration mode is "INSTRUMENT", we go to the state where we show the instrument toggles. * In this case we do not care if the user does not have any PagoPA instrument */ - cond: "isInstrumentsOnlyMode", + guard: "isInstrumentsOnlyMode", target: "DISPLAYING_INSTRUMENTS" }, { @@ -644,7 +598,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If we are configuring instruments only, back navigation should close the configuration flow */ - cond: "isInstrumentsOnlyMode", + guard: "isInstrumentsOnlyMode", target: "#ROOT.CONFIGURATION_CLOSED" }, { @@ -700,7 +654,7 @@ const createIDPayInitiativeConfigurationMachine = () => }, onError: [ { - cond: "isSessionExpired", + guard: "isSessionExpired", target: "#ROOT.SESSION_EXPIRED" }, { @@ -725,7 +679,7 @@ const createIDPayInitiativeConfigurationMachine = () => /** * If we are configuring instruments, the next state is the final state */ - cond: "isInstrumentsOnlyMode", + guard: "isInstrumentsOnlyMode", target: "CONFIGURATION_COMPLETED" }, { @@ -818,33 +772,7 @@ const createIDPayInitiativeConfigurationMachine = () => confirmIbanOnboarding: assign((_, event) => ({})), loadWalletInstrumentsSuccess: assign((_, event) => ({})), loadInitiativeInstrumentsSuccess: assign((_, event) => ({})), - updateInstrumentStatuses: assign((context, _) => { - const updatedStatuses = - context.initiativeInstruments.reduce( - (acc, instrument) => { - if (instrument.idWallet === undefined) { - return acc; - } - - const currentStatus = acc[instrument.idWallet]; - - if (currentStatus !== undefined && p.isLoading(currentStatus)) { - // Instrument is updating, its status will be updated by 'updateInstrumentStatus' action - return acc; - } - - return { - ...acc, - [instrument.idWallet]: p.some(instrument.status) - }; - }, - context.instrumentStatuses - ); - - return { - instrumentStatuses: updatedStatuses - }; - }), + updateInstrumentStatuses: assign((context, _) => ({})), forwardToInstrumentsEnrollmentService: forwardTo( "instrumentsEnrollmentService" ), @@ -857,42 +785,15 @@ const createIDPayInitiativeConfigurationMachine = () => skipInstruments: assign((_, __) => ({ areInstrumentsSkipped: true })), - setFailure: assign((_, event) => ({ - failure: pipe( - event.data, - InitiativeFailure.decode, - E.getOrElse(() => InitiativeFailureType.GENERIC) - ) - })) + setFailure: assign((_, event) => ({})) }, guards: { - isSessionExpired: (_, event) => - pipe( - event.data, - InitiativeFailure.decode, - O.fromEither, - O.filter( - failure => failure === InitiativeFailureType.SESSION_EXPIRED - ), - O.isSome - ), - isInitiativeConfigurationNeeded: (context, _) => - p.getOrElse( - p.map( - context.initiative, - i => i.status === InitiativeStatusEnum.NOT_REFUNDABLE - ), - false - ), - isIbanOnlyMode: (context, _) => context.mode === ConfigurationMode.IBAN, - hasIbanList: (context, _) => - p.getOrElse( - p.map(context.ibanList, list => list.length > 0), - false - ), - isInstrumentsOnlyMode: (context, _) => - context.mode === ConfigurationMode.INSTRUMENTS, - hasInstruments: (context, _) => context.walletInstruments.length > 0 + isSessionExpired: (_, event) => false, + isInitiativeConfigurationNeeded: (_, event) => false, + isIbanOnlyMode: (_, event) => false, + hasIbanList: (_, event) => false, + isInstrumentsOnlyMode: (_, event) => false, + hasInstruments: (_, event) => false }, delays: { /** @@ -902,10 +803,3 @@ const createIDPayInitiativeConfigurationMachine = () => } } ); - -type IDPayInitiativeConfigurationMachineType = ReturnType< - typeof createIDPayInitiativeConfigurationMachine ->; - -export { createIDPayInitiativeConfigurationMachine }; -export type { IDPayInitiativeConfigurationMachineType }; diff --git a/ts/features/idpay/configuration/xstate/provider.tsx b/ts/features/idpay/configuration/xstate/provider.tsx index c8bd425a151..84933d35c2c 100644 --- a/ts/features/idpay/configuration/xstate/provider.tsx +++ b/ts/features/idpay/configuration/xstate/provider.tsx @@ -1,7 +1,7 @@ -import { useInterpret } from "@xstate/react"; +import { createActorContext } from "@xstate/react"; import * as E from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import React from "react"; import { InterpreterFrom } from "xstate"; import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; @@ -14,7 +14,6 @@ import { pagoPaApiUrlPrefix, pagoPaApiUrlPrefixTest } from "../../../../config"; -import { useXStateMachine } from "../../../../xstate/hooks/useXStateMachine"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { sessionInfoSelector } from "../../../../store/reducers/authentication"; @@ -22,32 +21,24 @@ import { isPagoPATestEnabledSelector, preferredLanguageSelector } from "../../../../store/reducers/persistedPreferences"; +import { SessionManager } from "../../../../utils/SessionManager"; import { defaultRetryingFetch } from "../../../../utils/fetch"; import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; -import { SessionManager } from "../../../../utils/SessionManager"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; -import { - createIDPayInitiativeConfigurationMachine, - IDPayInitiativeConfigurationMachineType -} from "./machine"; +import { idPayInitiativeConfigurationMachine } from "./machine"; import { createServicesImplementation } from "./services"; -type ConfigurationMachineContext = - InterpreterFrom; - -const ConfigurationMachineContext = - React.createContext( - {} as ConfigurationMachineContext - ); - type Props = { children: React.ReactNode; }; -const IDPayConfigurationMachineProvider = (props: Props) => { +export const IdPayInitiativeConfigurationMachineContext = createActorContext( + idPayInitiativeConfigurationMachine +); + +export const IDPayConfigurationMachineProvider = (props: Props) => { const dispatch = useIODispatch(); - const [machine] = useXStateMachine(createIDPayInitiativeConfigurationMachine); const sessionInfo = useIOSelector(sessionInfoSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); @@ -94,7 +85,7 @@ const IDPayConfigurationMachineProvider = (props: Props) => { isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); - const services = createServicesImplementation( + const actors = createServicesImplementation( idPayClient, paymentManagerClient, pmSessionManager, @@ -104,23 +95,14 @@ const IDPayConfigurationMachineProvider = (props: Props) => { const actions = createActionsImplementation(navigation, dispatch); - const machineService = useInterpret(machine, { - actions, - services + const machine = idPayInitiativeConfigurationMachine.provide({ + actors, + actions }); return ( - + {props.children} - + ); }; - -const useConfigurationMachineService = () => - React.useContext(ConfigurationMachineContext); - -export { - IDPayConfigurationMachineProvider, - useConfigurationMachineService, - ConfigurationMachineContext -}; diff --git a/ts/features/idpay/onboarding/machine/events.ts b/ts/features/idpay/onboarding/machine/events.ts index c449aba6ce9..9d51bcb36dd 100644 --- a/ts/features/idpay/onboarding/machine/events.ts +++ b/ts/features/idpay/onboarding/machine/events.ts @@ -18,8 +18,4 @@ export interface SelectMultiConsent { readonly data: SelfConsentMultiDTO; } -export type Events = - | AutoInit - | SelectMultiConsent - | ToggleBoolCriteria - | GlobalEvents; +export type Events = GlobalEvents | SelectMultiConsent | ToggleBoolCriteria; diff --git a/ts/features/idpay/payment/xstate/actions.ts b/ts/features/idpay/payment/machine/actions.ts similarity index 76% rename from ts/features/idpay/payment/xstate/actions.ts rename to ts/features/idpay/payment/machine/actions.ts index 5b5dd99003a..544ad0856a2 100644 --- a/ts/features/idpay/payment/xstate/actions.ts +++ b/ts/features/idpay/payment/machine/actions.ts @@ -1,19 +1,16 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch } from "../../../../store/hooks"; import { showToast } from "../../../../utils/showToast"; import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayDetailsRoutes } from "../../details/navigation"; -import { IDPayPaymentRoutes } from "../navigation/navigator"; +import { IdPayPaymentRoutes } from "../navigation/routes"; import { Context } from "./context"; const createActionsImplementation = ( - navigation: IOStackNavigationProp, + navigation: ReturnType, dispatch: ReturnType ) => { const handleSessionExpired = () => { @@ -27,15 +24,15 @@ const createActionsImplementation = ( }; const navigateToAuthorizationScreen = () => { - navigation.navigate(IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { - screen: IDPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION, + navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { + screen: IdPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION, params: {} }); }; const navigateToResultScreen = () => - navigation.navigate(IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { - screen: IDPayPaymentRoutes.IDPAY_PAYMENT_RESULT + navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { + screen: IdPayPaymentRoutes.IDPAY_PAYMENT_RESULT }); const showErrorToast = () => diff --git a/ts/features/idpay/payment/xstate/services.ts b/ts/features/idpay/payment/machine/actors.ts similarity index 51% rename from ts/features/idpay/payment/xstate/services.ts rename to ts/features/idpay/payment/machine/actors.ts index d456eaa55a6..0d78910710e 100644 --- a/ts/features/idpay/payment/xstate/services.ts +++ b/ts/features/idpay/payment/machine/actors.ts @@ -1,148 +1,90 @@ import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { flow, pipe } from "fp-ts/lib/function"; +import { fromPromise } from "xstate"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { CodeEnum as TransactionErrorCodeEnum } from "../../../../../definitions/idpay/TransactionErrorDTO"; import { IDPayClient } from "../../common/api/client"; import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; -import { Context } from "./context"; -export type Services = { - preAuthorizePayment: { - data: AuthPaymentResponseDTO; - }; - authorizePayment: { - data: AuthPaymentResponseDTO; - }; -}; +export const createActorsImplementation = ( + client: IDPayClient, + token: string +) => { + const preAuthorizePayment = fromPromise( + async ({ input }) => { + const putPreAuthPaymentTask = (trxCode: string) => + TE.tryCatch( + async () => + await client.putPreAuthPayment({ + bearerAuth: token, + trxCode + }), + mapFetchError + ); -/** - * Maps the backed error codes to UI failure states - * @param code Error code from backend - * @returns The associated failure state - */ -export const mapErrorCodeToFailure = ( - code: TransactionErrorCodeEnum -): PaymentFailureEnum => { - switch (code) { - case TransactionErrorCodeEnum.PAYMENT_TRANSACTION_EXPIRED: - case TransactionErrorCodeEnum.PAYMENT_NOT_FOUND_OR_EXPIRED: - return PaymentFailureEnum.TRANSACTION_EXPIRED; - case TransactionErrorCodeEnum.PAYMENT_USER_SUSPENDED: - return PaymentFailureEnum.USER_SUSPENDED; - case TransactionErrorCodeEnum.PAYMENT_USER_NOT_ONBOARDED: - return PaymentFailureEnum.USER_NOT_ONBOARDED; - case TransactionErrorCodeEnum.PAYMENT_USER_UNSUBSCRIBED: - return PaymentFailureEnum.USER_UNSUBSCRIBED; - case TransactionErrorCodeEnum.PAYMENT_ALREADY_AUTHORIZED: - return PaymentFailureEnum.ALREADY_AUTHORIZED; - case TransactionErrorCodeEnum.PAYMENT_BUDGET_EXHAUSTED: - return PaymentFailureEnum.BUDGET_EXHAUSTED; - case TransactionErrorCodeEnum.PAYMENT_ALREADY_ASSIGNED: - return PaymentFailureEnum.ALREADY_ASSIGNED; - case TransactionErrorCodeEnum.PAYMENT_INITIATIVE_INVALID_DATE: - return PaymentFailureEnum.INVALID_DATE; - default: - return PaymentFailureEnum.GENERIC; - } -}; + const dataResponse = await putPreAuthPaymentTask(input)(); -/** - * This function maps errors from the fetch to the PaymentFailure type - * This helps to know if the error comes from a 429 status code - */ -const mapFetchError = (error: unknown): PaymentFailure => { - if (error === "max-retries") { - return PaymentFailureEnum.TOO_MANY_REQUESTS; - } - return PaymentFailureEnum.GENERIC; -}; - -const createServicesImplementation = (client: IDPayClient, token: string) => { - const preAuthorizePayment = async ( - context: Context - ): Promise => { - const putPreAuthPaymentTask = (trxCode: string) => - TE.tryCatch( - async () => - await client.putPreAuthPayment({ - bearerAuth: token, - trxCode - }), - mapFetchError - ); - - const dataResponse = await pipe( - context.trxCode, - TE.fromOption(() => PaymentFailureEnum.GENERIC), - TE.chain(putPreAuthPaymentTask) - )(); - - return pipe( - dataResponse, - E.fold( - failure => Promise.reject(failure), - flow( - E.map(({ status, value }) => { - switch (status) { - case 200: - return Promise.resolve(value); - case 401: - return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); - default: - return Promise.reject(mapErrorCodeToFailure(value.code)); - } - }), - E.getOrElse(() => Promise.reject(PaymentFailureEnum.GENERIC)) + return pipe( + dataResponse, + E.fold( + failure => Promise.reject(failure), + flow( + E.map(({ status, value }) => { + switch (status) { + case 200: + return Promise.resolve(value); + case 401: + return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); + default: + return Promise.reject(mapErrorCodeToFailure(value.code)); + } + }), + E.getOrElse(() => Promise.reject(PaymentFailureEnum.GENERIC)) + ) ) - ) - ); - }; - - const authorizePayment = async ( - context: Context - ): Promise => { - const putAuthPaymentTask = (trxCode: string) => - TE.tryCatch( - async () => - await client.putAuthPayment({ - bearerAuth: token, - trxCode - }), - mapFetchError ); + } + ); - const dataResponse = await pipe( - context.transactionData, - O.map(({ trxCode }) => trxCode), - TE.fromOption(() => PaymentFailureEnum.GENERIC), - TE.chain(putAuthPaymentTask) - )(); + const authorizePayment = fromPromise( + async ({ input }) => { + const authPaymentTask = (trxCode: string) => + TE.tryCatch( + async () => + await client.putAuthPayment({ + bearerAuth: token, + trxCode + }), + mapFetchError + ); - return pipe( - dataResponse, - E.fold( - failure => Promise.reject(failure), - flow( - // eslint-disable-next-line sonarjs/no-identical-functions - E.map(({ status, value }) => { - switch (status) { - case 200: - return Promise.resolve(value); - case 401: - return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); - default: - return Promise.reject(mapErrorCodeToFailure(value.code)); - } - }), - E.getOrElse(() => Promise.reject(PaymentFailureEnum.GENERIC)) + const dataResponse = await authPaymentTask(input)(); + + return pipe( + dataResponse, + E.fold( + failure => Promise.reject(failure), + flow( + // eslint-disable-next-line sonarjs/no-identical-functions + E.map(({ status, value }) => { + switch (status) { + case 200: + return Promise.resolve(value); + case 401: + return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); + default: + return Promise.reject(mapErrorCodeToFailure(value.code)); + } + }), + E.getOrElse(() => Promise.reject(PaymentFailureEnum.GENERIC)) + ) ) - ) - ); - }; + ); + } + ); - const deletePayment = async (context: Context): Promise => { + const deletePayment = fromPromise(async ({ input }) => { const deletePaymentTask = (trxCode: string) => TE.tryCatch( async () => @@ -153,12 +95,7 @@ const createServicesImplementation = (client: IDPayClient, token: string) => { mapFetchError ); - const dataResponse = await pipe( - context.transactionData, - O.map(({ trxCode }) => trxCode), - TE.fromOption(() => PaymentFailureEnum.GENERIC), - TE.chain(deletePaymentTask) - )(); + const dataResponse = await deletePaymentTask(input)(); return pipe( dataResponse, @@ -180,7 +117,7 @@ const createServicesImplementation = (client: IDPayClient, token: string) => { ) ) ); - }; + }); return { preAuthorizePayment, @@ -188,5 +125,44 @@ const createServicesImplementation = (client: IDPayClient, token: string) => { deletePayment }; }; +/** + * Maps the backed error codes to UI failure states + * @param code Error code from backend + * @returns The associated failure state + */ +export const mapErrorCodeToFailure = ( + code: TransactionErrorCodeEnum +): PaymentFailureEnum => { + switch (code) { + case TransactionErrorCodeEnum.PAYMENT_TRANSACTION_EXPIRED: + case TransactionErrorCodeEnum.PAYMENT_NOT_FOUND_OR_EXPIRED: + return PaymentFailureEnum.TRANSACTION_EXPIRED; + case TransactionErrorCodeEnum.PAYMENT_USER_SUSPENDED: + return PaymentFailureEnum.USER_SUSPENDED; + case TransactionErrorCodeEnum.PAYMENT_USER_NOT_ONBOARDED: + return PaymentFailureEnum.USER_NOT_ONBOARDED; + case TransactionErrorCodeEnum.PAYMENT_USER_UNSUBSCRIBED: + return PaymentFailureEnum.USER_UNSUBSCRIBED; + case TransactionErrorCodeEnum.PAYMENT_ALREADY_AUTHORIZED: + return PaymentFailureEnum.ALREADY_AUTHORIZED; + case TransactionErrorCodeEnum.PAYMENT_BUDGET_EXHAUSTED: + return PaymentFailureEnum.BUDGET_EXHAUSTED; + case TransactionErrorCodeEnum.PAYMENT_ALREADY_ASSIGNED: + return PaymentFailureEnum.ALREADY_ASSIGNED; + case TransactionErrorCodeEnum.PAYMENT_INITIATIVE_INVALID_DATE: + return PaymentFailureEnum.INVALID_DATE; + default: + return PaymentFailureEnum.GENERIC; + } +}; -export { createServicesImplementation }; +/** + * This function maps errors from the fetch to the PaymentFailure type + * This helps to know if the error comes from a 429 status code + */ +const mapFetchError = (error: unknown): PaymentFailure => { + if (error === "max-retries") { + return PaymentFailureEnum.TOO_MANY_REQUESTS; + } + return PaymentFailureEnum.GENERIC; +}; diff --git a/ts/features/idpay/payment/xstate/context.ts b/ts/features/idpay/payment/machine/context.ts similarity index 53% rename from ts/features/idpay/payment/xstate/context.ts rename to ts/features/idpay/payment/machine/context.ts index 24ab484b8b9..d13c35af906 100644 --- a/ts/features/idpay/payment/xstate/context.ts +++ b/ts/features/idpay/payment/machine/context.ts @@ -2,14 +2,14 @@ import * as O from "fp-ts/lib/Option"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { PaymentFailure } from "../types/PaymentFailure"; -export type Context = { - trxCode: O.Option; - transactionData: O.Option; - failure: O.Option; -}; +export interface Context { + readonly trxCode: string; + readonly transactionData: O.Option; + readonly failure: O.Option; +} -export const INITIAL_CONTEXT: Context = { - trxCode: O.none, +export const Context: Context = { + trxCode: "", transactionData: O.none, failure: O.none }; diff --git a/ts/features/idpay/payment/machine/events.ts b/ts/features/idpay/payment/machine/events.ts new file mode 100644 index 00000000000..0f5fad3b042 --- /dev/null +++ b/ts/features/idpay/payment/machine/events.ts @@ -0,0 +1,8 @@ +import { GlobalEvents } from "../../../../xstate/types/events"; + +export interface AuthorizePayment { + readonly type: "authorize-payment"; + readonly trxCode: string; +} + +export type Events = GlobalEvents | AuthorizePayment; diff --git a/ts/features/idpay/payment/machine/input.ts b/ts/features/idpay/payment/machine/input.ts new file mode 100644 index 00000000000..3030188249b --- /dev/null +++ b/ts/features/idpay/payment/machine/input.ts @@ -0,0 +1,8 @@ +import * as Context from "./context"; + +export interface Input { + readonly trxCode: string; +} + +export const Input = (input: Input): Promise => + Promise.resolve({ ...Context.Context, ...input }); diff --git a/ts/features/idpay/payment/machine/machine.ts b/ts/features/idpay/payment/machine/machine.ts new file mode 100644 index 00000000000..7c42d98f797 --- /dev/null +++ b/ts/features/idpay/payment/machine/machine.ts @@ -0,0 +1,182 @@ +import * as O from "fp-ts/lib/Option"; +import { assertEvent, assign, fromPromise, setup } from "xstate"; +import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; +import { + LOADING_TAG, + UPSERTING_TAG, + WAITING_USER_INPUT_TAG, + notImplementedStub +} from "../../../../xstate/utils"; +import * as Context from "./context"; +import * as Events from "./events"; +import * as Input from "./input"; + +export const idPayPaymentMachine = setup({ + types: { + input: {} as Input.Input, + context: {} as Context.Context, + events: {} as Events.Events + }, + actors: { + onInit: fromPromise(({ input }) => + Input.Input(input) + ), + preAuthorizePayment: fromPromise( + notImplementedStub + ), + deletePayment: fromPromise(notImplementedStub), + authorizePayment: fromPromise( + notImplementedStub + ) + }, + actions: { + navigateToAuthorizationScreen: notImplementedStub, + navigateToResultScreen: notImplementedStub, + handleSessionExpired: notImplementedStub, + closeAuthorization: notImplementedStub, + setFailure: notImplementedStub, + showErrorToast: notImplementedStub + }, + guards: { + isSessionExpired: () => false, + isBlockingFailure: () => false + } +}).createMachine({ + context: Context.Context, + id: "idpay-payment", + initial: "Idle", + states: { + Idle: { + tags: [LOADING_TAG], + on: { + "authorize-payment": { + target: "PreAuthorizing" + } + } + }, + PreAuthorizing: { + tags: [LOADING_TAG], + entry: "navigateToAuthorizationScreen", + invoke: { + id: "preAuthorizePayment", + src: "preAuthorizePayment", + input: ({ event }) => { + assertEvent(event, "authorize-payment"); + return event.trxCode; + }, + onDone: { + actions: assign(({ event }) => ({ + transactionData: O.some(event.output) + })), + target: "AwaitingConfirmation" + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + target: "AuthorizationFailure" + } + ] + } + }, + + AwaitingConfirmation: { + tags: [WAITING_USER_INPUT_TAG], + on: { + next: { + target: "Authorizing" + }, + close: { + target: "Cancelling" + } + } + }, + + Cancelling: { + tags: [UPSERTING_TAG], + invoke: { + id: "deletePayment", + src: "deletePayment", + input: ({ context }) => context.trxCode, + onDone: { + target: "AuthorizationCancelled" + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: "setFailure", + guard: "isBlockingFailure", + target: "AuthorizationFailure" + }, + { + actions: ["setFailure", "showErrorToast"], + target: "AwaitingConfirmation" + } + ] + } + }, + + Authorizing: { + tags: [UPSERTING_TAG], + invoke: { + id: "authorizePayment", + src: "authorizePayment", + input: ({ context }) => context.trxCode, + onDone: { + target: "AuthorizationSuccess" + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: "setFailure", + guard: "isBlockingFailure", + target: "AuthorizationFailure" + }, + { + actions: ["setFailure", "showErrorToast"], + target: "AwaitingConfirmation" + } + ] + } + }, + + AuthorizationSuccess: { + entry: "navigateToResultScreen", + on: { + close: { + actions: "closeAuthorization" + } + } + }, + + AuthorizationCancelled: { + entry: "navigateToResultScreen", + on: { + close: { + actions: "closeAuthorization" + } + } + }, + + AuthorizationFailure: { + entry: "navigateToResultScreen", + on: { + close: { + actions: "closeAuthorization" + } + } + }, + + SessionExpired: { + entry: ["handleSessionExpired", "closeAuthorization"] + } + } +}); diff --git a/ts/features/idpay/payment/xstate/provider.tsx b/ts/features/idpay/payment/machine/provider.tsx similarity index 53% rename from ts/features/idpay/payment/xstate/provider.tsx rename to ts/features/idpay/payment/machine/provider.tsx index 0f058dc5767..455d60a87dd 100644 --- a/ts/features/idpay/payment/xstate/provider.tsx +++ b/ts/features/idpay/payment/machine/provider.tsx @@ -1,70 +1,58 @@ -import { useInterpret } from "@xstate/react"; +import { createActorContext } from "@xstate/react"; import * as O from "fp-ts/lib/Option"; import React from "react"; -import { InterpreterFrom } from "xstate"; import { idPayApiBaseUrl, idPayApiUatBaseUrl, idPayTestToken } from "../../../../config"; -import { useXStateMachine } from "../../../../xstate/hooks/useXStateMachine"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { sessionInfoSelector } from "../../../../store/reducers/authentication"; import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; -import { IDPayPaymentMachineType, createIDPayPaymentMachine } from "./machine"; -import { createServicesImplementation } from "./services"; - -type PaymentMachineContext = InterpreterFrom; - -const PaymentMachineContext = React.createContext( - {} as PaymentMachineContext -); +import { idPayPaymentMachine } from "./machine"; +import { createActorsImplementation } from "./actors"; type Props = { children: React.ReactNode; }; -const IDPayPaymentMachineProvider = (props: Props) => { +export const IdPayPaymentMachineContext = + createActorContext(idPayPaymentMachine); + +export const IdPayPaymentMachineProvider = (props: Props) => { + const navigation = useIONavigation(); const dispatch = useIODispatch(); - const [machine] = useXStateMachine(createIDPayPaymentMachine); const sessionInfo = useIOSelector(sessionInfoSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - const navigation = useIONavigation(); - if (O.isNone(sessionInfo)) { throw new Error("Session info is undefined"); } const { bpdToken } = sessionInfo.value; - const idPayToken = idPayTestToken ?? bpdToken; - const IDPayPaymentClient = createIDPayClient( isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); + const actors = createActorsImplementation( + IDPayPaymentClient, + idPayTestToken ?? bpdToken + ); const actions = createActionsImplementation(navigation, dispatch); - const services = createServicesImplementation(IDPayPaymentClient, idPayToken); - - const machineService = useInterpret(machine, { services, actions }); + const machine = idPayPaymentMachine.provide({ + actors, + actions + }); return ( - + {props.children} - + ); }; - -const usePaymentMachineService = () => React.useContext(PaymentMachineContext); - -export { - IDPayPaymentMachineProvider, - usePaymentMachineService, - PaymentMachineContext -}; diff --git a/ts/features/idpay/payment/machine/selectors.ts b/ts/features/idpay/payment/machine/selectors.ts new file mode 100644 index 00000000000..1e8cbba7e1e --- /dev/null +++ b/ts/features/idpay/payment/machine/selectors.ts @@ -0,0 +1,19 @@ +import { SnapshotFrom } from "xstate"; +import { idPayPaymentMachine } from "./machine"; + +type MachineSnapshot = SnapshotFrom; + +export const isAuthorizingSelector = (snapshot: MachineSnapshot) => + snapshot.matches("Authorizing"); + +export const isCancellingSelector = (snapshot: MachineSnapshot) => + snapshot.matches("Cancelling"); + +export const isCancelledSelector = (snapshot: MachineSnapshot) => + snapshot.matches("AuthorizationCancelled"); + +export const failureSelector = (snapshot: MachineSnapshot) => + snapshot.context.failure; + +export const transactionDataSelector = (snapshot: MachineSnapshot) => + snapshot.context.transactionData; diff --git a/ts/features/idpay/payment/navigation/navigator.tsx b/ts/features/idpay/payment/navigation/navigator.tsx index 1f78fc47a2c..c9cd48badb9 100644 --- a/ts/features/idpay/payment/navigation/navigator.tsx +++ b/ts/features/idpay/payment/navigation/navigator.tsx @@ -1,65 +1,33 @@ -import { ParamListBase, RouteProp } from "@react-navigation/native"; -import { - StackNavigationProp, - createStackNavigator -} from "@react-navigation/stack"; +import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; -import { - IDPayPaymentAuthorizationScreen, - IDPayPaymentAuthorizationScreenRouteParams -} from "../screens/IDPayPaymentAuthorizationScreen"; +import { IdPayPaymentMachineProvider } from "../machine/provider"; +import { IDPayPaymentAuthorizationScreen } from "../screens/IDPayPaymentAuthorizationScreen"; import { IDPayPaymentCodeInputScreen } from "../screens/IDPayPaymentCodeInputScreen"; import { IDPayPaymentResultScreen } from "../screens/IDPayPaymentResultScreen"; -import { IDPayPaymentMachineProvider } from "../xstate/provider"; +import { IdPayPaymentParamsList } from "./params"; +import { IdPayPaymentRoutes } from "./routes"; -export const IDPayPaymentRoutes = { - IDPAY_PAYMENT_MAIN: "IDPAY_PAYMENT_MAIN", - IDPAY_PAYMENT_CODE_SCAN: "IDPAY_PAYMENT_CODE_SCAN", - IDPAY_PAYMENT_CODE_INPUT: "IDPAY_PAYMENT_CODE_INPUT", - IDPAY_PAYMENT_AUTHORIZATION: "IDPAY_PAYMENT_AUTHORIZATION", - IDPAY_PAYMENT_RESULT: "IDPAY_PAYMENT_RESULT" -} as const; - -export type IDPayPaymentParamsList = { - [IDPayPaymentRoutes.IDPAY_PAYMENT_CODE_INPUT]: undefined; - [IDPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION]: IDPayPaymentAuthorizationScreenRouteParams; - [IDPayPaymentRoutes.IDPAY_PAYMENT_RESULT]: undefined; -}; - -const Stack = createStackNavigator(); +const Stack = createStackNavigator(); export const IDPayPaymentNavigator = () => ( - + - + ); - -export type IDPayPaymentStackNavigationRouteProps< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = { - navigation: IDPayPaymentStackNavigationProp; - route: RouteProp; -}; - -export type IDPayPaymentStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = StackNavigationProp; diff --git a/ts/features/idpay/payment/navigation/params.ts b/ts/features/idpay/payment/navigation/params.ts new file mode 100644 index 00000000000..238640b484d --- /dev/null +++ b/ts/features/idpay/payment/navigation/params.ts @@ -0,0 +1,9 @@ +import { IDPayPaymentAuthorizationScreenRouteParams } from "../screens/IDPayPaymentAuthorizationScreen"; +import { IdPayPaymentRoutes } from "./routes"; + +export type IdPayPaymentParamsList = { + [IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: undefined; + [IdPayPaymentRoutes.IDPAY_PAYMENT_CODE_INPUT]: undefined; + [IdPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION]: IDPayPaymentAuthorizationScreenRouteParams; + [IdPayPaymentRoutes.IDPAY_PAYMENT_RESULT]: undefined; +}; diff --git a/ts/features/idpay/payment/navigation/routes.ts b/ts/features/idpay/payment/navigation/routes.ts new file mode 100644 index 00000000000..99d02a33635 --- /dev/null +++ b/ts/features/idpay/payment/navigation/routes.ts @@ -0,0 +1,7 @@ +export const IdPayPaymentRoutes = { + IDPAY_PAYMENT_MAIN: "IDPAY_PAYMENT_MAIN", + IDPAY_PAYMENT_CODE_SCAN: "IDPAY_PAYMENT_CODE_SCAN", + IDPAY_PAYMENT_CODE_INPUT: "IDPAY_PAYMENT_CODE_INPUT", + IDPAY_PAYMENT_AUTHORIZATION: "IDPAY_PAYMENT_AUTHORIZATION", + IDPAY_PAYMENT_RESULT: "IDPAY_PAYMENT_RESULT" +} as const; diff --git a/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx index ab353b9a00c..67235609d1c 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx @@ -1,18 +1,17 @@ -import { RouteProp, useRoute } from "@react-navigation/native"; -import { useSelector } from "@xstate/react"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import React from "react"; -import { SafeAreaView, View } from "react-native"; import { - Icon, + ContentWrapper, Divider, + H6, HSpacer, - VSpacer, - ContentWrapper, + Icon, ListItemInfo, - H6 + VSpacer } from "@pagopa/io-app-design-system"; +import { RouteProp, useRoute } from "@react-navigation/native"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import React from "react"; +import { SafeAreaView, View } from "react-native"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { H1 } from "../../../../components/core/typography/H1"; import { H3 } from "../../../../components/core/typography/H3"; @@ -20,62 +19,58 @@ import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import I18n from "../../../../i18n"; +import { identificationRequest } from "../../../../store/actions/identification"; +import { useIODispatch } from "../../../../store/hooks"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { + isLoadingSelector, + isUpseringSelector +} from "../../../../xstate/selectors"; import { Skeleton } from "../../common/components/Skeleton"; import { formatDateOrDefault, formatNumberCurrencyCents, formatNumberCurrencyCentsOrDefault } from "../../common/utils/strings"; -import { IDPayPaymentParamsList } from "../navigation/navigator"; -import { usePaymentMachineService } from "../xstate/provider"; +import { IdPayPaymentMachineContext } from "../machine/provider"; import { - selectIsAuthorizing, - selectIsCancelling, - selectIsPreAuthorizing, - selectTransactionData -} from "../xstate/selectors"; -import { useIODispatch } from "../../../../store/hooks"; -import { identificationRequest } from "../../../../store/actions/identification"; + isAuthorizingSelector, + isCancellingSelector, + transactionDataSelector +} from "../machine/selectors"; +import { IdPayPaymentParamsList } from "../navigation/params"; export type IDPayPaymentAuthorizationScreenRouteParams = { trxCode?: string; }; type IDPayPaymentAuthorizationRouteProps = RouteProp< - IDPayPaymentParamsList, + IdPayPaymentParamsList, "IDPAY_PAYMENT_AUTHORIZATION" >; const IDPayPaymentAuthorizationScreen = () => { - const route = useRoute(); - - const machine = usePaymentMachineService(); + const { useActorRef, useSelector } = IdPayPaymentMachineContext; + const { params } = useRoute(); + const machine = useActorRef(); const dispatch = useIODispatch(); - const transactionData = useSelector(machine, selectTransactionData); - - const { trxCode } = route.params; - React.useEffect(() => { pipe( - trxCode, + params.trxCode, O.fromNullable, - O.map(code => - machine.send({ type: "START_AUTHORIZATION", trxCode: code }) - ) + O.map(code => machine.send({ type: "authorize-payment", trxCode: code })) ); - }, [trxCode, machine]); + }, [params, machine]); - // Loading state for screen content - const isLoading = useSelector(machine, selectIsPreAuthorizing); - // Loading state for "Confirm" button - const isAuthorizing = useSelector(machine, selectIsAuthorizing); - const isCancelling = useSelector(machine, selectIsCancelling); - const isUpserting = isAuthorizing || isCancelling; + const transactionData = useSelector(transactionDataSelector); + const isLoading = useSelector(isLoadingSelector); + const isUpserting = useSelector(isUpseringSelector); + const isAuthorizing = useSelector(isAuthorizingSelector); + const isCancelling = useSelector(isCancellingSelector); const handleCancel = () => { - machine.send("CANCEL_AUTHORIZATION"); + machine.send({ type: "close" }); }; const handleConfirm = () => { @@ -89,15 +84,15 @@ const IDPayPaymentAuthorizationScreen = () => { onCancel: () => undefined }, { - onSuccess: () => machine.send("CONFIRM_AUTHORIZATION") + onSuccess: () => machine.send({ type: "next" }) } ) ); }; const renderContent = () => { - if (!isLoading && transactionData !== undefined) { - return ; + if (!isLoading && O.isSome(transactionData)) { + return ; } return ; }; diff --git a/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx index 2f061bacc3b..3363334e64d 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx @@ -7,7 +7,6 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; -import { useSelector } from "@xstate/react"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; @@ -19,8 +18,8 @@ import BaseScreenComponent from "../../../../components/screens/BaseScreenCompon import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { IDPayTransactionCode } from "../common/types"; -import { usePaymentMachineService } from "../xstate/provider"; -import { isLoadingSelector } from "../xstate/selectors"; +import { IdPayPaymentMachineContext } from "../machine/provider"; +import { isLoadingSelector } from "../../../../xstate/selectors"; type InputState = { value?: string; @@ -28,21 +27,23 @@ type InputState = { }; const IDPayPaymentCodeInputScreen = () => { - const machine = usePaymentMachineService(); + const { useActorRef, useSelector } = IdPayPaymentMachineContext; + const machine = useActorRef(); + const [inputState, setInputState] = React.useState({ value: undefined, code: O.none }); const isInputValid = pipe(inputState.code, O.map(E.isRight), O.toUndefined); - const isLoading = useSelector(machine, isLoadingSelector); + const isLoading = useSelector(isLoadingSelector); const navigateToPaymentAuthorization = () => pipe( inputState.code, O.filter(E.isRight), O.map(trxCode => trxCode.right), - O.map(trxCode => machine.send("START_AUTHORIZATION", { trxCode })) + O.map(trxCode => machine.send({ type: "authorize-payment", trxCode })) ); return ( diff --git a/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx index eaed669f2bf..9b8620e5246 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentCodeScanScreen.tsx @@ -1,10 +1,10 @@ +import { IOToast } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/native"; import React from "react"; import { Alert } from "react-native"; import ReactNativeHapticFeedback, { HapticFeedbackTypes } from "react-native-haptic-feedback"; -import { IOToast } from "@pagopa/io-app-design-system"; import { useOpenDeepLink } from "../../../../hooks/useOpenDeepLink"; import I18n from "../../../../i18n"; import { @@ -22,7 +22,7 @@ import { } from "../../../barcode"; import * as analytics from "../../../barcode/analytics"; import { IOBarcodeOrigin } from "../../../barcode/types/IOBarcode"; -import { IDPayPaymentRoutes } from "../navigation/navigator"; +import { IdPayPaymentRoutes } from "../navigation/routes"; const IDPayPaymentCodeScanScreen = () => { const navigation = useNavigation>(); @@ -68,8 +68,8 @@ const IDPayPaymentCodeScanScreen = () => { const navigateToCodeInputScreen = () => { analytics.trackBarcodeManualEntryPath("idpay"); - navigation.navigate(IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { - screen: IDPayPaymentRoutes.IDPAY_PAYMENT_CODE_INPUT + navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { + screen: IdPayPaymentRoutes.IDPAY_PAYMENT_CODE_INPUT }); }; diff --git a/ts/features/idpay/payment/screens/IDPayPaymentResultScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentResultScreen.tsx index 9e7eacdfafb..283b88e8069 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentResultScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentResultScreen.tsx @@ -1,46 +1,34 @@ -import { useSelector } from "@xstate/react"; import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; import { default as React } from "react"; import { OperationResultScreenContent, OperationResultScreenContentProps } from "../../../../components/screens/OperationResultScreenContent"; import I18n from "../../../../i18n"; +import { IdPayPaymentMachineContext } from "../machine/provider"; +import { failureSelector, isCancelledSelector } from "../machine/selectors"; import { PaymentFailureEnum } from "../types/PaymentFailure"; -import { usePaymentMachineService } from "../xstate/provider"; -import { - selectFailureOption, - selectIsCancelled, - selectIsFailure -} from "../xstate/selectors"; const IDPayPaymentResultScreen = () => { - const machine = usePaymentMachineService(); + const { useActorRef, useSelector } = IdPayPaymentMachineContext; + const machine = useActorRef(); - const failureOption = useSelector(machine, selectFailureOption); - const isCancelled = useSelector(machine, selectIsCancelled); - const isFailure = useSelector(machine, selectIsFailure); + const failureOption = useSelector(failureSelector); + const isCancelled = useSelector(isCancelledSelector); const defaultCloseAction = React.useMemo( () => ({ label: I18n.t("global.buttons.close"), accessibilityLabel: I18n.t("global.buttons.close"), - onPress: () => machine.send({ type: "EXIT" }) + onPress: () => machine.send({ type: "close" }) }), [machine] ); - if (isFailure) { - const failureContentProps = pipe( - failureOption, - O.map(mapFailureToContentProps), - O.getOrElse(() => genericErrorProps) - ); - + if (O.isSome(failureOption)) { return ( diff --git a/ts/features/idpay/payment/screens/__tests__/IDPayPaymentResultScreen.test.tsx b/ts/features/idpay/payment/screens/__tests__/IDPayPaymentResultScreen.test.tsx index 4d9a7e41685..64c19f671fa 100644 --- a/ts/features/idpay/payment/screens/__tests__/IDPayPaymentResultScreen.test.tsx +++ b/ts/features/idpay/payment/screens/__tests__/IDPayPaymentResultScreen.test.tsx @@ -7,15 +7,15 @@ import { appReducer } from "../../../../../store/reducers"; import { GlobalState } from "../../../../../store/reducers/types"; import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; import { IDPayPaymentRoutes } from "../../navigation/navigator"; -import { Context, INITIAL_CONTEXT } from "../../xstate/context"; +import { Context, INITIAL_CONTEXT } from "../../machine/context"; import { PaymentFailureEnum } from "../../types/PaymentFailure"; -import { createIDPayPaymentMachine } from "../../xstate/machine"; -import { PaymentMachineContext } from "../../xstate/provider"; +import { createIDPayPaymentMachine } from "../../machine/machine"; +import { PaymentMachineContext } from "../../machine/provider"; import { selectFailureOption, selectIsCancelled, selectIsFailure -} from "../../xstate/selectors"; +} from "../../machine/selectors"; import { IDPayPaymentResultScreen } from "../IDPayPaymentResultScreen"; jest.mock("../../xstate/selectors", () => { diff --git a/ts/features/idpay/payment/xstate/__tests__/machine.test.ts b/ts/features/idpay/payment/xstate/__tests__/machine.test.ts deleted file mode 100644 index e00b5d28690..00000000000 --- a/ts/features/idpay/payment/xstate/__tests__/machine.test.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { waitFor } from "@testing-library/react-native"; -import * as O from "fp-ts/lib/Option"; -import { interpret } from "xstate"; -import { - AuthPaymentResponseDTO, - StatusEnum -} from "../../../../../../definitions/idpay/AuthPaymentResponseDTO"; -import { PaymentFailureEnum } from "../../types/PaymentFailure"; -import { createIDPayPaymentMachine } from "../machine"; - -const T_TRX_CODE = "ABCD1234"; -const T_TRANSACTION_DATA_DTO: AuthPaymentResponseDTO = { - amountCents: 100, - id: "", - initiativeId: "", - status: StatusEnum.AUTHORIZED, - trxCode: T_TRX_CODE -}; - -describe("IDPay Payment machine", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should have the default state of AWAITING_TRX_CODE", () => { - const machine = createIDPayPaymentMachine(); - expect(machine.initialState.value).toEqual("AWAITING_TRX_CODE"); - }); - - it("should authorize payment on happy path", async () => { - const mockPreAuthorizePayment = jest.fn(async () => - Promise.resolve(T_TRANSACTION_DATA_DTO) - ); - const mockAuthorizePayment = jest.fn(async () => - Promise.resolve(T_TRANSACTION_DATA_DTO) - ); - - const mockExitAuthorization = jest.fn(); - const mockNavigateToAuthorizationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockShowErrorToast = jest.fn(); - - const machine = createIDPayPaymentMachine().withConfig({ - services: { - preAuthorizePayment: mockPreAuthorizePayment, - authorizePayment: mockAuthorizePayment, - deletePayment: jest.fn() - }, - actions: { - exitAuthorization: mockExitAuthorization, - navigateToAuthorizationScreen: mockNavigateToAuthorizationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - showErrorToast: mockShowErrorToast, - handleSessionExpired: jest.fn() - } - }); - - // eslint-disable-next-line functional/no-let - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("AWAITING_TRX_CODE"); - - service.send({ - type: "START_AUTHORIZATION", - trxCode: T_TRX_CODE - }); - - expect(currentState.value).toEqual("PRE_AUTHORIZING"); - - expect(currentState.context).toHaveProperty("trxCode", O.some(T_TRX_CODE)); - - await waitFor(() => - expect(mockPreAuthorizePayment).toHaveBeenCalledTimes(1) - ); - await waitFor(() => - expect(mockNavigateToAuthorizationScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AWAITING_USER_CONFIRMATION"); - - expect(currentState.context).toHaveProperty( - "transactionData", - O.some(T_TRANSACTION_DATA_DTO) - ); - - service.send({ - type: "CONFIRM_AUTHORIZATION" - }); - - expect(currentState.value).toEqual("AUTHORIZING"); - - await waitFor(() => expect(mockAuthorizePayment).toHaveBeenCalledTimes(1)); - await waitFor(() => - expect(mockNavigateToResultScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AUTHORIZATION_SUCCESS"); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitAuthorization).toHaveBeenCalledTimes(1)); - }); - - it("should show failure screen if pre-authorization fails", async () => { - const mockPreAuthorizePayment = jest.fn(async () => - Promise.reject(PaymentFailureEnum.GENERIC) - ); - - const mockExitAuthorization = jest.fn(); - const mockNavigateToAuthorizationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - - const machine = createIDPayPaymentMachine().withConfig({ - services: { - preAuthorizePayment: mockPreAuthorizePayment, - authorizePayment: jest.fn(), - deletePayment: jest.fn() - }, - actions: { - exitAuthorization: mockExitAuthorization, - navigateToAuthorizationScreen: mockNavigateToAuthorizationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - showErrorToast: jest.fn(), - handleSessionExpired: jest.fn() - } - }); - - // eslint-disable-next-line functional/no-let - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("AWAITING_TRX_CODE"); - - service.send({ - type: "START_AUTHORIZATION", - trxCode: T_TRX_CODE - }); - - expect(currentState.value).toEqual("PRE_AUTHORIZING"); - - expect(currentState.context).toHaveProperty("trxCode", O.some(T_TRX_CODE)); - - await waitFor(() => - expect(mockNavigateToAuthorizationScreen).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockPreAuthorizePayment).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AUTHORIZATION_FAILURE"); - - await waitFor(() => - expect(mockNavigateToResultScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.context).toHaveProperty( - "failure", - O.some(PaymentFailureEnum.GENERIC) - ); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitAuthorization).toHaveBeenCalled()); - }); - - it("should show failure screen if authorization fails", async () => { - const mockPreAuthorizePayment = jest.fn(async () => - Promise.resolve(T_TRANSACTION_DATA_DTO) - ); - const mockAuthorizePayment = jest.fn(async () => - Promise.reject(PaymentFailureEnum.GENERIC) - ); - - const mockExitAuthorization = jest.fn(); - const mockNavigateToAuthorizationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockShowErrorToast = jest.fn(); - - const machine = createIDPayPaymentMachine().withConfig({ - services: { - preAuthorizePayment: mockPreAuthorizePayment, - authorizePayment: mockAuthorizePayment, - deletePayment: jest.fn() - }, - actions: { - exitAuthorization: mockExitAuthorization, - navigateToAuthorizationScreen: mockNavigateToAuthorizationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - showErrorToast: mockShowErrorToast, - handleSessionExpired: jest.fn() - } - }); - - // eslint-disable-next-line functional/no-let - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("AWAITING_TRX_CODE"); - - service.send({ - type: "START_AUTHORIZATION", - trxCode: T_TRX_CODE - }); - - expect(currentState.value).toEqual("PRE_AUTHORIZING"); - - expect(currentState.context).toHaveProperty("trxCode", O.some(T_TRX_CODE)); - - await waitFor(() => - expect(mockPreAuthorizePayment).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockNavigateToAuthorizationScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AWAITING_USER_CONFIRMATION"); - - expect(currentState.context).toHaveProperty( - "transactionData", - O.some(T_TRANSACTION_DATA_DTO) - ); - - service.send({ - type: "CONFIRM_AUTHORIZATION" - }); - - expect(currentState.value).toEqual("AUTHORIZING"); - - await waitFor(() => expect(mockAuthorizePayment).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(mockShowErrorToast).toHaveBeenCalledTimes(0)); - await waitFor(() => - expect(mockNavigateToResultScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AUTHORIZATION_FAILURE"); - - expect(currentState.context).toHaveProperty( - "failure", - O.some(PaymentFailureEnum.GENERIC) - ); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitAuthorization).toHaveBeenCalled()); - }); - - it("should show failure toast if authorization fails with non-blocking error", async () => { - const mockPreAuthorizePayment = jest.fn(async () => - Promise.resolve(T_TRANSACTION_DATA_DTO) - ); - const mockAuthorizePayment = jest.fn(async () => - Promise.reject(PaymentFailureEnum.TOO_MANY_REQUESTS) - ); - - const mockExitAuthorization = jest.fn(); - const mockNavigateToAuthorizationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockShowErrorToast = jest.fn(); - - const machine = createIDPayPaymentMachine().withConfig({ - services: { - preAuthorizePayment: mockPreAuthorizePayment, - authorizePayment: mockAuthorizePayment, - deletePayment: jest.fn() - }, - actions: { - exitAuthorization: mockExitAuthorization, - navigateToAuthorizationScreen: mockNavigateToAuthorizationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - showErrorToast: mockShowErrorToast, - handleSessionExpired: jest.fn() - } - }); - - // eslint-disable-next-line functional/no-let - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("AWAITING_TRX_CODE"); - - service.send({ - type: "START_AUTHORIZATION", - trxCode: T_TRX_CODE - }); - - expect(currentState.value).toEqual("PRE_AUTHORIZING"); - - expect(currentState.context).toHaveProperty("trxCode", O.some(T_TRX_CODE)); - - await waitFor(() => - expect(mockPreAuthorizePayment).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockNavigateToAuthorizationScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AWAITING_USER_CONFIRMATION"); - - expect(currentState.context).toHaveProperty( - "transactionData", - O.some(T_TRANSACTION_DATA_DTO) - ); - - service.send({ - type: "CONFIRM_AUTHORIZATION" - }); - - expect(currentState.value).toEqual("AUTHORIZING"); - - await waitFor(() => expect(mockAuthorizePayment).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(mockShowErrorToast).toHaveBeenCalledTimes(1)); - await waitFor(() => - expect(mockNavigateToResultScreen).toHaveBeenCalledTimes(0) - ); - - expect(currentState.value).toEqual("AWAITING_USER_CONFIRMATION"); - - expect(currentState.context).toHaveProperty( - "failure", - O.some(PaymentFailureEnum.TOO_MANY_REQUESTS) - ); - }); - - it("should cancel payment", async () => { - const mockPreAuthorizePayment = jest.fn(async () => - Promise.resolve(T_TRANSACTION_DATA_DTO) - ); - const mockAuthorizePayment = jest.fn(async () => - Promise.resolve(T_TRANSACTION_DATA_DTO) - ); - const mockDeletePayment = jest.fn(async () => Promise.resolve(undefined)); - - const mockExitAuthorization = jest.fn(); - const mockNavigateToAuthorizationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockShowErrorToast = jest.fn(); - - const machine = createIDPayPaymentMachine().withConfig({ - services: { - preAuthorizePayment: mockPreAuthorizePayment, - authorizePayment: mockAuthorizePayment, - deletePayment: mockDeletePayment - }, - actions: { - exitAuthorization: mockExitAuthorization, - navigateToAuthorizationScreen: mockNavigateToAuthorizationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - showErrorToast: mockShowErrorToast, - handleSessionExpired: jest.fn() - } - }); - - // eslint-disable-next-line functional/no-let - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("AWAITING_TRX_CODE"); - - service.send({ - type: "START_AUTHORIZATION", - trxCode: T_TRX_CODE - }); - - expect(currentState.value).toEqual("PRE_AUTHORIZING"); - - expect(currentState.context).toHaveProperty("trxCode", O.some(T_TRX_CODE)); - - await waitFor(() => - expect(mockPreAuthorizePayment).toHaveBeenCalledTimes(1) - ); - await waitFor(() => - expect(mockNavigateToAuthorizationScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AWAITING_USER_CONFIRMATION"); - - expect(currentState.context).toHaveProperty( - "transactionData", - O.some(T_TRANSACTION_DATA_DTO) - ); - - service.send({ - type: "CANCEL_AUTHORIZATION" - }); - - expect(currentState.value).toEqual("CANCELLING"); - - await waitFor(() => expect(mockAuthorizePayment).toHaveBeenCalledTimes(0)); - await waitFor(() => expect(mockDeletePayment).toHaveBeenCalledTimes(1)); - await waitFor(() => - expect(mockNavigateToResultScreen).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toEqual("AUTHORIZATION_CANCELLED"); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitAuthorization).toHaveBeenCalledTimes(1)); - }); -}); diff --git a/ts/features/idpay/payment/xstate/__tests__/services.test.ts b/ts/features/idpay/payment/xstate/__tests__/services.test.ts deleted file mode 100644 index cf13a7eea8f..00000000000 --- a/ts/features/idpay/payment/xstate/__tests__/services.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; -import { - AuthPaymentResponseDTO, - StatusEnum -} from "../../../../../../definitions/idpay/AuthPaymentResponseDTO"; -import { mockIDPayClient } from "../../../common/api/__mocks__/client"; -import { Context, INITIAL_CONTEXT } from "../context"; -import { PaymentFailureEnum } from "../../types/PaymentFailure"; -import { - createServicesImplementation, - mapErrorCodeToFailure -} from "../services"; -import { - CodeEnum, - TransactionErrorDTO -} from "../../../../../../definitions/idpay/TransactionErrorDTO"; - -const T_AUTH_TOKEN = "abc123"; -const T_TRX_CODE = "ABCD1234"; -const T_TRANSACTION_DATA_DTO: AuthPaymentResponseDTO = { - amountCents: 100, - id: "", - initiativeId: "", - status: StatusEnum.AUTHORIZED, - trxCode: T_TRX_CODE -}; - -const T_CONTEXT: Context = INITIAL_CONTEXT; - -// This object maps status code to possibile failures -const possibleFailures: ReadonlyArray<[number, CodeEnum]> = [ - [404, CodeEnum.PAYMENT_NOT_FOUND_OR_EXPIRED], - [403, CodeEnum.PAYMENT_BUDGET_EXHAUSTED], - [403, CodeEnum.PAYMENT_GENERIC_REJECTED], - [429, CodeEnum.PAYMENT_TOO_MANY_REQUESTS], - [500, CodeEnum.PAYMENT_GENERIC_ERROR] -]; - -describe("IDPay Payment machine services", () => { - const services = createServicesImplementation(mockIDPayClient, T_AUTH_TOKEN); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("preAuthorizePayment", () => { - it("should fail if trxCode is not in context", async () => { - await expect( - services.preAuthorizePayment(T_CONTEXT) - ).rejects.toStrictEqual(PaymentFailureEnum.GENERIC); - - expect(mockIDPayClient.putPreAuthPayment).not.toHaveBeenCalled(); - }); - - it("should pre authorize payment", async () => { - const response: E.Either< - Error, - { status: number; value?: AuthPaymentResponseDTO } - > = E.right({ status: 200, value: T_TRANSACTION_DATA_DTO }); - - mockIDPayClient.putPreAuthPayment.mockImplementation(() => response); - - await expect( - services.preAuthorizePayment({ - ...T_CONTEXT, - trxCode: O.some(T_TRX_CODE) - }) - ).resolves.toStrictEqual(T_TRANSACTION_DATA_DTO); - - expect(mockIDPayClient.putPreAuthPayment).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - trxCode: T_TRX_CODE - }) - ); - }); - - test.each(possibleFailures)( - "when status code is %s it should get a %s failure", - async (status, code) => { - const T_FAILURE = mapErrorCodeToFailure(code); - - const response: E.Either< - Error, - { status: number; value?: TransactionErrorDTO } - > = E.right({ - status, - value: { code, message: "" } - }); - - mockIDPayClient.putPreAuthPayment.mockImplementation(() => response); - - await expect( - services.preAuthorizePayment({ - ...T_CONTEXT, - trxCode: O.some(T_TRX_CODE) - }) - ).rejects.toStrictEqual(T_FAILURE); - - expect(mockIDPayClient.putPreAuthPayment).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - trxCode: T_TRX_CODE - }) - ); - } - ); - }); - - describe("authorizePayment", () => { - it("should fail if transactionData is not in context", async () => { - await expect(services.authorizePayment(T_CONTEXT)).rejects.toStrictEqual( - PaymentFailureEnum.GENERIC - ); - - expect(mockIDPayClient.putAuthPayment).not.toHaveBeenCalled(); - }); - - it("should authorize payment", async () => { - const response: E.Either< - Error, - { status: number; value?: AuthPaymentResponseDTO } - > = E.right({ status: 200, value: T_TRANSACTION_DATA_DTO }); - - mockIDPayClient.putAuthPayment.mockImplementation(() => response); - - await expect( - services.authorizePayment({ - ...T_CONTEXT, - transactionData: O.some(T_TRANSACTION_DATA_DTO) - }) - ).resolves.toStrictEqual(T_TRANSACTION_DATA_DTO); - - expect(mockIDPayClient.putAuthPayment).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - trxCode: T_TRX_CODE - }) - ); - }); - - test.each(possibleFailures)( - "when status code is %s it should get a %s failure", - async (status, code) => { - const T_FAILURE = mapErrorCodeToFailure(code); - - const response: E.Either< - Error, - { status: number; value?: TransactionErrorDTO } - > = E.right({ - status, - value: { code, message: "" } - }); - - mockIDPayClient.putAuthPayment.mockImplementation(() => response); - - await expect( - services.authorizePayment({ - ...T_CONTEXT, - transactionData: O.some(T_TRANSACTION_DATA_DTO) - }) - ).rejects.toStrictEqual(T_FAILURE); - - expect(mockIDPayClient.putAuthPayment).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - trxCode: T_TRX_CODE - }) - ); - } - ); - }); - - describe("deletePayment", () => { - it("should fail if transactionData is not in context", async () => { - await expect(services.authorizePayment(T_CONTEXT)).rejects.toStrictEqual( - PaymentFailureEnum.GENERIC - ); - - expect(mockIDPayClient.deletePayment).not.toHaveBeenCalled(); - }); - - it("should delete payment", async () => { - const response: E.Either = - E.right({ status: 200, value: undefined }); - - mockIDPayClient.deletePayment.mockImplementation(() => response); - - await expect( - services.deletePayment({ - ...T_CONTEXT, - transactionData: O.some(T_TRANSACTION_DATA_DTO) - }) - ).resolves.toBeUndefined(); - - expect(mockIDPayClient.deletePayment).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - trxCode: T_TRX_CODE - }) - ); - }); - - test.each(possibleFailures)( - "when status code is %s it should get a %s failure", - async (status, code) => { - const T_FAILURE = mapErrorCodeToFailure(code); - - const response: E.Either< - Error, - { status: number; value?: TransactionErrorDTO } - > = E.right({ - status, - value: { code, message: "" } - }); - - mockIDPayClient.deletePayment.mockImplementation(() => response); - - await expect( - services.deletePayment({ - ...T_CONTEXT, - transactionData: O.some(T_TRANSACTION_DATA_DTO) - }) - ).rejects.toStrictEqual(T_FAILURE); - - expect(mockIDPayClient.deletePayment).toHaveBeenCalledWith( - expect.objectContaining({ - bearerAuth: T_AUTH_TOKEN, - trxCode: T_TRX_CODE - }) - ); - } - ); - }); -}); diff --git a/ts/features/idpay/payment/xstate/events.ts b/ts/features/idpay/payment/xstate/events.ts deleted file mode 100644 index 2e56df8f018..00000000000 --- a/ts/features/idpay/payment/xstate/events.ts +++ /dev/null @@ -1,22 +0,0 @@ -type E_EXIT = { - type: "EXIT"; -}; - -type E_START_AUTHORIZATION = { - type: "START_AUTHORIZATION"; - trxCode: string; -}; - -type E_CANCEL_AUTHORIZATION = { - type: "CANCEL_AUTHORIZATION"; -}; - -type E_CONFIRM_AUTHORIZATION = { - type: "CONFIRM_AUTHORIZATION"; -}; - -export type Events = - | E_EXIT - | E_START_AUTHORIZATION - | E_CANCEL_AUTHORIZATION - | E_CONFIRM_AUTHORIZATION; diff --git a/ts/features/idpay/payment/xstate/machine.ts b/ts/features/idpay/payment/xstate/machine.ts deleted file mode 100644 index b3f30e93fce..00000000000 --- a/ts/features/idpay/payment/xstate/machine.ts +++ /dev/null @@ -1,195 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { assign, createMachine } from "xstate4"; -import { LOADING_TAG, WAITING_USER_INPUT_TAG } from "../../../../xstate/utils"; -import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; -import { Context, INITIAL_CONTEXT } from "./context"; -import { Events } from "./events"; -import { Services } from "./services"; - -const createIDPayPaymentMachine = () => - createMachine( - { - /** @xstate-layout N4IgpgJg5mDOIC5QEkAiAFAggTQPoFUA5AZXwCFiBhAJWXQBVkB5QgYgFEANZegbQAYAuolAAHAPawAlgBcp4gHYiQAD0QBOABwAmAHQBmAIwA2fQHYALP20XN-Y5oA0IAJ6JD2gL6fnaLHiJSCho6RhZdAHVMHmRCAHFcWJjMRgA1dlxidgAZdkowtizc-MTCZLT2AWEkEAlpOUVlNQR9C0NdTQBWQ06LfSMzdUMLTs7nNwRtfnbjPu1DMzNOxeNRs29fDBwCEnIqWgZmQl1UZGJ0bJxYhMoWADFkagBZFKPWW8IH552g-dCjqrKOqyeRKGrNTTGdrWfpaEZ9CzqCzjRDaGy6KHqQbaSH8azdCwbEB+baBPYhQ7hMnBWhka6sCCKMC6KQKABu4gA1syAK4KWA8gBGsAAxgAnKSCsB3MXiAC2yAUIIAhnI2WBATVgQ0waBmj0jLoYeZlnZ+GYHCjJvxNLozPwjDbcfxeupOkSSQFdjT-lTvX86fFWGAxbKxbpRAAbVUAM3EYrluj5AuF4sl0tlCqVclVUnVmrEkhBjXB7k6huNSzMZotTlciCMxjtDo8xmMFsROO8PhACnEEDgyk9P3JBwKQKLOqaiAAtIYrW7dCMjPoHfpZvw3YSe8PqX9KccojF4qVysh0pkcnlx1rJ6DpwgLGYrWjOrpDIYseoccY8doCR6Wxer8FIFCcZwXFcJ4fF8Lw3oW9T3qWCC9G+FgjDieL2ssnR1hMUwzJ+FrmN+nYuoB-gjj6B66HuISBnEE6ISWeruIY+hvsa6jTJomiGHieHuN+75Ee2Zjruo6gcdumyUXRY5HLR-qgUcmT4JQlDsMQxBMcWuqqO4gy2sYbo2BxG4OGM9YIIYOi6G6jporxm42O6O5AVR+5gfJvqELgdzRNk+DUOwulTshCxaBipl9J0FldC+3H2eWHjTPwy4Otu3hAA */ - context: INITIAL_CONTEXT, - tsTypes: {} as import("./machine.typegen").Typegen0, - schema: { - context: {} as Context, - events: {} as Events, - services: {} as Services - }, - predictableActionArguments: true, - id: "IDPAY_PAYMENT", - initial: "AWAITING_TRX_CODE", - states: { - AWAITING_TRX_CODE: { - tags: [WAITING_USER_INPUT_TAG], - on: { - START_AUTHORIZATION: { - target: "PRE_AUTHORIZING", - actions: "startAuthorization" - } - } - }, - - PRE_AUTHORIZING: { - tags: [LOADING_TAG], - entry: "navigateToAuthorizationScreen", - invoke: { - id: "preAuthorizePayment", - src: "preAuthorizePayment", - onDone: { - actions: "setTransactionData", - target: "AWAITING_USER_CONFIRMATION" - }, - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - target: "AUTHORIZATION_FAILURE" - } - ] - } - }, - - AWAITING_USER_CONFIRMATION: { - tags: [WAITING_USER_INPUT_TAG], - on: { - CONFIRM_AUTHORIZATION: { - target: "AUTHORIZING" - }, - CANCEL_AUTHORIZATION: { - target: "CANCELLING" - } - } - }, - - CANCELLING: { - tags: [LOADING_TAG], - invoke: { - id: "deletePayment", - src: "deletePayment", - onDone: { - target: "AUTHORIZATION_CANCELLED" - }, - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - cond: "isBlockingFailure", - target: "AUTHORIZATION_FAILURE" - }, - { - actions: ["setFailure", "showErrorToast"], - target: "AWAITING_USER_CONFIRMATION" - } - ] - } - }, - - AUTHORIZING: { - tags: [LOADING_TAG], - invoke: { - id: "authorizePayment", - src: "authorizePayment", - onDone: { - target: "AUTHORIZATION_SUCCESS" - }, - onError: [ - { - cond: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - actions: "setFailure", - cond: "isBlockingFailure", - target: "AUTHORIZATION_FAILURE" - }, - { - actions: ["setFailure", "showErrorToast"], - target: "AWAITING_USER_CONFIRMATION" - } - ] - } - }, - - AUTHORIZATION_SUCCESS: { - entry: "navigateToResultScreen", - on: { - EXIT: { - actions: "exitAuthorization" - } - } - }, - - AUTHORIZATION_CANCELLED: { - entry: "navigateToResultScreen", - on: { - EXIT: { - actions: "exitAuthorization" - } - } - }, - - AUTHORIZATION_FAILURE: { - entry: "navigateToResultScreen", - on: { - EXIT: { - actions: "exitAuthorization" - } - } - }, - - SESSION_EXPIRED: { - entry: ["handleSessionExpired", "exitAuthorization"] - } - } - }, - { - actions: { - startAuthorization: assign((_, event) => ({ - trxCode: O.some(event.trxCode) - })), - setTransactionData: assign((_, event) => ({ - transactionData: O.some(event.data) - })), - setFailure: assign((_, event) => ({ - failure: pipe(event.data, O.of, O.filter(PaymentFailure.is)) - })) - }, - guards: { - isSessionExpired: (_, event) => - pipe( - event.data, - PaymentFailure.decode, - O.fromEither, - O.filter(failure => failure === PaymentFailureEnum.SESSION_EXPIRED), - O.isSome - ), - // Guard that checks if the failure is blocking or not. - // Currently, the only non-blocking failure is `TOO_MANY_REQUESTS` - // which should display only an error toast - isBlockingFailure: (_, event) => - pipe( - event.data, - PaymentFailure.decode, - O.fromEither, - O.filter( - failure => failure !== PaymentFailureEnum.TOO_MANY_REQUESTS - ), - O.isSome - ) - } - } - ); - -type IDPayPaymentMachineType = ReturnType; - -export { createIDPayPaymentMachine }; -export type { IDPayPaymentMachineType }; diff --git a/ts/features/idpay/payment/xstate/selectors.ts b/ts/features/idpay/payment/xstate/selectors.ts deleted file mode 100644 index 36e162b693d..00000000000 --- a/ts/features/idpay/payment/xstate/selectors.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { createSelector } from "reselect"; -import { StateFrom } from "xstate"; -import { LOADING_TAG } from "../../../../xstate/utils"; -import { IDPayPaymentMachineType } from "./machine"; - -type StateWithContext = StateFrom; - -const selectTags = (state: StateWithContext) => state.tags; - -export const isLoadingSelector = createSelector(selectTags, tags => - tags.has(LOADING_TAG) -); - -export const selectIsPreAuthorizing = (state: StateWithContext) => - state.matches("PRE_AUTHORIZING"); - -export const selectIsAuthorizing = (state: StateWithContext) => - state.matches("AUTHORIZING"); - -export const selectIsCancelling = (state: StateWithContext) => - state.matches("CANCELLING"); - -export const selectIsFailure = (state: StateWithContext) => - state.matches("AUTHORIZATION_FAILURE"); - -export const selectIsCancelled = (state: StateWithContext) => - state.matches("AUTHORIZATION_CANCELLED"); - -export const selectFailureOption = (state: StateWithContext) => - state.context.failure; - -export const selectTransactionData = (state: StateWithContext) => - pipe(state.context.transactionData, O.toUndefined); diff --git a/ts/features/idpay/unsubscription/machine/events.ts b/ts/features/idpay/unsubscription/machine/events.ts index 40ccb68c4d0..1b219a5e5ec 100644 --- a/ts/features/idpay/unsubscription/machine/events.ts +++ b/ts/features/idpay/unsubscription/machine/events.ts @@ -1,3 +1,4 @@ +import { GlobalEvents } from "../../../../xstate/types/events"; import * as Input from "./input"; export interface AutoInit { @@ -5,12 +6,8 @@ export interface AutoInit { readonly input: Input.Input; } -export interface Exit { - readonly type: "exit"; -} - export interface ConfirmUnsubscription { readonly type: "confirm-unsubscription"; } -export type Events = AutoInit | Exit | ConfirmUnsubscription; +export type Events = GlobalEvents | ConfirmUnsubscription; diff --git a/ts/features/idpay/unsubscription/machine/machine.ts b/ts/features/idpay/unsubscription/machine/machine.ts index f5649784f33..4f5e5235a8d 100644 --- a/ts/features/idpay/unsubscription/machine/machine.ts +++ b/ts/features/idpay/unsubscription/machine/machine.ts @@ -99,7 +99,7 @@ export const idPayUnsubscriptionMachine = setup({ "confirm-unsubscription": { target: "Unsubscribing" }, - exit: { + close: { actions: "exitUnsubscription" } } @@ -126,7 +126,7 @@ export const idPayUnsubscriptionMachine = setup({ UnsubscriptionSuccess: { entry: "navigateToResultScreen", on: { - exit: { + close: { actions: "exitToWallet" } } @@ -134,7 +134,7 @@ export const idPayUnsubscriptionMachine = setup({ UnsubscriptionFailure: { entry: "navigateToResultScreen", on: { - exit: { + close: { actions: "exitUnsubscription" } } @@ -144,5 +144,3 @@ export const idPayUnsubscriptionMachine = setup({ } } }); - -export type IdPayUnsubscriptionMachine = typeof idPayUnsubscriptionMachine; diff --git a/ts/features/idpay/unsubscription/machine/selectors.ts b/ts/features/idpay/unsubscription/machine/selectors.ts index 3f0fb1e792c..aab6f4717fd 100644 --- a/ts/features/idpay/unsubscription/machine/selectors.ts +++ b/ts/features/idpay/unsubscription/machine/selectors.ts @@ -1,29 +1,22 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; -import { StateFrom } from "xstate"; +import { SnapshotFrom } from "xstate"; import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import I18n from "../../../../i18n"; -import { LOADING_TAG } from "../../../../xstate/utils"; -import { IdPayUnsubscriptionMachine } from "./machine"; +import { idPayUnsubscriptionMachine } from "./machine"; -type StateWithContext = StateFrom; +type MachineSnapshot = SnapshotFrom; -export const selectInitiativeName = (state: StateWithContext) => - state.context.initiativeName; +export const selectInitiativeName = ({ context }: MachineSnapshot) => + context.initiativeName; -const selectTags = (state: StateWithContext) => state.tags; +export const selectIsFailure = (snapshot: MachineSnapshot) => + snapshot.matches("UnsubscriptionFailure"); -export const isLoadingSelector = createSelector(selectTags, tags => - tags.has(LOADING_TAG) -); - -export const selectIsFailure = (state: StateWithContext) => - state.matches("UnsubscriptionFailure"); - -export const selectInitiativeType = (state: StateWithContext) => +export const selectInitiativeType = ({ context }: MachineSnapshot) => pipe( - state.context.initiativeType, + context.initiativeType, O.fromNullable, O.getOrElse(() => InitiativeRewardTypeEnum.REFUND) ); diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx index 1738f85a1d1..9f91d50e1d7 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx @@ -20,14 +20,13 @@ import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bott import { UnsubscriptionCheckListItem } from "../components/UnsubscriptionCheckListItem"; import { IdPayUnsubscriptionMachineContext } from "../machine/provider"; import { - isLoadingSelector, selectInitiativeName, selectUnsubscriptionChecks } from "../machine/selectors"; +import { isLoadingSelector } from "../../../../xstate/selectors"; const UnsubscriptionConfirmationScreen = () => { const { useActorRef, useSelector } = IdPayUnsubscriptionMachineContext; - const machine = useActorRef(); const isLoading = useSelector(isLoadingSelector); @@ -38,7 +37,7 @@ const UnsubscriptionConfirmationScreen = () => { const handleClosePress = () => machine.send({ - type: "exit" + type: "close" }); const handleConfirmPress = () => { diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx index 9b1ea82de82..ea4aadf94b9 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionResultScreen.tsx @@ -41,7 +41,7 @@ const UnsubscriptionResultScreen = () => { buttonLabel: I18n.t("idpay.unsubscription.success.button") }; - const handleButtonPress = () => machine.send({ type: "exit" }); + const handleButtonPress = () => machine.send({ type: "close" }); return ( diff --git a/ts/navigation/params/AppParamsList.ts b/ts/navigation/params/AppParamsList.ts index 1e676ec827a..4fe69f4a4d1 100644 --- a/ts/navigation/params/AppParamsList.ts +++ b/ts/navigation/params/AppParamsList.ts @@ -30,14 +30,13 @@ import { IDPayDetailsRoutes } from "../../features/idpay/details/navigation"; import { - IdPayOnboardingParamsList, - IdPayOnboardingNavigatorParams + IdPayOnboardingNavigatorParams, + IdPayOnboardingParamsList } from "../../features/idpay/onboarding/navigation/params"; import { IdPayOnboardingRoutes } from "../../features/idpay/onboarding/navigation/routes"; -import { - IDPayPaymentParamsList, - IDPayPaymentRoutes -} from "../../features/idpay/payment/navigation/navigator"; + +import { IdPayPaymentParamsList } from "../../features/idpay/payment/navigation/params"; +import { IdPayPaymentRoutes } from "../../features/idpay/payment/navigation/routes"; import { IdPayUnsubscriptionNavigatorParams, IdPayUnsubscriptionParamsList @@ -109,8 +108,8 @@ export type AppParamsList = { [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR]: | IdPayUnsubscriptionNavigatorParams | NavigatorScreenParams; - [IDPayPaymentRoutes.IDPAY_PAYMENT_CODE_SCAN]: undefined; // FIXME IOBP-383: remove after react-navigation 6.x upgrade. This should be insde IDPAY_PAYMENT_MAIN - [IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: NavigatorScreenParams; + [IdPayPaymentRoutes.IDPAY_PAYMENT_CODE_SCAN]: undefined; // FIXME IOBP-383: remove after react-navigation 6.x upgrade. This should be insde IDPAY_PAYMENT_MAIN + [IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: NavigatorScreenParams; [IdPayCodeRoutes.IDPAY_CODE_MAIN]: NavigatorScreenParams; [IdPayBarcodeRoutes.IDPAY_BARCODE_MAIN]: NavigatorScreenParams; diff --git a/ts/xstate/selectors/index.ts b/ts/xstate/selectors/index.ts new file mode 100644 index 00000000000..b38ce5a029f --- /dev/null +++ b/ts/xstate/selectors/index.ts @@ -0,0 +1,15 @@ +import { createSelector } from "reselect"; +import { AnyActorLogic, SnapshotFrom } from "xstate"; +import { LOADING_TAG, UPSERTING_TAG } from "../utils"; + +type MachineSnapshot = SnapshotFrom; + +const selectTags = ({ tags }: MachineSnapshot) => tags; + +export const isLoadingSelector = createSelector(selectTags, tags => + tags.has(LOADING_TAG) +); + +export const isUpseringSelector = createSelector(selectTags, tags => + tags.has(UPSERTING_TAG) +); diff --git a/yarn.lock b/yarn.lock index 94c005953a5..467e6e88fbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4560,14 +4560,6 @@ dependencies: "@xstate/machine-extractor" "0.7.1" -"@xstate4/react@npm:@xstate/react@3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095" - integrity sha512-/tq/gg92P9ke8J+yDNDBv5/PAxBvXJf2cYyGDByzgtl5wKaxKxzDT82Gj3eWlCJXkrBg4J5/V47//gRJuVH2fA== - dependencies: - use-isomorphic-layout-effect "^1.0.0" - use-sync-external-store "^1.0.0" - "@yarnpkg/lockfile@^1.0.0", "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -16949,7 +16941,7 @@ url-parse@^1.5.9: querystringify "^2.1.1" requires-port "^1.0.0" -use-isomorphic-layout-effect@^1.0.0, use-isomorphic-layout-effect@^1.1.2: +use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== @@ -17363,11 +17355,6 @@ xss@1.0.10: commander "^2.20.3" cssfilter "0.0.10" -"xstate4@npm:xstate@4.33.6": - version "4.33.6" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.33.6.tgz#9e23f78879af106f1de853aba7acb2bc3b1eb950" - integrity sha512-A5R4fsVKADWogK2a43ssu8Fz1AF077SfrKP1ZNyDBD8lNa/l4zfR//Luofp5GSWehOQr36Jp0k2z7b+sH2ivyg== - xstate@^4.29.0: version "4.35.4" resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.35.4.tgz#87b2a45b6c7e84d820f56378408c6531ca5c4662" From 7a2da471a1505f03a6acf35773ea2276337f262c Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Wed, 1 May 2024 23:08:54 +0200 Subject: [PATCH 07/31] chore: xstatre v5 wip --- locales/en/index.yml | 3 +- locales/it/index.yml | 1 + .../barcode/screens/BarcodeScanScreen.tsx | 6 +- .../components/InstrumentEnrollmentSwitch.tsx | 9 +- .../idpay/configuration/machine/actions.ts | 113 ++ .../{xstate/services.ts => machine/actors.ts} | 141 +-- .../idpay/configuration/machine/context.ts | 31 + .../idpay/configuration/machine/events.ts | 61 + .../idpay/configuration/machine/input.ts | 14 + .../idpay/configuration/machine/machine.ts | 699 +++++++++++ .../{xstate => machine}/provider.tsx | 33 +- .../idpay/configuration/machine/selectors.ts | 97 ++ .../configuration/navigation/navigator.tsx | 138 +-- .../idpay/configuration/navigation/params.ts | 19 + .../idpay/configuration/navigation/routes.ts | 12 + .../screens/ConfigurationSuccessScreen.tsx | 48 +- .../IbanConfigurationLandingScreen.tsx | 48 +- .../screens/IbanEnrollmentScreen.tsx | 72 +- .../screens/IbanOnboardingScreen.tsx | 23 +- .../IdPayDiscountInstrumentsScreen.tsx | 43 +- .../InitiativeConfigurationIntroScreen.tsx | 45 +- .../screens/InstrumentsEnrollmentScreen.tsx | 117 +- .../__test__/IbanEnrollmentScreen.test.tsx | 144 --- .../{xstate => types}/failure.ts | 0 .../idpay/configuration/types/index.ts | 12 + .../configuration/xstate/__mocks__/actions.ts | 15 - .../xstate/__mocks__/services.ts | 120 -- .../xstate/__tests__/actions.test.ts | 249 ---- .../xstate/__tests__/machine.test.ts | 1088 ----------------- .../xstate/__tests__/machineIban.test.ts | 198 --- .../__tests__/machineInstruments.test.ts | 276 ----- .../xstate/__tests__/transitions.test.ts | 110 -- .../idpay/configuration/xstate/actions.ts | 158 --- .../idpay/configuration/xstate/context.ts | 43 - .../idpay/configuration/xstate/events.ts | 95 -- .../idpay/configuration/xstate/machine.ts | 805 ------------ .../idpay/configuration/xstate/selectors.ts | 104 -- .../InitiativeDiscountSettingsComponent.tsx | 29 +- .../InitiativeRefundSettingsComponent.tsx | 32 +- .../components/MissingConfigurationAlert.tsx | 29 +- .../useIdPayDiscountDetailsBottomSheet.tsx | 4 +- .../screens/IdPayInitiativeDetailsScreen.tsx | 9 +- .../idpay/onboarding/machine/actions.ts | 16 +- .../idpay/onboarding/machine/actors.ts | 20 +- .../idpay/onboarding/machine/events.ts | 6 +- .../idpay/onboarding/machine/machine.ts | 71 +- .../idpay/onboarding/machine/provider.tsx | 4 +- .../idpay/onboarding/machine/selectors.ts | 41 +- .../screens/BoolValuePrerequisitesScreen.tsx | 2 +- .../onboarding/screens/CompletionScreen.tsx | 2 +- .../screens/InitiativeDetailsScreen.tsx | 3 +- ts/features/idpay/payment/machine/actions.ts | 16 +- ts/features/idpay/payment/machine/actors.ts | 18 +- ts/features/idpay/payment/machine/machine.ts | 72 +- .../idpay/payment/machine/provider.tsx | 5 +- .../idpay/payment/navigation/navigator.tsx | 2 +- .../IDPayPaymentResultScreen.test.tsx | 130 -- .../TimelineRefundDetailsComponent.tsx | 6 +- .../idpay/unsubscription/machine/actions.ts | 36 +- .../idpay/unsubscription/machine/actors.ts | 17 +- .../idpay/unsubscription/machine/events.ts | 2 +- .../idpay/unsubscription/machine/machine.ts | 33 +- .../idpay/unsubscription/machine/provider.tsx | 5 +- .../unsubscription/navigation/navigator.tsx | 2 +- ts/navigation/AuthenticatedStackNavigator.tsx | 40 +- ts/navigation/params/AppParamsList.ts | 14 +- .../playgrounds/IdPayOnboardingPlayground.tsx | 6 +- yarn.lock | 8 +- 68 files changed, 1652 insertions(+), 4218 deletions(-) create mode 100644 ts/features/idpay/configuration/machine/actions.ts rename ts/features/idpay/configuration/{xstate/services.ts => machine/actors.ts} (75%) create mode 100644 ts/features/idpay/configuration/machine/context.ts create mode 100644 ts/features/idpay/configuration/machine/events.ts create mode 100644 ts/features/idpay/configuration/machine/input.ts create mode 100644 ts/features/idpay/configuration/machine/machine.ts rename ts/features/idpay/configuration/{xstate => machine}/provider.tsx (81%) create mode 100644 ts/features/idpay/configuration/machine/selectors.ts create mode 100644 ts/features/idpay/configuration/navigation/params.ts create mode 100644 ts/features/idpay/configuration/navigation/routes.ts delete mode 100644 ts/features/idpay/configuration/screens/__test__/IbanEnrollmentScreen.test.tsx rename ts/features/idpay/configuration/{xstate => types}/failure.ts (100%) create mode 100644 ts/features/idpay/configuration/types/index.ts delete mode 100644 ts/features/idpay/configuration/xstate/__mocks__/actions.ts delete mode 100644 ts/features/idpay/configuration/xstate/__mocks__/services.ts delete mode 100644 ts/features/idpay/configuration/xstate/__tests__/actions.test.ts delete mode 100644 ts/features/idpay/configuration/xstate/__tests__/machine.test.ts delete mode 100644 ts/features/idpay/configuration/xstate/__tests__/machineIban.test.ts delete mode 100644 ts/features/idpay/configuration/xstate/__tests__/machineInstruments.test.ts delete mode 100644 ts/features/idpay/configuration/xstate/__tests__/transitions.test.ts delete mode 100644 ts/features/idpay/configuration/xstate/actions.ts delete mode 100644 ts/features/idpay/configuration/xstate/context.ts delete mode 100644 ts/features/idpay/configuration/xstate/events.ts delete mode 100644 ts/features/idpay/configuration/xstate/machine.ts delete mode 100644 ts/features/idpay/configuration/xstate/selectors.ts delete mode 100644 ts/features/idpay/payment/screens/__tests__/IDPayPaymentResultScreen.test.tsx diff --git a/locales/en/index.yml b/locales/en/index.yml index 3d7867aa5ae..fe20e404238 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -2827,7 +2827,7 @@ features: DELIVERING: Invio in corso EFFECTIVE_DATE: Perfezionata per decorrenza termini UNREACHABLE: Destinatario irreperibile - VIEWED: Avvenuto accesso + VIEWED: Avvenuto accesso PAID: Pagata REFUSED: REFUSED IN_VALIDATION: IN VALIDATION @@ -3352,6 +3352,7 @@ idpay: INSTRUMENTS_LIST_LOAD_FAILURE: "Errore nel caricamento della lista strumenti" INSTRUMENT_ENROLL_FAILURE: "Errore durante l'aggiunta del metodo di pagamento" INSTRUMENT_DELETE_FAILURE: "Errore durante la rimozione del metodo di pagamento" + SESSION_EXPIRED: "C'è stato un problema, riprova" headerTitle: "Configure the initiative" intro: title: "We are almost there" diff --git a/locales/it/index.yml b/locales/it/index.yml index 63ecf5b45bd..5a6bb3ee8ec 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3352,6 +3352,7 @@ idpay: INSTRUMENTS_LIST_LOAD_FAILURE: "Errore nel caricamento della lista strumenti" INSTRUMENT_ENROLL_FAILURE: "Errore durante l'aggiunta del metodo di pagamento" INSTRUMENT_DELETE_FAILURE: "Errore durante la rimozione del metodo di pagamento" + SESSION_EXPIRED: "C'è stato un problema, riprova" headerTitle: "Configura l'iniziativa" intro: title: "Ci siamo quasi" diff --git a/ts/features/barcode/screens/BarcodeScanScreen.tsx b/ts/features/barcode/screens/BarcodeScanScreen.tsx index 4094bc40b59..d914bead30d 100644 --- a/ts/features/barcode/screens/BarcodeScanScreen.tsx +++ b/ts/features/barcode/screens/BarcodeScanScreen.tsx @@ -26,7 +26,7 @@ import { } from "../../../store/reducers/backendStatus"; import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../utils/hooks/bottomSheet"; -import { IDPayPaymentRoutes } from "../../idpay/payment/navigation/navigator"; +import { IdPayPaymentRoutes } from "../../idpay/payment/navigation/routes"; import { PaymentsCheckoutRoutes } from "../../payments/checkout/navigation/routes"; import * as analytics from "../analytics"; import { BarcodeScanBaseScreenComponent } from "../components/BarcodeScanBaseScreenComponent"; @@ -169,8 +169,8 @@ const BarcodeScanScreen = () => { const handleIdPayPaymentCodeInput = () => { manualInputModal.dismiss(); - navigation.navigate(IDPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { - screen: IDPayPaymentRoutes.IDPAY_PAYMENT_CODE_INPUT + navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { + screen: IdPayPaymentRoutes.IDPAY_PAYMENT_CODE_INPUT }); }; diff --git a/ts/features/idpay/configuration/components/InstrumentEnrollmentSwitch.tsx b/ts/features/idpay/configuration/components/InstrumentEnrollmentSwitch.tsx index 707524a1875..7279bdb180f 100644 --- a/ts/features/idpay/configuration/components/InstrumentEnrollmentSwitch.tsx +++ b/ts/features/idpay/configuration/components/InstrumentEnrollmentSwitch.tsx @@ -5,7 +5,6 @@ import { ListItemSwitch } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import { useSelector } from "@xstate/react"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; @@ -13,8 +12,8 @@ import { default as React } from "react"; import { StatusEnum as InstrumentStatusEnum } from "../../../../../definitions/idpay/InstrumentDTO"; import { CreditCardType, Wallet } from "../../../../types/pagopa"; import { instrumentStatusLabels } from "../../common/labels"; -import { useConfigurationMachineService } from "../xstate/provider"; -import { instrumentStatusByIdWalletSelector } from "../xstate/selectors"; +import { IdPayConfigurationMachineContext } from "../machine/provider"; +import { instrumentStatusByIdWalletSelector } from "../machine/selectors"; /** * See @ListItemSwitch @@ -40,11 +39,9 @@ type InstrumentEnrollmentSwitchProps = { */ const InstrumentEnrollmentSwitch = (props: InstrumentEnrollmentSwitchProps) => { const { wallet, isStaged, onValueChange } = props; - - const configurationMachine = useConfigurationMachineService(); + const { useSelector } = IdPayConfigurationMachineContext; const instrumentStatusPot = useSelector( - configurationMachine, instrumentStatusByIdWalletSelector(wallet.idWallet) ); diff --git a/ts/features/idpay/configuration/machine/actions.ts b/ts/features/idpay/configuration/machine/actions.ts new file mode 100644 index 00000000000..ade9e86b0d5 --- /dev/null +++ b/ts/features/idpay/configuration/machine/actions.ts @@ -0,0 +1,113 @@ +import { IOToast } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; +import { IDPayDetailsRoutes } from "../../details/navigation"; +import { IdPayConfigurationRoutes } from "../navigation/routes"; +import { InitiativeFailure } from "../types/failure"; +import * as Context from "./context"; + +const createActionsImplementation = ( + navigation: ReturnType +) => { + const navigateToConfigurationIntro = guardedNavigationAction( + () => { + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO + } + ); + } + ); + + const navigateToIbanEnrollmentScreen = guardedNavigationAction(() => + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT + } + ) + ); + + const navigateToIbanOnboardingScreen = guardedNavigationAction(() => + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING + } + ) + ); + + const navigateToIbanOnboardingFormScreen = guardedNavigationAction(() => + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING + } + ) + ); + + const navigateToInstrumentsEnrollmentScreen = guardedNavigationAction(() => + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT + } + ) + ); + + const navigateToConfigurationSuccessScreen = () => { + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_SUCCESS + } + ); + }; + + const navigateToInitiativeDetailScreen = (args: { + context: Context.Context; + }) => { + const initiativeId = args.context.initiativeId; + navigation.navigate(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { + screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, + params: { initiativeId } + }); + }; + + const showUpdateIbanToast = () => { + IOToast.success(I18n.t(`idpay.configuration.iban.updateToast`)); + }; + + const showFailureToast = (args: { context: Context.Context }) => { + pipe( + args.context.failure, + InitiativeFailure.decode, + O.fromEither, + O.map(IOToast.error) + ); + }; + + const exitConfiguration = () => { + navigation.pop(); + }; + + return { + navigateToConfigurationIntro, + navigateToIbanEnrollmentScreen, + navigateToIbanOnboardingScreen, + navigateToIbanOnboardingFormScreen, + navigateToInstrumentsEnrollmentScreen, + navigateToConfigurationSuccessScreen, + navigateToInitiativeDetailScreen, + showUpdateIbanToast, + showFailureToast, + exitConfiguration + }; +}; + +export { createActionsImplementation }; diff --git a/ts/features/idpay/configuration/xstate/services.ts b/ts/features/idpay/configuration/machine/actors.ts similarity index 75% rename from ts/features/idpay/configuration/xstate/services.ts rename to ts/features/idpay/configuration/machine/actors.ts index f50d8be1ebd..b36b3007b8d 100644 --- a/ts/features/idpay/configuration/xstate/services.ts +++ b/ts/features/idpay/configuration/machine/actors.ts @@ -1,35 +1,46 @@ +/* eslint-disable sonarjs/no-identical-functions */ import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { InvokeCreator, Receiver, Sender } from "xstate"; +import { fromCallback, fromPromise } from "xstate"; import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; +import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; import { IbanListDTO } from "../../../../../definitions/idpay/IbanListDTO"; +import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; import { TypeEnum } from "../../../../../definitions/pagopa/Wallet"; import { PaymentManagerClient } from "../../../../api/pagopa"; +import { useIODispatch } from "../../../../store/hooks"; import { PaymentManagerToken, Wallet } from "../../../../types/pagopa"; import { SessionManager } from "../../../../utils/SessionManager"; import { convertWalletV2toWalletV1 } from "../../../../utils/walletv2"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayClient } from "../../common/api/client"; -import { Context } from "./context"; -import { Events } from "./events"; -import { InitiativeFailureType } from "./failure"; +import { InitiativeFailureType } from "../types/failure"; +import * as Events from "./events"; const createServicesImplementation = ( idPayClient: IDPayClient, paymentManagerClient: PaymentManagerClient, pmSessionManager: SessionManager, bearerToken: string, - language: PreferredLanguageEnum + language: PreferredLanguageEnum, + dispatch: ReturnType ) => { - const loadInitiative = async (context: Context) => { - if (context.initiativeId === undefined) { - return Promise.reject(InitiativeFailureType.GENERIC); - } + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + const getInitiative = fromPromise(async params => { const response = await idPayClient.getWalletDetail({ - initiativeId: context.initiativeId, + initiativeId: params.input, bearerAuth: bearerToken, "Accept-Language": language }); @@ -43,6 +54,7 @@ const createServicesImplementation = ( case 200: return Promise.resolve(value); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject(InitiativeFailureType.GENERIC); @@ -52,9 +64,9 @@ const createServicesImplementation = ( ); return data; - }; + }); - const loadIbanList = async (_: Context) => { + const getIbanList = fromPromise(async () => { const response = await idPayClient.getIbanList({ bearerAuth: bearerToken, "Accept-Language": language @@ -76,6 +88,7 @@ const createServicesImplementation = ( ); return Promise.resolve({ ibanList: uniqueIbanList }); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -87,18 +100,21 @@ const createServicesImplementation = ( ); return data; - }; + }); - const confirmIban = async (context: Context): Promise => { - if (context.initiativeId === undefined) { - return Promise.reject(InitiativeFailureType.GENERIC); - } + const enrollIban = fromPromise< + undefined, + { initiativeId: string; iban: IbanDTO | IbanPutDTO } + >(async ({ input }) => { try { const res = await idPayClient.enrollIban({ "Accept-Language": language, bearerAuth: bearerToken, - initiativeId: context.initiativeId, - body: context.ibanBody + initiativeId: input.initiativeId, + body: { + iban: input.iban.iban, + description: input.iban.description + } }); return pipe( res, @@ -109,6 +125,7 @@ const createServicesImplementation = ( case 200: return Promise.resolve(undefined); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -121,27 +138,9 @@ const createServicesImplementation = ( } catch (e) { return Promise.reject(InitiativeFailureType.IBAN_ENROLL_FAILURE); } - }; - - const enrollIban = async (context: Context): Promise => { - if (context.initiativeId === undefined) { - return Promise.reject(InitiativeFailureType.GENERIC); - } + }); - if (context.selectedIban === undefined) { - return Promise.reject(InitiativeFailureType.GENERIC); - } - - return confirmIban({ - ...context, - ibanBody: { - iban: context.selectedIban.iban, - description: context.selectedIban.description - } - }); - }; - - const loadWalletInstruments = async () => { + const getWalletInstruments = fromPromise>(async () => { const response = await pmSessionManager.withRefresh( paymentManagerClient.getWalletsV2 )(); @@ -168,6 +167,7 @@ const createServicesImplementation = ( return Promise.resolve(wallet); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -179,15 +179,14 @@ const createServicesImplementation = ( ); return data; - }; - - const loadInitiativeInstruments = async (context: Context) => { - if (context.initiativeId === undefined) { - return Promise.reject(InitiativeFailureType.GENERIC); - } + }); + const getInitiativeInstruments = fromPromise< + ReadonlyArray, + string + >(async ({ input }) => { const response = await idPayClient.getInstrumentList({ - initiativeId: context.initiativeId, + initiativeId: input, bearerAuth: bearerToken, "Accept-Language": language }); @@ -202,6 +201,7 @@ const createServicesImplementation = ( case 200: return Promise.resolve(value.instrumentList); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -213,7 +213,7 @@ const createServicesImplementation = ( ); return data; - }; + }); const enrollInstrument = async (initiativeId?: string, idWallet?: string) => { if (initiativeId === undefined || idWallet === undefined) { @@ -236,6 +236,7 @@ const createServicesImplementation = ( case 200: return Promise.resolve(undefined); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -273,6 +274,7 @@ const createServicesImplementation = ( case 200: return Promise.resolve(undefined); case 401: + handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -286,38 +288,38 @@ const createServicesImplementation = ( return data; }; - const instrumentsEnrollmentService: InvokeCreator = - (context: Context) => - (callback: Sender, onReceive: Receiver) => - onReceive(async event => { + const instrumentsEnrollmentLogic = fromCallback( + ({ sendBack, receive, input }) => { + receive(event => { switch (event.type) { - case "DELETE_INSTRUMENT": - deleteInstrument(context.initiativeId, event.instrumentId) + case "delete-instrument": + deleteInstrument(input, event.instrumentId) .then(() => - callback({ + sendBack({ ...event, - type: "DELETE_INSTRUMENT_SUCCESS" + type: "update-instrument-success" }) ) .catch(() => - callback({ + sendBack({ ...event, - type: "DELETE_INSTRUMENT_FAILURE" + type: "update-instrument-failure" }) ); break; - case "ENROLL_INSTRUMENT": - enrollInstrument(context.initiativeId, event.walletId) + case "enroll-instrument": + enrollInstrument(input, event.walletId) .then(() => - callback({ + sendBack({ ...event, - type: "ENROLL_INSTRUMENT_SUCCESS" + type: "update-instrument-success", + enrolling: true }) ) .catch(() => - callback({ + sendBack({ ...event, - type: "ENROLL_INSTRUMENT_FAILURE" + type: "update-instrument-failure" }) ); break; @@ -325,15 +327,16 @@ const createServicesImplementation = ( break; } }); + } + ); return { - loadInitiative, - loadIbanList, + getInitiative, + getIbanList, enrollIban, - confirmIban, - loadWalletInstruments, - loadInitiativeInstruments, - instrumentsEnrollmentService + getWalletInstruments, + getInitiativeInstruments, + instrumentsEnrollmentLogic }; }; diff --git a/ts/features/idpay/configuration/machine/context.ts b/ts/features/idpay/configuration/machine/context.ts new file mode 100644 index 00000000000..0d3fa3a9f3a --- /dev/null +++ b/ts/features/idpay/configuration/machine/context.ts @@ -0,0 +1,31 @@ +import * as O from "fp-ts/lib/Option"; +import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; +import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; +import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; +import { Wallet } from "../../../../types/pagopa"; +import { ConfigurationMode, InstrumentStatusByIdWallet } from "../types"; +import { InitiativeFailureType } from "../types/failure"; + +export interface Context { + readonly initiativeId: string; + readonly mode: ConfigurationMode; + readonly initiative: O.Option; + readonly ibanList: ReadonlyArray; + readonly walletInstruments: ReadonlyArray; + readonly initiativeInstruments: ReadonlyArray; + readonly instrumentStatuses: InstrumentStatusByIdWallet; + readonly areInstrumentsSkipped: boolean; + readonly failure: O.Option; +} + +export const Context: Context = { + initiativeId: "", + mode: ConfigurationMode.COMPLETE, + initiative: O.none, + ibanList: [], + walletInstruments: [], + initiativeInstruments: [], + instrumentStatuses: {}, + areInstrumentsSkipped: false, + failure: O.none +}; diff --git a/ts/features/idpay/configuration/machine/events.ts b/ts/features/idpay/configuration/machine/events.ts new file mode 100644 index 00000000000..ff8626cf1f6 --- /dev/null +++ b/ts/features/idpay/configuration/machine/events.ts @@ -0,0 +1,61 @@ +import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; +import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; +import { GlobalEvents } from "../../../../xstate/types/events"; +import * as Input from "./input"; + +export interface AutoInit { + readonly type: "xstate.init"; + readonly input: Input.Input; +} + +export interface ConfirmIbanOnboarding { + readonly type: "confirm-iban-onboarding"; + readonly ibanBody: IbanPutDTO; +} + +export interface NewIbanOnboarding { + readonly type: "new-iban-onboarding"; +} + +export interface EnrollIban { + readonly type: "enroll-iban"; + readonly iban: IbanDTO; +} + +export interface EnrollInstrument { + readonly type: "enroll-instrument"; + readonly walletId: string; +} + +export interface DeleteInstrument { + readonly type: "delete-instrument"; + readonly instrumentId: string; + readonly walletId: string; +} + +export interface UpdateInstrumentSuccess { + readonly type: "update-instrument-success"; + readonly walletId: string; + readonly enrolling: boolean; +} + +export interface UpdateInstrumentFailure { + readonly type: "update-instrument-failure"; + readonly walletId: string; +} + +export interface SkipInstruments { + readonly type: "skip-instruments"; +} + +export type Events = + | AutoInit + | NewIbanOnboarding + | ConfirmIbanOnboarding + | EnrollIban + | EnrollInstrument + | DeleteInstrument + | UpdateInstrumentSuccess + | UpdateInstrumentFailure + | SkipInstruments + | GlobalEvents; diff --git a/ts/features/idpay/configuration/machine/input.ts b/ts/features/idpay/configuration/machine/input.ts new file mode 100644 index 00000000000..9f9cfe0b526 --- /dev/null +++ b/ts/features/idpay/configuration/machine/input.ts @@ -0,0 +1,14 @@ +import { ConfigurationMode } from "../types"; +import * as Context from "./context"; + +export interface Input { + readonly initiativeId: string; + readonly mode?: ConfigurationMode; +} + +export const Input = (input: Input): Promise => + Promise.resolve({ + ...Context.Context, + initiativeId: input.initiativeId, + mode: input.mode ?? ConfigurationMode.COMPLETE + }); diff --git a/ts/features/idpay/configuration/machine/machine.ts b/ts/features/idpay/configuration/machine/machine.ts new file mode 100644 index 00000000000..b0c9119a615 --- /dev/null +++ b/ts/features/idpay/configuration/machine/machine.ts @@ -0,0 +1,699 @@ +/* eslint-disable sonarjs/no-identical-functions */ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { + assertEvent, + assign, + forwardTo, + fromCallback, + fromPromise, + setup +} from "xstate"; +import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; +import { IbanListDTO } from "../../../../../definitions/idpay/IbanListDTO"; +import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; +import { + InitiativeDTO, + StatusEnum +} from "../../../../../definitions/idpay/InitiativeDTO"; +import { + InstrumentDTO, + StatusEnum as InstrumentStatusEnum +} from "../../../../../definitions/idpay/InstrumentDTO"; +import { Wallet } from "../../../../types/pagopa"; +import { + LOADING_TAG, + UPSERTING_TAG, + WAITING_USER_INPUT_TAG, + notImplementedStub +} from "../../../../xstate/utils"; +import { ConfigurationMode, InstrumentStatusByIdWallet } from "../types"; +import { InitiativeFailure, InitiativeFailureType } from "../types/failure"; +import * as Context from "./context"; +import * as Events from "./events"; +import * as Input from "./input"; + +/** PLEASE DO NO USE AUTO-LAYOUT WHEN USING VISUAL EDITOR */ +export const idPayConfigurationMachine = setup({ + types: { + input: {} as Input.Input, + context: {} as Context.Context, + events: {} as Events.Events + }, + actions: { + navigateToConfigurationIntro: notImplementedStub, + navigateToIbanEnrollmentScreen: notImplementedStub, + navigateToIbanOnboardingScreen: notImplementedStub, + navigateToIbanOnboardingFormScreen: notImplementedStub, + showUpdateIbanToast: notImplementedStub, + navigateToInstrumentsEnrollmentScreen: notImplementedStub, + navigateToConfigurationSuccessScreen: notImplementedStub, + navigateToInitiativeDetailScreen: notImplementedStub, + updateAllInstrumentsStatus: assign(({ context }) => { + const updatedStatuses = + context.initiativeInstruments.reduce( + (acc, instrument) => { + if (instrument.idWallet === undefined) { + return acc; + } + + const currentStatus = acc[instrument.idWallet]; + + if (currentStatus !== undefined && pot.isLoading(currentStatus)) { + // Instrument is updating, its status will be updated by 'updateInstrumentStatus' action + return acc; + } + + return { + ...acc, + [instrument.idWallet]: pot.some(instrument.status) + }; + }, + context.instrumentStatuses + ); + + return { + instrumentStatuses: updatedStatuses + }; + }), + updateInstrumentStatus: assign(({ context, event }) => { + assertEvent(event, ["enroll-instrument", "delete-instrument"]); + return { + instrumentStatuses: { + ...context.instrumentStatuses, + [event.walletId]: pot.noneLoading + } + }; + }), + updateInstrumentStatusSuccess: assign(({ context, event }) => { + assertEvent(event, "update-instrument-success"); + + const currentEnrollStatus = context.instrumentStatuses[event.walletId]; + + if (pot.isSome(currentEnrollStatus)) { + // No need to update instrument status + return {}; + } + + const status = event.enrolling + ? InstrumentStatusEnum.PENDING_ENROLLMENT_REQUEST + : InstrumentStatusEnum.PENDING_DEACTIVATION_REQUEST; + + return { + instrumentStatuses: { + ...context.instrumentStatuses, + [event.walletId]: pot.some(status) + } + }; + }), + updateInstrumentStatusFailure: assign(({ context, event }) => { + assertEvent(event, "update-instrument-failure"); + + const { [event.walletId]: _removedStatus, ...updatedStatuses } = + context.instrumentStatuses; + + return { + instrumentStatuses: updatedStatuses + }; + }), + handleSessionExpired: notImplementedStub, + showFailureToast: notImplementedStub, + exitConfiguration: notImplementedStub + }, + actors: { + onInit: fromPromise(({ input }) => + Input.Input(input) + ), + getInitiative: fromPromise(notImplementedStub), + getIbanList: fromPromise(notImplementedStub), + getWalletInstruments: + fromPromise>(notImplementedStub), + getInitiativeInstruments: fromPromise, string>( + notImplementedStub + ), + instrumentsEnrollmentLogic: fromCallback( + notImplementedStub + ), + enrollIban: fromPromise< + undefined, + { initiativeId: string; iban: IbanDTO | IbanPutDTO } + >(notImplementedStub) + }, + guards: { + isSessionExpired: ({ context }) => + pipe( + context.failure, + O.map(failure => failure === InitiativeFailureType.SESSION_EXPIRED), + O.getOrElse(() => false) + ), + isInstrumentsOnlyMode: ({ context }) => + context.mode === ConfigurationMode.INSTRUMENTS, + isIbanOnlyMode: ({ context }) => context.mode === ConfigurationMode.IBAN, + isConfigurationRequired: ({ context }) => + pipe( + context.initiative, + O.map(i => i.status === StatusEnum.NOT_REFUNDABLE), + O.getOrElse(() => false) + ), + hasIbanList: ({ context }) => context.ibanList.length > 0, + hasInstruments: ({ context }) => context.walletInstruments.length > 0 + }, + delays: { + INSTRUMENTS_POLLING_INTERVAL: 3000 + } +}).createMachine({ + context: Context.Context, + id: "idpay-configuration", + invoke: { + src: "onInit", + input: ({ event }) => { + assertEvent(event, "xstate.init"); + return event.input; + }, + onError: { + target: "ConfigurationFailure" + }, + onDone: { + actions: assign(event => ({ ...event.event.output })), + target: "LoadingInitiative" + } + }, + initial: "Idle", + on: { + close: { + target: "#idpay-configuration.ConfigurationClosed" + } + }, + states: { + Idle: { + tags: [LOADING_TAG] + }, + + LoadingInitiative: { + tags: [LOADING_TAG], + invoke: { + src: "getInitiative", + id: "getInitiative", + input: ({ context }) => context.initiativeId, + onDone: { + actions: assign(({ event }) => ({ + initiative: O.some(event.output) + })), + target: "EvaluatingInitiativeConfiguration" + }, + onError: { + actions: assign(({ event }) => ({ + failure: pipe(InitiativeFailure.decode(event.error), O.fromEither) + })), + target: "ConfigurationFailure" + } + } + }, + + EvaluatingInitiativeConfiguration: { + tags: [LOADING_TAG], + always: [ + { + guard: "isInstrumentsOnlyMode", + target: "ConfiguringInstruments" + }, + { + guard: "isIbanOnlyMode", + target: "ConfiguringIban" + }, + { + guard: "isConfigurationRequired", + target: "DisplayingConfigurationIntro" + }, + { + target: "ConfigurationNotNeeded" + } + ] + }, + + DisplayingConfigurationIntro: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToConfigurationIntro", + on: { + next: { + target: "ConfiguringIban" + } + } + }, + + ConfiguringIban: { + id: "configuration-iban", + initial: "LoadingIbanList", + states: { + LoadingIbanList: { + tags: [LOADING_TAG], + invoke: { + src: "getIbanList", + id: "getIbanList", + onDone: [ + { + actions: assign(({ event }) => ({ + ibanList: event.output.ibanList + })) + }, + [ + { + guard: "hasIbanList", + target: "DisplayingIbanList" + }, + { + target: "DisplayingIbanOnboarding" + } + ] + ], + onError: [ + { + actions: assign(({ event }) => ({ + failure: pipe( + InitiativeFailure.decode(event.error), + O.fromEither + ) + })) + }, + [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + guard: "isIbanOnlyMode", + target: "#idpay-configuration.ConfigurationFailure" + }, + { + target: "#idpay-configuration.DisplayingConfigurationIntro", + actions: "showFailureToast" + } + ] + ] + } + }, + + DisplayingIbanList: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToIbanEnrollmentScreen", + on: { + back: [ + { + guard: "isIbanOnlyMode", + target: "#idpay-configuration.ConfigurationClosed" + }, + { + target: "#idpay-configuration.DisplayingConfigurationIntro" + } + ], + "new-iban-onboarding": { + target: "DisplayingIbanOnboarding" + }, + "enroll-iban": { + target: "EnrollingIban" + } + } + }, + + DisplayingIbanOnboarding: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToIbanOnboardingScreen", + on: { + back: [ + { + guard: "hasIbanList", + target: "DisplayingIbanList" + }, + { + guard: "isIbanOnlyMode", + target: "#idpay-configuration.ConfigurationClosed" + }, + { + target: "#idpay-configuration.DisplayingConfigurationIntro" + } + ] + } + }, + + DisplayingIbanOnboardingForm: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToIbanOnboardingFormScreen", + on: { + back: [ + { + target: "#idpay-configuration.DisplayingIbanOnboarding" + } + ], + "confirm-iban-onboarding": { + target: "OnboardingNewIban" + } + } + }, + + OnboardingNewIban: { + tags: [LOADING_TAG], + invoke: { + src: "enrollIban", + id: "enrollIban", + input: ({ context, event }) => { + assertEvent(event, "confirm-iban-onboarding"); + return { + initiativeId: context.initiativeId, + iban: event.ibanBody + }; + }, + onDone: { + target: "IbanConfigurationCompleted" + }, + onError: [ + { + actions: assign(({ event }) => ({ + failure: pipe( + InitiativeFailure.decode(event.error), + O.fromEither + ) + })) + }, + [ + { + guard: "isSessionExpired", + target: "#idpay-configuration.SessionExpired" + }, + { + target: "DisplayingIbanOnboardingForm", + actions: "showFailureToast" + } + ] + ] + } + }, + + EnrollingIban: { + tags: [UPSERTING_TAG], + invoke: { + src: "enrollIban", + id: "enrollIban", + input: ({ context, event }) => { + assertEvent(event, "enroll-iban"); + return { + initiativeId: context.initiativeId, + iban: event.iban + }; + }, + onDone: [ + { + guard: "isIbanOnlyMode", + target: "DisplayingIbanList", + actions: "showUpdateIbanToast" + }, + { + target: "IbanConfigurationCompleted" + } + ], + onError: [ + { + guard: "isSessionExpired", + target: "#idpay-configuration.SessionExpired" + }, + { + target: "DisplayingIbanList", + actions: "showFailureToast" + } + ] + } + }, + + IbanConfigurationCompleted: { + type: "final" + } + }, + onDone: [ + { + guard: "isIbanOnlyMode", + target: "ConfigurationCompleted" + }, + { + target: "#idpay-configuration.ConfiguringInstruments" + } + ] + }, + + ConfiguringInstruments: { + id: "configuration-instruments", + initial: "LoadingInstruments", + states: { + LoadingInstruments: { + tags: [LOADING_TAG], + entry: "navigateToInstrumentsEnrollmentScreen", + type: "parallel", + states: { + LoadingWalletInstruments: { + initial: "LOADING", + invoke: { + src: "getWalletInstruments", + id: "getWalletInstruments", + input: ({ context }) => context.initiativeId, + onDone: { + actions: assign(({ event }) => ({ + walletInstruments: event.output + })) + }, + onError: [ + { + actions: assign(({ event }) => ({ + failure: pipe( + InitiativeFailure.decode(event.error), + O.fromEither + ) + })) + }, + [ + { + guard: "isSessionExpired", + target: "#idpay-configuration.SessionExpired" + }, + { + guard: "isInstrumentsOnlyMode", + target: "#idpay-configuration.ConfigurationFailure" + }, + { + target: "#idpay-configuration.ConfiguringIban", + actions: "showFailureToast" + } + ] + ] + } + }, + + LoadingInitiativeInstruments: { + initial: "LOADING", + invoke: { + src: "getInitiativeInstruments", + id: "getInitiativeInstruments", + input: ({ context }) => context.initiativeId, + onDone: { + actions: assign(({ event }) => ({ + initiativeInstruments: event.output + })) + }, + onError: [ + { + actions: assign(({ event }) => ({ + failure: pipe( + InitiativeFailure.decode(event.error), + O.fromEither + ) + })) + }, + [ + { + guard: "isSessionExpired", + target: "#idpay-configuration.SessionExpired" + }, + { + guard: "isInstrumentsOnlyMode", + target: "#idpay-configuration.ConfigurationFailure" + }, + { + target: "#idpay-configuration.ConfiguringIban", + actions: "showFailureToast" + } + ] + ] + } + } + }, + onDone: [ + { + guard: "hasInstruments", + target: "DisplayingInstruments" + }, + { + guard: "isInstrumentsOnlyMode", + target: "DisplayingInstruments" + }, + { + target: "#idpay-configuration.DisplayingConfigurationSuccess" + } + ] + }, + + DisplayingInstruments: { + tags: [WAITING_USER_INPUT_TAG], + entry: "updateAllInstrumentsStatus", + initial: "DISPLAYING", + invoke: { + id: "instrumentsEnrollment", + src: "instrumentsEnrollmentLogic", + input: ({ context }) => context.initiativeId + }, + on: { + "enroll-instrument": { + actions: [ + forwardTo("instrumentsEnrollment"), + "updateInstrumentStatus" + ] + }, + + "delete-instrument": { + actions: [ + forwardTo("instrumentsEnrollment"), + "updateInstrumentStatus" + ] + }, + + "update-instrument-success": { + actions: "updateInstrumentStatusSuccess" + }, + + "update-instrument-failure": { + actions: "updateInstrumentStatusFailure" + }, + + back: [ + { + guard: "isInstrumentsOnlyMode", + target: "#idpay-configuration.ConfigurationClosed" + }, + { + target: "#idpay-configuration.ConfiguringIban" + } + ], + + next: { + target: "InstrumentsConfigurationCompleted" + }, + + "skip-instruments": { + target: "InstrumentsConfigurationCompleted", + actions: assign(() => ({ areInstrumentsSkipped: true })) + } + }, + states: { + DisplayingInstrument: { + after: { + INSTRUMENTS_POLLING_INTERVAL: { + target: "RefreshingInstrumentState" + } + } + }, + RefreshingInstrumentState: { + invoke: { + src: "getInitiativeInstruments", + id: "getInitiativeInstruments", + input: ({ context }) => context.initiativeId, + onDone: { + target: "DisplayingInstrument", + actions: [ + assign(({ event }) => ({ + initiativeInstruments: event.output + })), + "updateAllInstrumentsStatus" + ] + }, + onError: [ + { + actions: assign(({ event }) => ({ + failure: pipe( + InitiativeFailure.decode(event.error), + O.fromEither + ) + })) + }, + [ + { + guard: "isSessionExpired", + target: "#idpay-configuration.SessionExpired" + }, + { + target: "DisplayingInstrument", + actions: "showFailureToast" + } + ] + ] + } + } + } + }, + + InstrumentsConfigurationCompleted: { + type: "final" + } + }, + onDone: [ + { + guard: "isInstrumentsOnlyMode", + target: "ConfigurationCompleted" + }, + { + target: "DisplayingConfigurationSuccess" + } + ] + }, + + DisplayingConfigurationSuccess: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToConfigurationSuccessScreen", + on: { + next: { + target: "ConfigurationCompleted" + } + } + }, + + ConfigurationNotNeeded: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToConfigurationSuccessScreen", + on: { + next: { + target: "ConfigurationCompleted" + } + } + }, + + ConfigurationCompleted: { + type: "final", + entry: "navigateToInitiativeDetailScreen" + }, + + ConfigurationClosed: { + type: "final", + entry: "exitConfiguration" + }, + + ConfigurationFailure: { + type: "final", + always: { + guard: "isSessionExpired", + target: "#idpay-configuration.SessionExpired" + }, + entry: ["showFailureToast", "exitConfiguration"] + }, + + SessionExpired: { + type: "final", + entry: ["handleSessionExpired", "exitConfiguration"] + } + } +}); diff --git a/ts/features/idpay/configuration/xstate/provider.tsx b/ts/features/idpay/configuration/machine/provider.tsx similarity index 81% rename from ts/features/idpay/configuration/xstate/provider.tsx rename to ts/features/idpay/configuration/machine/provider.tsx index 84933d35c2c..3db13a9c33a 100644 --- a/ts/features/idpay/configuration/xstate/provider.tsx +++ b/ts/features/idpay/configuration/machine/provider.tsx @@ -3,7 +3,6 @@ import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; -import { InterpreterFrom } from "xstate"; import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; import { PaymentManagerClient } from "../../../../api/pagopa"; import { @@ -26,18 +25,23 @@ import { defaultRetryingFetch } from "../../../../utils/fetch"; import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; -import { idPayInitiativeConfigurationMachine } from "./machine"; -import { createServicesImplementation } from "./services"; +import { createServicesImplementation } from "./actors"; +import { idPayConfigurationMachine } from "./machine"; +import * as Input from "./input"; type Props = { children: React.ReactNode; + input: Input.Input; }; -export const IdPayInitiativeConfigurationMachineContext = createActorContext( - idPayInitiativeConfigurationMachine +export const IdPayConfigurationMachineContext = createActorContext( + idPayConfigurationMachine ); -export const IDPayConfigurationMachineProvider = (props: Props) => { +export const IDPayConfigurationMachineProvider = ({ + children, + input +}: Props) => { const dispatch = useIODispatch(); const sessionInfo = useIOSelector(sessionInfoSelector); @@ -90,19 +94,22 @@ export const IDPayConfigurationMachineProvider = (props: Props) => { paymentManagerClient, pmSessionManager, idPayToken, - language + language, + dispatch ); + const actions = createActionsImplementation(navigation); - const actions = createActionsImplementation(navigation, dispatch); - - const machine = idPayInitiativeConfigurationMachine.provide({ + const machine = idPayConfigurationMachine.provide({ actors, actions }); return ( - - {props.children} - + + {children} + ); }; diff --git a/ts/features/idpay/configuration/machine/selectors.ts b/ts/features/idpay/configuration/machine/selectors.ts new file mode 100644 index 00000000000..73affa60f80 --- /dev/null +++ b/ts/features/idpay/configuration/machine/selectors.ts @@ -0,0 +1,97 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import * as O from "fp-ts/lib/Option"; +import { createSelector } from "reselect"; +import { SnapshotFrom } from "xstate"; +import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; +import { LOADING_TAG } from "../../../../xstate/utils"; +import { ConfigurationMode } from "../types"; +import { idPayConfigurationMachine } from "./machine"; + +type MachineSnapshot = SnapshotFrom; + +type IDPayInstrumentsByIdWallet = { + [idWallet: string]: InstrumentDTO; +}; + +const isLoadingSelector = (snapshot: MachineSnapshot) => + snapshot.hasTag(LOADING_TAG as never); + +const selectInitiativeDetails = (snapshot: MachineSnapshot) => + snapshot.context.initiative; + +const selectIsInstrumentsOnlyMode = (snapshot: MachineSnapshot) => + snapshot.context.mode === ConfigurationMode.INSTRUMENTS; + +const selectIsIbanOnlyMode = (snapshot: MachineSnapshot) => + snapshot.context.mode === ConfigurationMode.IBAN; + +const isLoadingIbanListSelector = (snapshot: MachineSnapshot) => + snapshot.matches("ConfiguringIban"); + +const ibanListSelector = (snapshot: MachineSnapshot) => + snapshot.context.ibanList; + +const selectAreInstrumentsSkipped = (snapshot: MachineSnapshot) => + snapshot.context.areInstrumentsSkipped ?? false; + +const selectEnrolledIban = createSelector( + selectInitiativeDetails, + ibanListSelector, + (initiativeOption, ibanList) => { + const initiative = O.toUndefined(initiativeOption); + if (initiative?.iban === undefined) { + return undefined; + } + return ibanList.find(_ => _.iban === initiative.iban); + } +); + +const selectWalletInstruments = (snapshot: MachineSnapshot) => + snapshot.context.walletInstruments; + +const selectInitiativeInstruments = (snapshot: MachineSnapshot) => + snapshot.context.initiativeInstruments; + +const initiativeInstrumentsByIdWalletSelector = createSelector( + selectInitiativeInstruments, + instruments => + instruments.reduce((acc, instrument) => { + if (instrument.idWallet !== undefined) { + // eslint-disable-next-line functional/immutable-data + acc[instrument.idWallet] = instrument; + } + return acc; + }, {}) +); + +const selectInstrumentStatuses = (snapshot: MachineSnapshot) => + snapshot.context.instrumentStatuses; + +const isUpsertingInstrumentSelector = createSelector( + selectInstrumentStatuses, + statuses => Object.values(statuses).some(pot.isLoading) +); + +const instrumentStatusByIdWalletSelector = (idWallet: number) => + createSelector( + selectInstrumentStatuses, + statuses => statuses[idWallet] ?? pot.some(undefined) + ); + +const failureSelector = (snapshot: MachineSnapshot) => snapshot.context.failure; + +export { + failureSelector, + ibanListSelector, + initiativeInstrumentsByIdWalletSelector, + instrumentStatusByIdWalletSelector, + isLoadingIbanListSelector, + isLoadingSelector, + isUpsertingInstrumentSelector, + selectAreInstrumentsSkipped, + selectEnrolledIban, + selectInitiativeDetails, + selectIsIbanOnlyMode, + selectIsInstrumentsOnlyMode, + selectWalletInstruments +}; diff --git a/ts/features/idpay/configuration/navigation/navigator.tsx b/ts/features/idpay/configuration/navigation/navigator.tsx index 680e6e5ec9b..6699dabf875 100644 --- a/ts/features/idpay/configuration/navigation/navigator.tsx +++ b/ts/features/idpay/configuration/navigation/navigator.tsx @@ -1,90 +1,74 @@ +import { RouteProp, useRoute } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; -import IbanEnrollmentScreen, { - IbanEnrollmentScreenRouteParams -} from "../screens/IbanEnrollmentScreen"; -import InitiativeConfigurationIntroScreen, { - InitiativeConfigurationIntroScreenRouteParams -} from "../screens/InitiativeConfigurationIntroScreen"; -import ConfigurationSuccessScreen from "../screens/ConfigurationSuccessScreen"; -import IbanConfigurationLanding from "../screens/IbanConfigurationLandingScreen"; -import IbanOnboardingScreen from "../screens/IbanOnboardingScreen"; -import InstrumentsEnrollmentScreen, { - InstrumentsEnrollmentScreenRouteParams -} from "../screens/InstrumentsEnrollmentScreen"; -import { IDPayConfigurationMachineProvider } from "../xstate/provider"; import { isGestureEnabled } from "../../../../utils/navigation"; -import IdPayDiscountInstrumentsScreen, { - IdPayDiscountInstrumentsScreenRouteParams -} from "../screens/IdPayDiscountInstrumentsScreen"; +import { IDPayConfigurationMachineProvider } from "../machine/provider"; +import { ConfigurationSuccessScreen } from "../screens/ConfigurationSuccessScreen"; +import { IbanConfigurationLanding } from "../screens/IbanConfigurationLandingScreen"; +import { IbanEnrollmentScreen } from "../screens/IbanEnrollmentScreen"; +import { IbanOnboardingScreen } from "../screens/IbanOnboardingScreen"; +import { IdPayDiscountInstrumentsScreen } from "../screens/IdPayDiscountInstrumentsScreen"; +import { InitiativeConfigurationIntroScreen } from "../screens/InitiativeConfigurationIntroScreen"; +import { InstrumentsEnrollmentScreen } from "../screens/InstrumentsEnrollmentScreen"; +import { IdPayConfigurationParamsList } from "./params"; +import { IdPayConfigurationRoutes } from "./routes"; -export const IDPayConfigurationRoutes = { - IDPAY_CONFIGURATION_MAIN: "IDPAY_CONFIGURATION_MAIN", - IDPAY_CONFIGURATION_INTRO: "IDPAY_CONFIGURATION_INTRO", - IDPAY_CONFIGURATION_IBAN_LANDING: "IDPAY_CONFIGURATION_IBAN_LANDING", - IDPAY_CONFIGURATION_IBAN_ONBOARDING: "IDPAY_CONFIGURATION_IBAN_ONBOARDING", - IDPAY_CONFIGURATION_IBAN_ENROLLMENT: "IDPAY_CONFIGURATION_IBAN_ENROLLMENT", - IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT: - "IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT", - IDPAY_CONFIGURATION_SUCCESS: "IDPAY_CONFIGURATION_SUCCESS", - IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS: - "IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS" -} as const; +const Stack = createStackNavigator(); -export type IDPayConfigurationParamsList = { - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO]: InitiativeConfigurationIntroScreenRouteParams; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING]: undefined; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING]: undefined; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT]: IbanEnrollmentScreenRouteParams; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT]: InstrumentsEnrollmentScreenRouteParams; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS]: IdPayDiscountInstrumentsScreenRouteParams; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_SUCCESS]: undefined; -}; +type IdPayConfigurationRouteProps = RouteProp< + IdPayConfigurationParamsList, + "IDPAY_CONFIGURATION_NAVIGATOR" +>; -const Stack = createStackNavigator(); +export const IdPayConfigurationNavigator = () => { + const { params } = useRoute(); + const { initiativeId, mode } = params; -export const IDPayConfigurationNavigator = () => ( - - - + return ( + + + - + - + - + - + - + - - - -); + + + + ); +}; diff --git a/ts/features/idpay/configuration/navigation/params.ts b/ts/features/idpay/configuration/navigation/params.ts new file mode 100644 index 00000000000..6e7a4383733 --- /dev/null +++ b/ts/features/idpay/configuration/navigation/params.ts @@ -0,0 +1,19 @@ +import { IdPayDiscountInstrumentsScreenRouteParams } from "../screens/IdPayDiscountInstrumentsScreen"; +import { ConfigurationMode } from "../types"; +import { IdPayConfigurationRoutes } from "./routes"; + +export type IdPayConfigurationNavigatorParams = { + initiativeId: string; + mode?: ConfigurationMode; +}; + +export type IdPayConfigurationParamsList = { + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR]: IdPayConfigurationNavigatorParams; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS]: IdPayDiscountInstrumentsScreenRouteParams; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_SUCCESS]: undefined; +}; diff --git a/ts/features/idpay/configuration/navigation/routes.ts b/ts/features/idpay/configuration/navigation/routes.ts new file mode 100644 index 00000000000..c13f92d3247 --- /dev/null +++ b/ts/features/idpay/configuration/navigation/routes.ts @@ -0,0 +1,12 @@ +export const IdPayConfigurationRoutes = { + IDPAY_CONFIGURATION_NAVIGATOR: "IDPAY_CONFIGURATION_NAVIGATOR", + IDPAY_CONFIGURATION_INTRO: "IDPAY_CONFIGURATION_INTRO", + IDPAY_CONFIGURATION_IBAN_LANDING: "IDPAY_CONFIGURATION_IBAN_LANDING", + IDPAY_CONFIGURATION_IBAN_ONBOARDING: "IDPAY_CONFIGURATION_IBAN_ONBOARDING", + IDPAY_CONFIGURATION_IBAN_ENROLLMENT: "IDPAY_CONFIGURATION_IBAN_ENROLLMENT", + IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT: + "IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT", + IDPAY_CONFIGURATION_SUCCESS: "IDPAY_CONFIGURATION_SUCCESS", + IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS: + "IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS" +} as const; diff --git a/ts/features/idpay/configuration/screens/ConfigurationSuccessScreen.tsx b/ts/features/idpay/configuration/screens/ConfigurationSuccessScreen.tsx index 9093d5bf518..01ee5c1c373 100644 --- a/ts/features/idpay/configuration/screens/ConfigurationSuccessScreen.tsx +++ b/ts/features/idpay/configuration/screens/ConfigurationSuccessScreen.tsx @@ -1,6 +1,3 @@ -import React from "react"; -import { SafeAreaView, StyleSheet, View } from "react-native"; -import { useSelector } from "@xstate/react"; import { Body, ButtonOutline, @@ -10,38 +7,39 @@ import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import React from "react"; +import { SafeAreaView, StyleSheet, View } from "react-native"; import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import ROUTES from "../../../../navigation/routes"; import themeVariables from "../../../../theme/variables"; -import { useConfigurationMachineService } from "../xstate/provider"; +import { IdPayConfigurationMachineContext } from "../machine/provider"; import { selectAreInstrumentsSkipped, selectInitiativeDetails -} from "../xstate/selectors"; +} from "../machine/selectors"; -const ConfigurationSuccessScreen = () => { - const configurationMachine = useConfigurationMachineService(); +export const ConfigurationSuccessScreen = () => { + const navigation = useIONavigation(); + const { useActorRef, useSelector } = IdPayConfigurationMachineContext; + const machine = useActorRef(); - const initiativeDetails = useSelector( - configurationMachine, - selectInitiativeDetails - ); - - const areInstrumentsSkipped = useSelector( - configurationMachine, - selectAreInstrumentsSkipped - ); + const initiativeDetails = useSelector(selectInitiativeDetails); + const areInstrumentsSkipped = useSelector(selectAreInstrumentsSkipped); if (initiativeDetails === undefined) { return null; } - const { initiativeName } = initiativeDetails; - - const handleNavigateToInitiativePress = () => - configurationMachine.send({ type: "COMPLETE_CONFIGURATION" }); + const handleNavigateToInitiativePress = () => machine.send({ type: "next" }); const handleAddPaymentMethodButtonPress = () => - configurationMachine.send({ type: "ADD_PAYMENT_METHOD" }); + navigation.replace(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.WALLET_ADD_PAYMENT_METHOD, + params: { inPayment: O.none } + }); const renderButtons = () => { if (areInstrumentsSkipped) { @@ -86,6 +84,12 @@ const ConfigurationSuccessScreen = () => { ); }; + const initiativeName = pipe( + initiativeDetails, + O.map(i => i.initiativeName), + O.toUndefined + ); + return ( @@ -125,5 +129,3 @@ const styles = StyleSheet.create({ alignItems: "center" } }); - -export default ConfigurationSuccessScreen; diff --git a/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx b/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx index fb00b35d553..056bb08f860 100644 --- a/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx @@ -1,11 +1,11 @@ -import React from "react"; -import { SafeAreaView, StyleSheet, View } from "react-native"; import { ButtonSolid, - VSpacer, + ContentWrapper, Pictogram, - ContentWrapper + VSpacer } from "@pagopa/io-app-design-system"; +import React from "react"; +import { SafeAreaView, StyleSheet, View } from "react-native"; import { Body } from "../../../../components/core/typography/Body"; import { H3 } from "../../../../components/core/typography/H3"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; @@ -15,28 +15,16 @@ import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationS import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; -import { useConfigurationMachineService } from "../xstate/provider"; - -const styles = StyleSheet.create({ - mainContainer: { - justifyContent: "center", - alignItems: "center", - paddingBottom: 90 - }, - textContainer: { - alignItems: "center", - justifyContent: "flex-start" - }, - textCenter: { textAlign: "center" } -}); +import { IdPayConfigurationMachineContext } from "../machine/provider"; -const IbanConfigurationLanding = () => { - const configurationMachine = useConfigurationMachineService(); +export const IbanConfigurationLanding = () => { + const { useActorRef } = IdPayConfigurationMachineContext; + const machine = useActorRef(); - const customGoBack = () => configurationMachine.send({ type: "BACK" }); + const customGoBack = () => machine.send({ type: "back" }); useNavigationSwipeBackListener(() => { - configurationMachine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); const { bottomSheet, dismiss, present } = useIOBottomSheetAutoresizableModal( @@ -94,7 +82,7 @@ const IbanConfigurationLanding = () => { type="SingleButton" leftButton={{ title: I18n.t("global.buttons.continue"), - onPress: () => configurationMachine.send({ type: "NEXT" }) + onPress: () => machine.send({ type: "next" }) }} /> @@ -102,4 +90,16 @@ const IbanConfigurationLanding = () => { ); }; -export default IbanConfigurationLanding; + +const styles = StyleSheet.create({ + mainContainer: { + justifyContent: "center", + alignItems: "center", + paddingBottom: 90 + }, + textContainer: { + alignItems: "center", + justifyContent: "flex-start" + }, + textCenter: { textAlign: "center" } +}); diff --git a/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx b/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx index 6cb8dff01be..6372b90818d 100644 --- a/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx @@ -1,6 +1,4 @@ import { HSpacer, Icon, VSpacer } from "@pagopa/io-app-design-system"; -import { RouteProp, useRoute } from "@react-navigation/native"; -import { useSelector } from "@xstate/react"; import React from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; @@ -16,37 +14,24 @@ import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationS import I18n from "../../../../i18n"; import customVariables from "../../../../theme/variables"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { IDPayConfigurationParamsList } from "../navigation/navigator"; -import { ConfigurationMode } from "../xstate/context"; -import { useConfigurationMachineService } from "../xstate/provider"; +import { isUpseringSelector } from "../../../../xstate/selectors"; +import { IdPayConfigurationMachineContext } from "../machine/provider"; import { ibanListSelector, isLoadingSelector, - isUpsertingIbanSelector, selectEnrolledIban, selectIsIbanOnlyMode -} from "../xstate/selectors"; +} from "../machine/selectors"; -type IbanEnrollmentScreenRouteParams = { - initiativeId?: string; -}; - -type IbanEnrollmentScreenRouteProps = RouteProp< - IDPayConfigurationParamsList, - "IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT" ->; - -const IbanEnrollmentScreen = () => { - const route = useRoute(); - const { initiativeId } = route.params; +export const IbanEnrollmentScreen = () => { + const { useActorRef, useSelector } = IdPayConfigurationMachineContext; + const machine = useActorRef(); - const configurationMachine = useConfigurationMachineService(); - - const isLoading = useSelector(configurationMachine, isLoadingSelector); - const ibanList = useSelector(configurationMachine, ibanListSelector); - const isIbanOnly = useSelector(configurationMachine, selectIsIbanOnlyMode); - - const enrolledIban = useSelector(configurationMachine, selectEnrolledIban); + const isLoading = useSelector(isLoadingSelector); + const ibanList = useSelector(ibanListSelector); + const isIbanOnly = useSelector(selectIsIbanOnlyMode); + const isUpsertingIban = useSelector(isUpseringSelector); + const enrolledIban = useSelector(selectEnrolledIban); const [selectedIban, setSelectedIban] = React.useState(); React.useEffect(() => { @@ -55,38 +40,33 @@ const IbanEnrollmentScreen = () => { } }, [enrolledIban]); - const isUpsertingIban = useSelector( - configurationMachine, - isUpsertingIbanSelector - ); - const handleSelectIban = React.useCallback( (iban: IbanDTO) => { setSelectedIban(iban); if (isIbanOnly) { - configurationMachine.send({ type: "ENROLL_IBAN", iban }); + machine.send({ type: "enroll-iban", iban }); } }, - [isIbanOnly, configurationMachine] + [isIbanOnly, machine] ); const handleBackPress = () => { - configurationMachine.send({ type: "BACK" }); + machine.send({ type: "back" }); }; const handleContinuePress = () => { if (selectedIban !== undefined) { - configurationMachine.send({ type: "ENROLL_IBAN", iban: selectedIban }); + machine.send({ type: "enroll-iban", iban: selectedIban }); } }; const handleAddNewIbanPress = () => { - configurationMachine.send({ type: "NEW_IBAN_ONBOARDING" }); + machine.send({ type: "new-iban-onboarding" }); }; useNavigationSwipeBackListener(() => { - configurationMachine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); const renderFooter = () => { @@ -125,20 +105,6 @@ const IbanEnrollmentScreen = () => { ); }; - /** - * If when navigating to this screen we have an initiativeId, we set the configuration machine to - * show only the IBAN related screens and not the whole configuration flow. - */ - React.useEffect(() => { - if (initiativeId) { - configurationMachine.send({ - type: "START_CONFIGURATION", - initiativeId, - mode: ConfigurationMode.IBAN - }); - } - }, [configurationMachine, initiativeId]); - const renderIbanList = () => ibanList.map(iban => { const isSelected = iban.iban === selectedIban?.iban; @@ -206,7 +172,3 @@ const styles = StyleSheet.create({ alignItems: "center" } }); - -export type { IbanEnrollmentScreenRouteParams }; - -export default IbanEnrollmentScreen; diff --git a/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx b/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx index c396ede9a97..f43a1c77701 100644 --- a/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx @@ -1,5 +1,4 @@ import { HSpacer, Icon, VSpacer } from "@pagopa/io-app-design-system"; -import { useSelector } from "@xstate/react"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; @@ -17,15 +16,17 @@ import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { useConfigurationMachineService } from "../xstate/provider"; -import { isLoadingSelector } from "../xstate/selectors"; +import { IdPayConfigurationMachineContext } from "../machine/provider"; +import { isLoadingSelector } from "../machine/selectors"; -const IbanOnboardingScreen = () => { - const configurationMachine = useConfigurationMachineService(); - const customGoBack = () => configurationMachine.send({ type: "BACK" }); +export const IbanOnboardingScreen = () => { + const { useActorRef, useSelector } = IdPayConfigurationMachineContext; + const machine = useActorRef(); + + const customGoBack = () => machine.send({ type: "back" }); const [iban, setIban] = React.useState(undefined); const [ibanName, setIbanName] = React.useState(undefined); - const isLoading = useSelector(configurationMachine, isLoadingSelector); + const isLoading = useSelector(isLoadingSelector); const isIbanValid = () => pipe( iban, @@ -47,7 +48,7 @@ const IbanOnboardingScreen = () => { ); useNavigationSwipeBackListener(() => { - configurationMachine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); return ( @@ -117,8 +118,8 @@ const IbanOnboardingScreen = () => { ibanName !== undefined && ibanName.length > 0; if (isDataSendable) { - configurationMachine.send({ - type: "CONFIRM_IBAN", + machine.send({ + type: "confirm-iban-onboarding", ibanBody: { iban, description: ibanName } }); } else { @@ -133,5 +134,3 @@ const IbanOnboardingScreen = () => { ); }; - -export default IbanOnboardingScreen; diff --git a/ts/features/idpay/configuration/screens/IdPayDiscountInstrumentsScreen.tsx b/ts/features/idpay/configuration/screens/IdPayDiscountInstrumentsScreen.tsx index eaebc5f866d..74b007dd446 100644 --- a/ts/features/idpay/configuration/screens/IdPayDiscountInstrumentsScreen.tsx +++ b/ts/features/idpay/configuration/screens/IdPayDiscountInstrumentsScreen.tsx @@ -1,3 +1,5 @@ +import { Divider, H1, VSpacer } from "@pagopa/io-app-design-system"; +import * as pot from "@pagopa/ts-commons/lib/pot"; import { RouteProp, useFocusEffect, @@ -5,42 +7,39 @@ import { useRoute } from "@react-navigation/native"; import React from "react"; -import { Divider, H1, VSpacer } from "@pagopa/io-app-design-system"; -import * as pot from "@pagopa/ts-commons/lib/pot"; - import { ScrollView, StyleSheet } from "react-native"; +import { InstrumentTypeEnum } from "../../../../../definitions/idpay/InstrumentDTO"; import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import { Body } from "../../../../components/core/typography/Body"; +import TopScreenComponent from "../../../../components/screens/TopScreenComponent"; import I18n from "../../../../i18n"; +import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import customVariables from "../../../../theme/variables"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { - idPayInitiativeInstrumentsRefreshStart, - idPayInitiativeInstrumentsRefreshStop, - idpayInitiativeInstrumentDelete -} from "../store/actions"; +import { useIdPayInfoCieBottomSheet } from "../../code/components/IdPayInfoCieBottomSheet"; +import { IdPayCodeParamsList } from "../../code/navigation/params"; +import { IdPayCodeRoutes } from "../../code/navigation/routes"; +import { IdPayDiscountInstrumentEnrollmentSwitch } from "../components/IdPayDiscountInstrumentEnrollmentSwitch"; +import { IdPayConfigurationParamsList } from "../navigation/params"; import { idPayIsLoadingInitiativeInstrumentSelector, idpayDiscountInitiativeInstrumentsSelector, isLoadingDiscountInitiativeInstrumentsSelector } from "../store"; -import { IdPayDiscountInstrumentEnrollmentSwitch } from "../components/IdPayDiscountInstrumentEnrollmentSwitch"; -import { IDPayConfigurationParamsList } from "../navigation/navigator"; -import TopScreenComponent from "../../../../components/screens/TopScreenComponent"; -import { useIdPayInfoCieBottomSheet } from "../../code/components/IdPayInfoCieBottomSheet"; -import { InstrumentTypeEnum } from "../../../../../definitions/idpay/InstrumentDTO"; -import { IdPayCodeRoutes } from "../../code/navigation/routes"; -import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import { IdPayCodeParamsList } from "../../code/navigation/params"; +import { + idPayInitiativeInstrumentsRefreshStart, + idPayInitiativeInstrumentsRefreshStop, + idpayInitiativeInstrumentDelete +} from "../store/actions"; -type IdPayDiscountInstrumentsScreenRouteParams = { +export type IdPayDiscountInstrumentsScreenRouteParams = { initiativeId: string; initiativeName?: string; }; type IdPayDiscountInstrumentsScreenRouteProps = RouteProp< - IDPayConfigurationParamsList, + IdPayConfigurationParamsList, "IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS" >; @@ -48,7 +47,7 @@ type IdPayDiscountInstrumentsScreenRouteProps = RouteProp< * Screen that shows the list of available instruments for a discount initiative which has been selected * Actually are available only the CIE and the QRCode */ -const IdPayDiscountInstrumentsScreen = () => { +export const IdPayDiscountInstrumentsScreen = () => { const dispatch = useIODispatch(); const route = useRoute(); const navigation = @@ -156,7 +155,3 @@ const styles = StyleSheet.create({ paddingHorizontal: customVariables.contentPadding } }); - -export type { IdPayDiscountInstrumentsScreenRouteParams }; - -export default IdPayDiscountInstrumentsScreen; diff --git a/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx b/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx index dc7b8a251ac..366bac30bb5 100644 --- a/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx +++ b/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx @@ -8,8 +8,7 @@ import { VSpacer, useIOTheme } from "@pagopa/io-app-design-system"; -import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; -import { useActor } from "@xstate/react"; +import { useNavigation } from "@react-navigation/native"; import React from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; @@ -20,19 +19,8 @@ import BaseScreenComponent from "../../../../components/screens/BaseScreenCompon import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { LOADING_TAG } from "../../../../xstate/utils"; -import { IDPayConfigurationParamsList } from "../navigation/navigator"; -import { ConfigurationMode } from "../xstate/context"; -import { useConfigurationMachineService } from "../xstate/provider"; - -type InitiativeConfigurationIntroScreenRouteParams = { - initiativeId: string; -}; - -type InitiativeConfigurationIntroRouteProps = RouteProp< - IDPayConfigurationParamsList, - "IDPAY_CONFIGURATION_INTRO" ->; +import { isLoadingSelector } from "../../../../xstate/selectors"; +import { IdPayConfigurationMachineContext } from "../machine/provider"; type RequiredDataItemProps = { icon?: React.ReactNode; @@ -54,21 +42,18 @@ const RequiredDataItem = (props: RequiredDataItemProps) => ( ); -const InitiativeConfigurationIntroScreen = () => { - const navigation = useNavigation(); - const route = useRoute(); - - const { initiativeId } = route.params; +export const InitiativeConfigurationIntroScreen = () => { + const { useActorRef, useSelector } = IdPayConfigurationMachineContext; + const machine = useActorRef(); - const configurationMachine = useConfigurationMachineService(); - const [state, send] = useActor(configurationMachine); + const navigation = useNavigation(); const theme = useIOTheme(); - const isLoading = state.tags.has(LOADING_TAG); + const isLoading = useSelector(isLoadingSelector); const handleContinuePress = () => { - send({ type: "NEXT" }); + machine.send({ type: "next" }); }; const customGoBack = ( @@ -104,14 +89,6 @@ const InitiativeConfigurationIntroScreen = () => { } ]; - React.useEffect(() => { - send({ - type: "START_CONFIGURATION", - initiativeId, - mode: ConfigurationMode.COMPLETE - }); - }, [send, initiativeId]); - return ( ; - -const InstrumentsEnrollmentScreen = () => { - const route = useRoute(); - const { initiativeId } = route.params; +export const InstrumentsEnrollmentScreen = () => { + const navigation = useIONavigation(); + const { useActorRef, useSelector } = IdPayConfigurationMachineContext; + const machine = useActorRef(); const [stagedWalletId, setStagedWalletId] = React.useState(); - const configurationMachine = useConfigurationMachineService(); + const isLoading = useSelector(isLoadingSelector); + const failure = useSelector(failureSelector); - const isLoading = useSelector(configurationMachine, isLoadingSelector); - const failure = useSelector(configurationMachine, failureSelector); + const initiativeDetails = useSelector(selectInitiativeDetails); + const isInstrumentsOnlyMode = useSelector(selectIsInstrumentsOnlyMode); + const walletInstruments = useSelector(selectWalletInstruments); - const initiativeDetails = useSelector( - configurationMachine, - selectInitiativeDetails - ); - const isInstrumentsOnlyMode = useSelector( - configurationMachine, - selectIsInstrumentsOnlyMode - ); - - const walletInstruments = useSelector( - configurationMachine, - selectWalletInstruments - ); - - const isUpserting = useSelector( - configurationMachine, - isUpsertingInstrumentSelector - ); + const isUpserting = useSelector(isUpsertingInstrumentSelector); const initiativeInstrumentsByIdWallet = useSelector( - configurationMachine, initiativeInstrumentsByIdWalletSelector ); @@ -82,38 +58,33 @@ const InstrumentsEnrollmentScreen = () => { Object.keys(initiativeInstrumentsByIdWallet).length > 0; React.useEffect(() => { - if (initiativeId) { - configurationMachine.send({ - type: "START_CONFIGURATION", - initiativeId, - mode: ConfigurationMode.INSTRUMENTS - }); - } - }, [configurationMachine, initiativeId]); - - React.useEffect(() => { - if ( - failure === InitiativeFailureType.INSTRUMENT_ENROLL_FAILURE || - failure === InitiativeFailureType.INSTRUMENT_DELETE_FAILURE - ) { - setStagedWalletId(undefined); - } + pipe( + failure, + O.filter( + failure => + failure === InitiativeFailureType.INSTRUMENT_ENROLL_FAILURE || + failure === InitiativeFailureType.INSTRUMENT_DELETE_FAILURE + ), + O.map(() => setStagedWalletId(undefined)) + ); }, [failure]); - const handleBackPress = () => configurationMachine.send({ type: "BACK" }); + const handleBackPress = () => machine.send({ type: "back" }); - const handleSkipButton = () => configurationMachine.send({ type: "SKIP" }); + const handleSkipButton = () => machine.send({ type: "skip-instruments" }); - const handleContinueButton = () => - configurationMachine.send({ type: "NEXT" }); + const handleContinueButton = () => machine.send({ type: "next" }); const handleAddPaymentMethodButton = () => - configurationMachine.send({ type: "ADD_PAYMENT_METHOD" }); + navigation.replace(ROUTES.WALLET_NAVIGATOR, { + screen: ROUTES.WALLET_ADD_PAYMENT_METHOD, + params: { inPayment: O.none } + }); const handleEnrollConfirm = () => { if (stagedWalletId) { - configurationMachine.send({ - type: "ENROLL_INSTRUMENT", + machine.send({ + type: "enroll-instrument", walletId: stagedWalletId.toString() }); setStagedWalletId(undefined); @@ -217,7 +188,7 @@ const InstrumentsEnrollmentScreen = () => { }; useNavigationSwipeBackListener(() => { - configurationMachine.send({ type: "BACK", skipNavigation: true }); + machine.send({ type: "back", skipNavigation: true }); }); const handleInstrumentValueChange = (wallet: Wallet) => (value: boolean) => { @@ -225,14 +196,20 @@ const InstrumentsEnrollmentScreen = () => { setStagedWalletId(wallet.idWallet); } else { const instrument = initiativeInstrumentsByIdWallet[wallet.idWallet]; - configurationMachine.send({ - type: "DELETE_INSTRUMENT", + machine.send({ + type: "delete-instrument", instrumentId: instrument.instrumentId, walletId: wallet.idWallet.toString() }); } }; + const initiativeName = pipe( + initiativeDetails, + O.map(i => i.initiativeName), + O.toUndefined + ); + return ( <> { {I18n.t("idpay.configuration.instruments.body", { - initiativeName: initiativeDetails?.initiativeName ?? "" + initiativeName })} @@ -280,7 +257,3 @@ const InstrumentsEnrollmentScreen = () => { ); }; - -export type { InstrumentsEnrollmentScreenRouteParams }; - -export default InstrumentsEnrollmentScreen; diff --git a/ts/features/idpay/configuration/screens/__test__/IbanEnrollmentScreen.test.tsx b/ts/features/idpay/configuration/screens/__test__/IbanEnrollmentScreen.test.tsx deleted file mode 100644 index e75284fe1c3..00000000000 --- a/ts/features/idpay/configuration/screens/__test__/IbanEnrollmentScreen.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import * as React from "react"; -import configureMockStore from "redux-mock-store"; -import { interpret } from "xstate"; -import { applicationChangeState } from "../../../../../store/actions/application"; -import { appReducer } from "../../../../../store/reducers"; -import { GlobalState } from "../../../../../store/reducers/types"; -import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; -import { IDPayConfigurationRoutes } from "../../navigation/navigator"; -import { createIDPayInitiativeConfigurationMachine } from "../../xstate/machine"; -import { ConfigurationMachineContext } from "../../xstate/provider"; -import IbanEnrollmentScreen from "../IbanEnrollmentScreen"; -import I18n from "../../../../../i18n"; -import { - ConfigurationMode, - Context, - INITIAL_CONTEXT -} from "../../xstate/context"; - -describe("IbanEnrollmentScreen", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - it("should render the screen correctly", () => { - const { component } = renderComponent(); - expect(component).toBeTruthy(); - }); - it("should render the screen with the right title", () => { - const { component } = renderComponent(); - expect(component).toBeTruthy(); - expect(component).not.toBeNull(); - - const titleComponent = component.queryByText( - I18n.t("idpay.configuration.headerTitle") - ); - expect(titleComponent).not.toBeNull(); - }); - it(`should render "continue" and "add new" button button`, () => { - const { component } = renderComponent(); - expect(component).toBeTruthy(); - expect(component).not.toBeNull(); - - const continueButtonComponent = component.queryByTestId( - "continueButtonTestID" - ); - expect(continueButtonComponent).not.toBeNull(); - - const addIbanButtonComponent = component.queryByTestId( - "addIbanButtonTestID" - ); - expect(addIbanButtonComponent).not.toBeNull(); - }); -}); - -describe("IbanEnrollmentScreen in IBAN only mode", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - it("should render the screen correctly", () => { - const { component } = renderComponent({ mode: ConfigurationMode.IBAN }); - expect(component).toBeTruthy(); - }); - it("should render the screen with the right title", () => { - const { component } = renderComponent({ mode: ConfigurationMode.IBAN }); - expect(component).toBeTruthy(); - expect(component).not.toBeNull(); - - const titleComponent = component.queryByText( - I18n.t("idpay.configuration.iban.title") - ); - expect(titleComponent).not.toBeNull(); - }); - it(`should render only "add new" button button`, () => { - const { component } = renderComponent({ mode: ConfigurationMode.IBAN }); - expect(component).toBeTruthy(); - expect(component).not.toBeNull(); - - const continueButtonComponent = component.queryByTestId( - "continueButtonTestID" - ); - expect(continueButtonComponent).toBeNull(); - - const addIbanButtonComponent = component.queryByTestId( - "addIbanButtonTestID" - ); - expect(addIbanButtonComponent).not.toBeNull(); - }); -}); - -const renderComponent = (context?: Partial) => { - const globalState = appReducer(undefined, applicationChangeState("active")); - - const mockStore = configureMockStore(); - const store: ReturnType = mockStore({ - ...globalState - } as GlobalState); - - const mockMachine = createIDPayInitiativeConfigurationMachine() - .withConfig({ - services: { - confirmIban: jest.fn(), - enrollIban: jest.fn(), - loadIbanList: jest.fn(), - loadInitiative: jest.fn(), - loadWalletInstruments: jest.fn(), - loadInitiativeInstruments: jest.fn(), - instrumentsEnrollmentService: jest.fn() - }, - actions: { - exitConfiguration: jest.fn(), - navigateToAddPaymentMethodScreen: jest.fn(), - navigateToConfigurationIntro: jest.fn(), - navigateToConfigurationSuccessScreen: jest.fn(), - navigateToIbanEnrollmentScreen: jest.fn(), - navigateToIbanLandingScreen: jest.fn(), - navigateToIbanOnboardingScreen: jest.fn(), - navigateToInitiativeDetailScreen: jest.fn(), - navigateToInstrumentsEnrollmentScreen: jest.fn(), - showUpdateIbanToast: jest.fn(), - showFailureToast: jest.fn(), - showInstrumentFailureToast: jest.fn(), - handleSessionExpired: jest.fn() - } - }) - .withContext({ - ...INITIAL_CONTEXT, - ...context - }); - - const mockService = interpret(mockMachine); - - return { - component: renderScreenWithNavigationStoreContext( - () => ( - - - - ), - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, - {}, - store - ), - store - }; -}; diff --git a/ts/features/idpay/configuration/xstate/failure.ts b/ts/features/idpay/configuration/types/failure.ts similarity index 100% rename from ts/features/idpay/configuration/xstate/failure.ts rename to ts/features/idpay/configuration/types/failure.ts diff --git a/ts/features/idpay/configuration/types/index.ts b/ts/features/idpay/configuration/types/index.ts new file mode 100644 index 00000000000..ce58cb587d7 --- /dev/null +++ b/ts/features/idpay/configuration/types/index.ts @@ -0,0 +1,12 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { StatusEnum as InstrumentStatusEnum } from "../../../../../definitions/idpay/InstrumentDTO"; + +export enum ConfigurationMode { + COMPLETE = "COMPLETE", + IBAN = "IBAN", + INSTRUMENTS = "INSTRUMENTS" +} + +export type InstrumentStatusByIdWallet = { + [idWallet: string]: pot.Pot; +}; diff --git a/ts/features/idpay/configuration/xstate/__mocks__/actions.ts b/ts/features/idpay/configuration/xstate/__mocks__/actions.ts deleted file mode 100644 index 1bb97aa6741..00000000000 --- a/ts/features/idpay/configuration/xstate/__mocks__/actions.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const mockActions = { - handleSessionExpired: jest.fn(), - navigateToConfigurationIntro: jest.fn(), - navigateToIbanOnboardingScreen: jest.fn(), - exitConfiguration: jest.fn(), - navigateToConfigurationSuccessScreen: jest.fn(), - navigateToIbanEnrollmentScreen: jest.fn(), - navigateToIbanLandingScreen: jest.fn(), - navigateToInitiativeDetailScreen: jest.fn(), - navigateToInstrumentsEnrollmentScreen: jest.fn(), - navigateToAddPaymentMethodScreen: jest.fn(), - showUpdateIbanToast: jest.fn(), - showFailureToast: jest.fn(), - showInstrumentFailureToast: jest.fn() -}; diff --git a/ts/features/idpay/configuration/xstate/__mocks__/services.ts b/ts/features/idpay/configuration/xstate/__mocks__/services.ts deleted file mode 100644 index 8f948ce79e3..00000000000 --- a/ts/features/idpay/configuration/xstate/__mocks__/services.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Receiver, Sender } from "xstate"; -import { IbanListDTO } from "../../../../../../definitions/idpay/IbanListDTO"; -import { - InitiativeDTO, - StatusEnum -} from "../../../../../../definitions/idpay/InitiativeDTO"; -import { - InstrumentDTO, - InstrumentTypeEnum -} from "../../../../../../definitions/idpay/InstrumentDTO"; - -import { TypeEnum as WalletTypeEnumV1 } from "../../../../../../definitions/pagopa/Wallet"; -import { Wallet } from "../../../../../types/pagopa"; -import { Events } from "../events"; - -export const T_INITIATIVE_ID = "123456"; -export const T_IBAN = "IT60X0542811101000000123456"; -export const T_INSTRUMENT_ID = "123456"; -export const T_WALLET: Wallet = { - idWallet: 123, - type: WalletTypeEnumV1.CREDIT_CARD, - favourite: false, - creditCard: undefined, - psp: undefined, - idPsp: undefined, - pspEditable: false, - lastUsage: undefined, - isPspToIgnore: false, - registeredNexi: false, - saved: true, - paymentMethod: undefined -}; - -export const T_INSTRUMENT_DTO: InstrumentDTO = { - instrumentId: "1234", - idWallet: "12345", - instrumentType: InstrumentTypeEnum.CARD -}; - -export const T_NOT_REFUNDABLE_INITIATIVE_DTO: InitiativeDTO = { - initiativeId: T_INITIATIVE_ID, - status: StatusEnum.NOT_REFUNDABLE, - endDate: new Date("2023-01-25T13:00:25.477Z"), - nInstr: 1 -}; - -export const T_REFUNDABLE_INITIATIVE_DTO: InitiativeDTO = { - initiativeId: T_INITIATIVE_ID, - status: StatusEnum.REFUNDABLE, - endDate: new Date("2023-01-25T13:00:25.477Z"), - nInstr: 1 -}; - -export const T_IBAN_LIST: IbanListDTO["ibanList"] = [ - { - channel: "IO", - checkIbanStatus: "", - description: "Test", - iban: T_IBAN - } -]; - -export const T_PAGOPA_INSTRUMENTS = [T_WALLET]; - -export const mockEnrollInstrument: jest.Mock> = jest.fn( - async () => Promise.resolve(undefined) -); - -export const mockDeleteInstrument: jest.Mock> = jest.fn( - async () => Promise.resolve(undefined) -); - -const mockInstrumentsEnrollmentService = jest.fn( - () => (callback: Sender, onReceive: Receiver) => - onReceive(async event => { - switch (event.type) { - case "DELETE_INSTRUMENT": - mockDeleteInstrument() - .then(() => - callback({ - ...event, - type: "DELETE_INSTRUMENT_SUCCESS" - }) - ) - .catch(() => - callback({ - ...event, - type: "DELETE_INSTRUMENT_FAILURE" - }) - ); - break; - case "ENROLL_INSTRUMENT": - mockEnrollInstrument() - .then(() => - callback({ - ...event, - type: "ENROLL_INSTRUMENT_SUCCESS" - }) - ) - .catch(() => - callback({ - ...event, - type: "ENROLL_INSTRUMENT_FAILURE" - }) - ); - break; - default: - break; - } - }) -); -export const mockServices = { - loadInitiative: jest.fn(), - loadIbanList: jest.fn(), - enrollIban: jest.fn(), - confirmIban: jest.fn(), - loadWalletInstruments: jest.fn(), - loadInitiativeInstruments: jest.fn(), - instrumentsEnrollmentService: mockInstrumentsEnrollmentService -}; diff --git a/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts b/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts deleted file mode 100644 index 57d5f37f112..00000000000 --- a/ts/features/idpay/configuration/xstate/__tests__/actions.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import * as O from "fp-ts/lib/Option"; -import * as p from "@pagopa/ts-commons/lib/pot"; -import { IOToast } from "@pagopa/io-app-design-system"; -import { createActionsImplementation } from "../actions"; -import { ConfigurationMode, Context } from "../context"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../../navigation/params/AppParamsList"; -import { IDPayConfigurationRoutes } from "../../navigation/navigator"; -import ROUTES from "../../../../../navigation/routes"; -import { IDPayDetailsRoutes } from "../../../details/navigation"; -import { InitiativeFailureType } from "../failure"; -import I18n from "../../../../../i18n"; -import { refreshSessionToken } from "../../../../fastLogin/store/actions/tokenRefreshActions"; - -jest.mock("../../../../../utils/showToast", () => ({ - showToast: jest.fn() -})); - -const navigation: Partial> = { - navigate: jest.fn(), - replace: jest.fn(), - pop: jest.fn() -}; - -const dispatch = jest.fn(); - -const T_CONTEXT: Context = { - initiative: p.none, - mode: ConfigurationMode.COMPLETE, - ibanList: p.none, - walletInstruments: [], - initiativeInstruments: [], - instrumentStatuses: {} -}; - -const T_INITIATIVE_ID = "123456"; - -const T_NO_EVENT = { type: "" }; -const T_BACK_EVENT = { type: "BACK", skipNavigation: true }; - -const T_FAILURE = InitiativeFailureType.GENERIC; - -describe("IDPay Configuration machine actions", () => { - const actions = createActionsImplementation( - navigation as IOStackNavigationProp, - dispatch - ); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe("handleSessionExpired", () => { - it("should call dispatch with sessionExpired", async () => { - actions.handleSessionExpired(); - expect(dispatch).toHaveBeenCalledWith( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }); - }); - - describe("navigateToConfigurationIntro", () => { - it("should throw error if initiativeId is not provided in context", async () => { - expect(() => { - actions.navigateToConfigurationIntro(T_CONTEXT, { type: "" }); - }).toThrow("initiativeId is undefined"); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - - it("should navigate to screen", async () => { - actions.navigateToConfigurationIntro( - { ...T_CONTEXT, initiativeId: T_INITIATIVE_ID }, - T_NO_EVENT - ); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, - { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, - params: { - initiativeId: T_INITIATIVE_ID - } - } - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToConfigurationIntro( - { ...T_CONTEXT, initiativeId: T_INITIATIVE_ID }, - T_BACK_EVENT - ); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToIbanLandingScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToIbanLandingScreen(T_CONTEXT, T_NO_EVENT); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, - { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING - } - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToIbanLandingScreen(T_CONTEXT, T_BACK_EVENT); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToIbanOnboardingScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToIbanOnboardingScreen(T_CONTEXT, T_NO_EVENT); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, - { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING - } - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToIbanOnboardingScreen(T_CONTEXT, T_BACK_EVENT); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToIbanEnrollmentScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToIbanEnrollmentScreen(T_CONTEXT, T_NO_EVENT); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, - { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, - params: {} - } - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToIbanEnrollmentScreen(T_CONTEXT, T_BACK_EVENT); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToAddPaymentMethodScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToAddPaymentMethodScreen(T_CONTEXT, T_NO_EVENT); - expect(navigation.replace).toHaveBeenCalledWith(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.WALLET_ADD_PAYMENT_METHOD, - params: { inPayment: O.none } - }); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToAddPaymentMethodScreen(T_CONTEXT, T_BACK_EVENT); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToInstrumentsEnrollmentScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToInstrumentsEnrollmentScreen(T_CONTEXT, T_NO_EVENT); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, - { - screen: - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, - params: {} - } - ); - }); - - it("should not navigate to screen if BACK event with skipNavigation set to true", async () => { - actions.navigateToInstrumentsEnrollmentScreen(T_CONTEXT, T_BACK_EVENT); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - }); - - describe("navigateToConfigurationSuccessScreen", () => { - it("should navigate to screen", async () => { - actions.navigateToConfigurationSuccessScreen(); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, - { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_SUCCESS - } - ); - }); - }); - - describe("navigateToInitiativeDetailScreen", () => { - it("should throw error if initiativeId is not provided in context", async () => { - expect(() => { - actions.navigateToInitiativeDetailScreen(T_CONTEXT); - }).toThrow("initiativeId is undefined"); - expect(navigation.navigate).toHaveBeenCalledTimes(0); - }); - - it("should navigate to screen", async () => { - actions.navigateToInitiativeDetailScreen({ - ...T_CONTEXT, - initiativeId: T_INITIATIVE_ID - }); - expect(navigation.navigate).toHaveBeenCalledWith( - IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, - { - screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, - params: { initiativeId: T_INITIATIVE_ID } - } - ); - }); - }); - - describe("showFailureToast", () => { - it("should show toast", async () => { - const showToastSpy = jest.spyOn(IOToast, "error"); - actions.showFailureToast({ ...T_CONTEXT, failure: T_FAILURE }); - expect(showToastSpy).toHaveBeenCalledWith( - I18n.t(`idpay.configuration.failureStates.${T_FAILURE}`) - ); - }); - }); - - describe("showUpdateIbanToast", () => { - it("should show toast", async () => { - const showToastSpy = jest.spyOn(IOToast, "success"); - actions.showUpdateIbanToast(); - expect(showToastSpy).toHaveBeenCalledWith( - I18n.t(`idpay.configuration.iban.updateToast`) - ); - }); - }); - - describe("exitConfiguration", () => { - it("should close screen", async () => { - actions.exitConfiguration(); - expect(navigation.pop).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/ts/features/idpay/configuration/xstate/__tests__/machine.test.ts b/ts/features/idpay/configuration/xstate/__tests__/machine.test.ts deleted file mode 100644 index dcf9ba166c6..00000000000 --- a/ts/features/idpay/configuration/xstate/__tests__/machine.test.ts +++ /dev/null @@ -1,1088 +0,0 @@ -/* eslint-disable sonarjs/no-identical-functions */ -/* eslint-disable functional/no-let */ -import { waitFor } from "@testing-library/react-native"; -import { interpret, StateValue } from "xstate"; -import { IbanDTO } from "../../../../../../definitions/idpay/IbanDTO"; -import { ConfigurationMode } from "../context"; -import { InitiativeFailureType } from "../failure"; -import { createIDPayInitiativeConfigurationMachine } from "../machine"; -import { - ibanListSelector, - selectInitiativeDetails, - selectWalletInstruments -} from "../selectors"; -import { mockActions } from "../__mocks__/actions"; -import { - mockDeleteInstrument, - mockEnrollInstrument, - mockServices, - T_IBAN, - T_IBAN_LIST, - T_INITIATIVE_ID, - T_INSTRUMENT_DTO, - T_NOT_REFUNDABLE_INITIATIVE_DTO, - T_PAGOPA_INSTRUMENTS, - T_REFUNDABLE_INITIATIVE_DTO, - T_WALLET -} from "../__mocks__/services"; - -const T_IBAN_ENROLL: IbanDTO = { - channel: "IO", - checkIbanStatus: "", - description: "Test", - iban: T_IBAN -}; - -describe("IDPay configuration machine", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should have the default state of WAITING_START", () => { - const machine = createIDPayInitiativeConfigurationMachine(); - expect(machine.initialState.value).toEqual("WAITING_START"); - }); - - it("should not allow the citizen to configure an initiative if it's already configured", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_REFUNDABLE_INITIATIVE_DTO) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(selectInitiativeDetails(currentState as never)).toStrictEqual( - T_REFUNDABLE_INITIATIVE_DTO - ); - - await waitFor(() => - expect( - mockActions.navigateToConfigurationSuccessScreen - ).toHaveBeenCalledTimes(1) - ); - - expect(currentState.value).toMatch("CONFIGURATION_NOT_NEEDED"); - - service.send({ - type: "COMPLETE_CONFIGURATION" - }); - - expect(currentState.value).toMatch("CONFIGURATION_COMPLETED"); - - await waitFor(() => - expect( - mockActions.navigateToInitiativeDetailScreen - ).toHaveBeenCalledTimes(1) - ); - }); - - it("should allow the citizen to configure an initiative", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve(T_PAGOPA_INSTRUMENTS) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - mockEnrollInstrument.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockDeleteInstrument.mockImplementation(async () => - Promise.resolve(undefined) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - expect(currentState.value).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(selectInitiativeDetails(currentState as never)).toStrictEqual( - T_NOT_REFUNDABLE_INITIATIVE_DTO - ); - - expect(currentState.value).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(ibanListSelector(currentState as never)).toStrictEqual(T_IBAN_LIST); - - expect(currentState.value).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: T_IBAN_ENROLL - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - expect(selectWalletInstruments(currentState as never)).toStrictEqual( - T_PAGOPA_INSTRUMENTS - ); - - expect(currentState.value).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "ENROLL_INSTRUMENT", - walletId: T_WALLET.idWallet.toString() - }); - - await waitFor(() => expect(mockEnrollInstrument).toHaveBeenCalledTimes(1)); - - expect(currentState.value).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "DELETE_INSTRUMENT", - walletId: T_WALLET.idWallet.toString(), - instrumentId: T_INSTRUMENT_DTO.instrumentId - }); - - await waitFor(() => expect(mockDeleteInstrument).toHaveBeenCalledTimes(1)); - - expect(currentState.value).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "NEXT" - }); - - expect(currentState.value).toMatch("DISPLAYING_CONFIGURATION_SUCCESS"); - - await waitFor(() => - expect( - mockActions.navigateToConfigurationSuccessScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "COMPLETE_CONFIGURATION" - }); - - expect(currentState.value).toMatch("CONFIGURATION_COMPLETED"); - - await waitFor(() => - expect( - mockActions.navigateToInitiativeDetailScreen - ).toHaveBeenCalledTimes(1) - ); - }); - - it("should allow a citizen without any IBAN to configure an initiative", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: [] }) - ); - - mockServices.confirmIban.mockImplementation(async () => Promise.resolve()); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_ONBOARDING" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanLandingScreen).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_ONBOARDING_FORM" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanOnboardingScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "CONFIRM_IBAN", - ibanBody: { - description: "Test", - iban: T_IBAN - } - }); - - await waitFor(() => - expect(mockServices.confirmIban).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - // From here same as previous test case - }); - - it("should allow a citizen without any instrument to configure an initiative", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: T_IBAN_ENROLL - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_CONFIGURATION_SUCCESS"); - - service.send({ - type: "ADD_PAYMENT_METHOD" - }); - - await waitFor(() => - expect( - mockActions.navigateToAddPaymentMethodScreen - ).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_CONFIGURATION_SUCCESS"); - - service.send({ - type: "COMPLETE_CONFIGURATION" - }); - - expect(currentState).toMatch("CONFIGURATION_COMPLETED"); - - await waitFor(() => - expect( - mockActions.navigateToInitiativeDetailScreen - ).toHaveBeenCalledTimes(1) - ); - }); - - it("should allow the citizen to configure an initiative skipping the instrument step", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve(T_PAGOPA_INSTRUMENTS) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: T_IBAN_ENROLL - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "SKIP" - }); - - expect(currentState).toMatch("DISPLAYING_CONFIGURATION_SUCCESS"); - - await waitFor(() => - expect( - mockActions.navigateToConfigurationSuccessScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "COMPLETE_CONFIGURATION" - }); - - expect(currentState).toMatch("CONFIGURATION_COMPLETED"); - - await waitFor(() => - expect( - mockActions.navigateToInitiativeDetailScreen - ).toHaveBeenCalledTimes(1) - ); - }); - - it("should go to CONFIGURATION_FAILURE if initiative fails to load", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.reject(InitiativeFailureType.GENERIC) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toEqual("CONFIGURATION_FAILURE"); - }); - - it("should show a failure toast if IBAN list fails to load", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.reject(InitiativeFailureType.IBAN_LIST_LOAD_FAILURE) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockActions.showFailureToast).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - }); - - it("should show a failure toast if IBAN fails to enroll", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.reject(InitiativeFailureType.IBAN_ENROLL_FAILURE) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: T_IBAN_ENROLL - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockActions.showFailureToast).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - }); - - it("should show a failure toast if IBAN fails to add", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: [] }) - ); - - mockServices.confirmIban.mockImplementation(async () => - Promise.reject(InitiativeFailureType.IBAN_ENROLL_FAILURE) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_ONBOARDING" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanLandingScreen).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_ONBOARDING_FORM" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanOnboardingScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "CONFIRM_IBAN", - ibanBody: { - description: "Test", - iban: T_IBAN - } - }); - - await waitFor(() => - expect(mockServices.confirmIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockActions.showFailureToast).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_ONBOARDING_FORM" - }); - }); - - it("should show a failure toast if instrument list fails to load", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.reject(InitiativeFailureType.INSTRUMENTS_LIST_LOAD_FAILURE) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: T_IBAN_ENROLL - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockActions.showFailureToast).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - }); - - it("should show a failure toast if instruments fails to enroll/delete", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve(T_PAGOPA_INSTRUMENTS) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - mockEnrollInstrument.mockImplementation(async () => Promise.reject()); - - mockDeleteInstrument.mockImplementation(async () => Promise.reject()); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.COMPLETE - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("DISPLAYING_INTRO"); - - await waitFor(() => - expect(mockActions.navigateToConfigurationIntro).toHaveBeenCalledTimes(1) - ); - - service.send({ type: "NEXT" }); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: T_IBAN_ENROLL - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "ENROLL_INSTRUMENT", - walletId: T_WALLET.idWallet.toString() - }); - - await waitFor(() => expect(mockEnrollInstrument).toHaveBeenCalledTimes(1)); - - await waitFor(() => - expect(mockActions.showInstrumentFailureToast).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "DELETE_INSTRUMENT", - walletId: T_WALLET.idWallet.toString(), - instrumentId: T_INSTRUMENT_DTO.instrumentId - }); - - await waitFor(() => expect(mockDeleteInstrument).toHaveBeenCalledTimes(1)); - - await waitFor(() => - expect(mockActions.showInstrumentFailureToast).toHaveBeenCalledTimes(2) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - }); -}); diff --git a/ts/features/idpay/configuration/xstate/__tests__/machineIban.test.ts b/ts/features/idpay/configuration/xstate/__tests__/machineIban.test.ts deleted file mode 100644 index 7728de18282..00000000000 --- a/ts/features/idpay/configuration/xstate/__tests__/machineIban.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* eslint-disable sonarjs/no-identical-functions */ -/* eslint-disable functional/no-let */ -import { waitFor } from "@testing-library/react-native"; -import { interpret, StateValue } from "xstate"; -import { ConfigurationMode } from "../context"; -import { InitiativeFailureType } from "../failure"; -import { createIDPayInitiativeConfigurationMachine } from "../machine"; -import { mockActions } from "../__mocks__/actions"; -import { - mockServices, - T_IBAN, - T_IBAN_LIST, - T_INITIATIVE_ID, - T_NOT_REFUNDABLE_INITIATIVE_DTO -} from "../__mocks__/services"; - -describe("IDPay configuration machine in IBAN mode", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should allow the citizen to enroll an IBAN to the initiative", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.IBAN - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "ENROLL_IBAN", - iban: { - channel: "IO", - checkIbanStatus: "", - description: "Test", - iban: T_IBAN - } - }); - - await waitFor(() => - expect(mockServices.enrollIban).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.showUpdateIbanToast).toHaveBeenCalledTimes(1) - ); - }); - - it("should exit configuration on BACK event", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.resolve({ ibanList: T_IBAN_LIST }) - ); - - mockServices.enrollIban.mockImplementation(async () => - Promise.resolve(undefined) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.IBAN - }); - - await waitFor(() => expect(mockServices.loadInitiative).toHaveBeenCalled()); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_IBAN: "DISPLAYING_IBAN_LIST" - }); - - await waitFor(() => - expect(mockActions.navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes( - 1 - ) - ); - - service.send({ - type: "BACK" - }); - - expect(currentState).toMatch("CONFIGURATION_CLOSED"); - - await waitFor(() => - expect(mockActions.exitConfiguration).toHaveBeenCalledTimes(1) - ); - }); - - it("should go to CONFIGURATION_FAILURE if IBAN list fails to load", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadIbanList.mockImplementation(async () => - Promise.reject(InitiativeFailureType.IBAN_LIST_LOAD_FAILURE) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toEqual("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.IBAN - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadIbanList).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("CONFIGURATION_FAILURE"); - }); -}); diff --git a/ts/features/idpay/configuration/xstate/__tests__/machineInstruments.test.ts b/ts/features/idpay/configuration/xstate/__tests__/machineInstruments.test.ts deleted file mode 100644 index 95a1fbf4eb6..00000000000 --- a/ts/features/idpay/configuration/xstate/__tests__/machineInstruments.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -/* eslint-disable sonarjs/no-identical-functions */ -/* eslint-disable functional/no-let */ -import { waitFor } from "@testing-library/react-native"; -import { interpret, StateValue } from "xstate"; -import { ConfigurationMode } from "../context"; -import { InitiativeFailureType } from "../failure"; -import { createIDPayInitiativeConfigurationMachine } from "../machine"; -import { mockActions } from "../__mocks__/actions"; -import { - mockDeleteInstrument, - mockEnrollInstrument, - mockServices, - T_INITIATIVE_ID, - T_INSTRUMENT_DTO, - T_NOT_REFUNDABLE_INITIATIVE_DTO, - T_PAGOPA_INSTRUMENTS, - T_WALLET -} from "../__mocks__/services"; - -describe("IDPay configuration machine in INSTRUMENTS mode", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should allow the citizen to enroll/delete an Instrument to the initiative", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve(T_PAGOPA_INSTRUMENTS) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - mockEnrollInstrument.mockImplementation(async () => - Promise.resolve(undefined) - ); - - mockDeleteInstrument.mockImplementation(async () => - Promise.resolve(undefined) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toMatch("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.INSTRUMENTS - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "ENROLL_INSTRUMENT", - walletId: T_WALLET.idWallet.toString() - }); - - await waitFor(() => expect(mockEnrollInstrument).toHaveBeenCalledTimes(1)); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "DELETE_INSTRUMENT", - walletId: T_WALLET.idWallet.toString(), - instrumentId: T_INSTRUMENT_DTO.instrumentId - }); - - await waitFor(() => expect(mockDeleteInstrument).toHaveBeenCalledTimes(1)); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "ADD_PAYMENT_METHOD" - }); - - await waitFor(() => - expect( - mockActions.navigateToAddPaymentMethodScreen - ).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - service.send({ - type: "NEXT" - }); - - expect(currentState).toMatch("CONFIGURATION_COMPLETED"); - - await waitFor(() => - expect( - mockActions.navigateToInitiativeDetailScreen - ).toHaveBeenCalledTimes(1) - ); - }); - - it("should exit configuration on BACK event", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve(T_PAGOPA_INSTRUMENTS) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.resolve([]) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toMatch("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.INSTRUMENTS - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatchObject({ - CONFIGURING_INSTRUMENTS: { - DISPLAYING_INSTRUMENTS: "DISPLAYING" - } - }); - - await waitFor(() => - expect( - mockActions.navigateToInstrumentsEnrollmentScreen - ).toHaveBeenCalledTimes(1) - ); - - service.send({ - type: "BACK" - }); - - expect(currentState).toMatch("CONFIGURATION_CLOSED"); - - await waitFor(() => - expect(mockActions.exitConfiguration).toHaveBeenCalledTimes(1) - ); - }); - - it("should go to CONFIGURATION_FAILURE if instrument list fails to load", async () => { - mockServices.loadInitiative.mockImplementation(async () => - Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) - ); - - mockServices.loadWalletInstruments.mockImplementation(async () => - Promise.resolve(T_PAGOPA_INSTRUMENTS) - ); - - mockServices.loadInitiativeInstruments.mockImplementation(async () => - Promise.reject(InitiativeFailureType.INSTRUMENTS_LIST_LOAD_FAILURE) - ); - - const machine = createIDPayInitiativeConfigurationMachine().withConfig({ - services: mockServices, - actions: mockActions - }); - - let currentState: StateValue = machine.initialState.value; - - const service = interpret(machine).onTransition(state => { - currentState = state.value; - }); - - service.start(); - - expect(currentState).toMatch("WAITING_START"); - - service.send({ - type: "START_CONFIGURATION", - initiativeId: T_INITIATIVE_ID, - mode: ConfigurationMode.INSTRUMENTS - }); - - await waitFor(() => - expect(mockServices.loadInitiative).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadWalletInstruments).toHaveBeenCalledTimes(1) - ); - - await waitFor(() => - expect(mockServices.loadInitiativeInstruments).toHaveBeenCalledTimes(1) - ); - - expect(currentState).toMatch("CONFIGURATION_FAILURE"); - }); -}); diff --git a/ts/features/idpay/configuration/xstate/__tests__/transitions.test.ts b/ts/features/idpay/configuration/xstate/__tests__/transitions.test.ts deleted file mode 100644 index 4d924fe9935..00000000000 --- a/ts/features/idpay/configuration/xstate/__tests__/transitions.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as pot from "@pagopa/ts-commons/lib/pot"; -import { ConfigurationMode, Context, INITIAL_CONTEXT } from "../context"; -import { Events } from "../events"; -import { createIDPayInitiativeConfigurationMachine } from "../machine"; -import { Typegen0 } from "../machine.typegen"; -import { T_IBAN } from "../__mocks__/services"; - -type TransitionTestType = { - currentState: Typegen0["matchesStates"]; - expectedState: Typegen0["matchesStates"]; - event: Events; - context?: Partial; -}; - -const transitions: ReadonlyArray = [ - { - currentState: "CONFIGURING_IBAN.DISPLAYING_IBAN_ONBOARDING", - expectedState: "DISPLAYING_INTRO", - event: { - type: "BACK" - } - }, - { - currentState: "CONFIGURING_IBAN.DISPLAYING_IBAN_ONBOARDING", - expectedState: "CONFIGURATION_CLOSED", - event: { - type: "BACK" - }, - context: { - mode: ConfigurationMode.IBAN - } - }, - { - currentState: "CONFIGURING_IBAN.DISPLAYING_IBAN_ONBOARDING_FORM", - expectedState: "CONFIGURING_IBAN.DISPLAYING_IBAN_LIST", - event: { - type: "BACK" - }, - context: { - ibanList: pot.some([ - { - channel: "IO", - checkIbanStatus: "", - description: "Test", - iban: T_IBAN, - bicCode: "", - checkIbanResponseDate: new Date(), - holderBank: "", - queueDate: "" - } - ]) - } - }, - { - currentState: "CONFIGURING_IBAN.DISPLAYING_IBAN_ONBOARDING_FORM", - expectedState: "CONFIGURING_IBAN.DISPLAYING_IBAN_ONBOARDING", - event: { - type: "BACK" - } - }, - { - currentState: "CONFIGURING_IBAN.DISPLAYING_IBAN_LIST", - expectedState: "CONFIGURATION_CLOSED", - event: { - type: "BACK" - }, - context: { - mode: ConfigurationMode.IBAN - } - }, - { - currentState: "CONFIGURING_IBAN.DISPLAYING_IBAN_LIST", - expectedState: "DISPLAYING_INTRO", - event: { - type: "BACK" - } - }, - { - currentState: "CONFIGURING_INSTRUMENTS.DISPLAYING_INSTRUMENTS", - expectedState: "CONFIGURATION_CLOSED", - event: { - type: "BACK" - }, - context: { - mode: ConfigurationMode.INSTRUMENTS - } - }, - { - currentState: "CONFIGURING_INSTRUMENTS.DISPLAYING_INSTRUMENTS", - expectedState: "CONFIGURING_IBAN", - event: { - type: "BACK" - } - } -]; - -describe("IDPay configuration machine transitions", () => { - transitions.forEach(({ currentState, expectedState, event, context }) => { - it(`should reach "${expectedState}" given "${currentState}" when "${event.type}" event occurs`, () => { - const machine = createIDPayInitiativeConfigurationMachine().withContext({ - ...INITIAL_CONTEXT, - ...context - }); - - const actualState = machine.transition(currentState, event as Events); - - expect(actualState.matches(expectedState)).toBeTruthy(); - }); - }); -}); diff --git a/ts/features/idpay/configuration/xstate/actions.ts b/ts/features/idpay/configuration/xstate/actions.ts deleted file mode 100644 index c4ae50f451e..00000000000 --- a/ts/features/idpay/configuration/xstate/actions.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { IOToast } from "@pagopa/io-app-design-system"; -import I18n from "../../../../i18n"; -import { - AppParamsList, - IOStackNavigationProp -} from "../../../../navigation/params/AppParamsList"; -import ROUTES from "../../../../navigation/routes"; -import { useIODispatch } from "../../../../store/hooks"; -import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; -import { IDPayConfigurationRoutes } from "../navigation/navigator"; -import { IDPayDetailsRoutes } from "../../details/navigation"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; -import { Context } from "./context"; -import { Events } from "./events"; -import { InitiativeFailure, InitiativeFailureType } from "./failure"; - -const createActionsImplementation = ( - navigation: IOStackNavigationProp, - dispatch: ReturnType -) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - - const navigateToConfigurationIntro = guardedNavigationAction( - (args: { context: Context }) => { - if (args.context.initiativeId === undefined) { - throw new Error("initiativeId is undefined"); - } - - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, - params: { - initiativeId: args.context.initiativeId - } - }); - } - ); - - const navigateToIbanLandingScreen = guardedNavigationAction(() => - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING - }) - ); - - const navigateToIbanOnboardingScreen = guardedNavigationAction(() => - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING - }) - ); - - const navigateToIbanEnrollmentScreen = guardedNavigationAction(() => - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, - params: {} - }) - ); - - const navigateToAddPaymentMethodScreen = guardedNavigationAction(() => - navigation.replace(ROUTES.WALLET_NAVIGATOR, { - screen: ROUTES.WALLET_ADD_PAYMENT_METHOD, - params: { inPayment: O.none } - }) - ); - - const navigateToInstrumentsEnrollmentScreen = guardedNavigationAction(() => - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, - params: {} - }) - ); - - const navigateToConfigurationSuccessScreen = () => { - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_SUCCESS - }); - }; - - const navigateToInitiativeDetailScreen = (context: Context) => { - if (context.initiativeId === undefined) { - throw new Error("initiativeId is undefined"); - } - - navigation.navigate(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { - screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, - params: { initiativeId: context.initiativeId } - }); - }; - - const showFailureToast = (context: Context) => { - pipe( - context.failure, - InitiativeFailure.decode, - O.fromEither, - O.chain(failure => { - if (failure !== InitiativeFailureType.SESSION_EXPIRED) { - return O.some(I18n.t(`idpay.configuration.failureStates.${failure}`)); - } - return O.none; - }), - O.map(IOToast.error) - ); - }; - - const showUpdateIbanToast = () => { - IOToast.success(I18n.t(`idpay.configuration.iban.updateToast`)); - }; - - const showInstrumentFailureToast = (_: Context, event: Events) => { - switch (event.type) { - case "ENROLL_INSTRUMENT_FAILURE": - IOToast.error( - I18n.t( - `idpay.configuration.failureStates.${InitiativeFailureType.INSTRUMENT_ENROLL_FAILURE}` - ) - ); - break; - case "DELETE_INSTRUMENT_FAILURE": - IOToast.error( - I18n.t( - `idpay.configuration.failureStates.${InitiativeFailureType.INSTRUMENT_DELETE_FAILURE}` - ) - ); - break; - } - }; - - const exitConfiguration = () => { - navigation.pop(); - }; - - return { - handleSessionExpired, - navigateToConfigurationIntro, - navigateToIbanLandingScreen, - navigateToIbanOnboardingScreen, - navigateToIbanEnrollmentScreen, - navigateToInstrumentsEnrollmentScreen, - navigateToAddPaymentMethodScreen, - navigateToInitiativeDetailScreen, - navigateToConfigurationSuccessScreen, - showFailureToast, - showUpdateIbanToast, - showInstrumentFailureToast, - exitConfiguration - }; -}; - -export { createActionsImplementation }; diff --git a/ts/features/idpay/configuration/xstate/context.ts b/ts/features/idpay/configuration/xstate/context.ts deleted file mode 100644 index fc86f797829..00000000000 --- a/ts/features/idpay/configuration/xstate/context.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as p from "@pagopa/ts-commons/lib/pot"; -import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; -import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; -import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; -import { - InstrumentDTO, - StatusEnum as InstrumentStatusEnum -} from "../../../../../definitions/idpay/InstrumentDTO"; -import { Wallet } from "../../../../types/pagopa"; -import { InitiativeFailureType } from "./failure"; - -export enum ConfigurationMode { - COMPLETE = "COMPLETE", - IBAN = "IBAN", - INSTRUMENTS = "INSTRUMENTS" -} - -export type InstrumentStatusByIdWallet = { - [idWallet: string]: p.Pot; -}; - -export type Context = { - initiativeId?: string; - mode: ConfigurationMode; - initiative: p.Pot; - ibanList: p.Pot, Error>; - walletInstruments: ReadonlyArray; - initiativeInstruments: ReadonlyArray; - instrumentStatuses: InstrumentStatusByIdWallet; - areInstrumentsSkipped?: boolean; - selectedIban?: IbanDTO; - ibanBody?: IbanPutDTO; - failure?: InitiativeFailureType; -}; - -export const INITIAL_CONTEXT: Context = { - initiative: p.none, - mode: ConfigurationMode.COMPLETE, - ibanList: p.none, - walletInstruments: [], - initiativeInstruments: [], - instrumentStatuses: {} -}; diff --git a/ts/features/idpay/configuration/xstate/events.ts b/ts/features/idpay/configuration/xstate/events.ts deleted file mode 100644 index 8c37649de97..00000000000 --- a/ts/features/idpay/configuration/xstate/events.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; -import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; -import { Back } from "../../../../xstate/types/events"; -import { ConfigurationMode } from "./context"; - -type E_START_CONFIGURATION = { - type: "START_CONFIGURATION"; - initiativeId: string; - mode: ConfigurationMode; -}; - -type E_NEW_IBAN_ONBOARDING = { - type: "NEW_IBAN_ONBOARDING"; -}; - -type E_CONFIRM_IBAN = { - type: "CONFIRM_IBAN"; - ibanBody: IbanPutDTO; -}; - -type E_ENROLL_IBAN = { - type: "ENROLL_IBAN"; - iban: IbanDTO; -}; - -type E_ENROLL_INSTRUMENT = { - type: "ENROLL_INSTRUMENT"; - walletId: string; -}; - -type E_ENROLL_INSTRUMENT_FAILURE = { - type: "ENROLL_INSTRUMENT_FAILURE"; - walletId: string; -}; - -type E_ENROLL_INSTRUMENT_SUCCESS = { - type: "ENROLL_INSTRUMENT_SUCCESS"; - walletId: string; -}; - -type E_DELETE_INSTRUMENT = { - type: "DELETE_INSTRUMENT"; - instrumentId: string; - walletId: string; -}; - -type E_DELETE_INSTRUMENT_SUCCESS = { - type: "DELETE_INSTRUMENT_SUCCESS"; - instrumentId: string; - walletId: string; -}; - -type E_DELETE_INSTRUMENT_FAILURE = { - type: "DELETE_INSTRUMENT_FAILURE"; - instrumentId: string; - walletId: string; -}; - -type E_ADD_PAYMENT_METHOD = { - type: "ADD_PAYMENT_METHOD"; -}; - -type E_COMPLETE_CONFIGURATION = { - type: "COMPLETE_CONFIGURATION"; -}; - -type E_SKIP = { - type: "SKIP"; -}; - -type E_NEXT = { - type: "NEXT"; -}; - -type E_QUIT = { - type: "QUIT"; -}; - -export type Events = - | E_START_CONFIGURATION - | E_NEW_IBAN_ONBOARDING - | E_CONFIRM_IBAN - | E_ENROLL_IBAN - | E_ENROLL_INSTRUMENT - | E_ENROLL_INSTRUMENT_SUCCESS - | E_ENROLL_INSTRUMENT_FAILURE - | E_DELETE_INSTRUMENT - | E_DELETE_INSTRUMENT_SUCCESS - | E_DELETE_INSTRUMENT_FAILURE - | E_ADD_PAYMENT_METHOD - | E_COMPLETE_CONFIGURATION - | E_SKIP - | E_NEXT - | Back - | E_QUIT; diff --git a/ts/features/idpay/configuration/xstate/machine.ts b/ts/features/idpay/configuration/xstate/machine.ts deleted file mode 100644 index 785494589c7..00000000000 --- a/ts/features/idpay/configuration/xstate/machine.ts +++ /dev/null @@ -1,805 +0,0 @@ -import { assign, createMachine, forwardTo } from "xstate"; -import { - LOADING_TAG, - UPSERTING_TAG, - WAITING_USER_INPUT_TAG -} from "../../../../xstate/utils"; -import { INITIAL_CONTEXT } from "./context"; - -/** PLEASE DO NO USE AUTO-LAYOUT WHEN USING VISUAL EDITOR */ -export const idPayInitiativeConfigurationMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QCUDyqAqBiAigVQEkMBtABgF1FQAHAe1gEsAXB2gOypAA9EBWXgIy8AdAHYALPwCc4qaSlSBAZgA0IAJ6JRvKcNKiAbKPkGDADgNKlBgL421aTMIDqAQSIEAcgHEA+gGUMV2RsQOCMXwBhVE8AMQJvPGRXDAIYskokEDpGFnZOHgQAJjNDYVNSAXEDIqV+USK1TWKzXXkFXlEhUiLSAyq7B3QMYQAZVFcAES8-Lw8UggA1AFEsCHYwYQY2ADdaAGtNgBtaAEMIAjZmBlOWHbAMzhzr-KzC3lKi4SVapVkFJTaJqIARmL68UiQqS8IrGGRFKSDECOEbjKYzXxzVILFZYMAAJ3xtHxwmoR1uADNiQBbYQnc6Xa63Bj3R5ZZ55DhvPifb6-f5SQG8YEIJSkcTCMwCwSAqSfJEo4TLRauUZ4BY+TGeeapFZRGLxRLJVIxLBsmj0F5c0CFAydESVCy8ao6CxSEVFASkPSGZ28O2iVoGUhmBXDJUqtUa2ba7G65b6uIJJILU3EASZC25VjW7gg-QigRyESlXh-KSiaFyu1hpzK1Xq1KarEEHEJ6JJo2pzxmoqZ7KWzkFRC1QXCBTBj6g4xVAyFgQSH2WST8cziIpFWsjetRpsxnVLdsG5PGtI94hKfscnPDhABh2gu0u6vujSIcTiURLmqGSxFAziFKW7CNM-gAAqjK4ACaGJeBgaBYJ4ywABokBQTyDje3IIJ0ShmJKSgyH0oLiICqhvggAgIuChh4XaOgwpYwEdoaSSwQAQq4PbrGwmywEwtybIqLEnhxXHmgO2avDafCCCIEjSLI7TKCKgLBsIXr6JIsignhzHHkaYncRswj8YJwjCQZbHNpxnjplemHSXmOH-l8RSSF64ghtYliFuIVF8gBPxynI1iiPpnbWbMtljBM0w2VxvijAQgRrCZ2x7IcdJnBcABGpxsKMDD8RJ15OYUPyQsIOi0ZCzrmCpFHudUBEwr6C5eUx9jIuGImGQlnixeiA1JSl2AEkSJJkpSNLZQy+WFcVTClY5ubvII3oKToSkKI1zQ-P6wjVGYZj6DIEJyJu3WWZFyBGUN8XRYlyWpRNxKkuSTBUvitL0nlBVFSVGYYVJa2yUIYgrjIci7eRzT-oCehmB87Q-P5iLXb1Vl3QNEYNtGmK2aNqUraDt7WEoAiQzV-nmPojRNZ+BjjoG-COpT4WY04fVRYTXF47uRnE9g9kg1a5OWFTCkVrTp0NB6VgShuUqfvofysxFrE409g2gRB0FCzE7ETMgj2IShaEOWT2H+XhBFSOYgaAaCZgK+u1UlNUG7mDoAia6JuN65BMEjUbJtm5xkQANKk+L2H8BDW3Q8pcP5lVnR2pUa7+nK-v9TrIEpfrIc674YfBBHrjR6L7KrbeCfyVDO2KKnxQfMzHydAuauEUBXMjDz2t87rRfB4bnjGxXGKxKgyAALJYCJ8-D7HQ421U3qeoB-nt1R4gekWIj2gYcqnTIAi2P3wiD-dQcG6HE-h9Ps8L5HMfobX1syQgn529o1gVn4HUT0HoSib2hHhB2gFvZ515gQGKd8S7DzLo-KemoZ7zywG-GuWY47f1tvhMwhFHZSjMC7UBUgvg9CISfLy0JjCXyGNzbG90l5zyMmlXiWxdgHE2AAY3YBSBgP0CALVXlhfByhCHENKKQ8hFEhCEUCpTMUXkqiVlgUPeB-M2EcLelNT631aQCLYEIkRYiP64LXpIu2RCHayOdidD0+gRAX0FJCIQp1SyaNvqPe+pcXrYGwZYySeDnIN2pttGGLdCykEEEuBEgI6p9B8YHPxSDtGeGFlgquMdgafzCetROTdol7RHGWZmIZ-TuS3oCUQnMmEDxYWk8CY8RqBPNs4ZB5dTYzHEeVd8cthCwlED8ZGYpAzIwVmKMQ0JoEQgaNURhPVmG3V8a0-xyCOnLE8GgUYowV4hLKmDH+rQvh2lqFKHoXQ8KFhkEoPQCIajyB6MFXgqSC47L2clAanDNgZV4cIMAbAiRHCOKIgq-STm-3wv-E+2gywwgEM4pR8zKF-FXEQj5w8lS7NQPsjhPF-k8KysC0F4KxH5KsRI5yMKxAVMAYikBCi7T4QznUTuC4JBXUadfZpny8UEt+foj6M0fpApBbQMFEK2BQtvHSuFjLgHIqakIXQJ1WjRPMA7ZZN0tawU8IEZAeA547IwP4P5pkBJMCEljNZzZDXwRNWa-wcrsKjgeROOJZDSiVGqKpaEVNkbyEBFKf8phsVeCNc6zw5rLVmRtRZO1+qHXRtNbG11VLQnWOch68cDtvXTj9XOCiakqbBi0ooeQDQL6Rsdca9N5qHoGrTS6+N1rbWrJTTGVtGbm2pqdY211Ry67YQEIo4Z64ijOmgRIMEHo4T5s9BuAtsh3lXxvgOhtLr+09sHW2olVrzJ6oDnu7dfa0SPS1L281ODs00sKOOssk6NwzrBHOhm8NTBfFkL0MigIaniDrTe-wu7r37oze249ybT3gfPU2y9LaIO3r7GLHNhQ81eqnL62cqkz4aUhEQ3+CIJDAeQ6BxDW6Y0IbihiNw+zlgRCjeR3dlqAVZT+s4U4YKwBMEuPxfEABXakwKmCwDdd-YMzND51C8V6UwqlehfloUQhxRYMa8s3We6jFHaNUaHWB+joxGNwZ06xkV00vqzU49xo4vH+NMCEyJtgYmJO5sIp6gt2GZz+tLb3coMhVNO3U2R+DunhraYM5RvwRmTPMbC+Zwk71LNGLmhALjPG+NsAE8J0T4ms3HNvJhrzPqfMlvhjoXQnc-gbinTLULZnoumai3p-ccZDzNZ3dFtjJLjg5UZCwZk9wHNOby2594Y5ISQkrP6D8oJP0jlOm5dcIY-UnRPg1lrEXOsXta1qA8ep4uNda3iJLBixW-X61cQbdwwAjdyy58TI6v7ubHFh0rxaPSnWZv+eok4pQNJWU0+1kWut7aO1tq9LY2w7ZoxF07k1RVWfFX9AbNxbv3ec65gro7v7Ff+0W3DjNKwaUDPcnQJgNNA75SD2HoHEFIbC1gMI3gEwQ7NeNkE0gfTrhhOt-0pRQHaDEFUWEYvJbik2zuhn+m21fPxQc9nsbOeUV+2IO0J8tJCGna7JqwZdCSEpsWRQNMpd9pl6DyDkxljGYwGz+t1GVdUQAuUeQbNDAnQrCiqmQo2r0wRGCM3TaLd06wFMSYvgwLQUbb4U1GAAASqBJhO5hJvPCfwPOfgdgfEn-5NLikInKQHJ78508LhsjJDuh05Ors9wpI4PP5oJzh3zzRSjqvDXEowFQeXU602XkPSu43YJxy9jDjf3uE9byOdc+EjAnQWUb2QSgg-0-SYznT5tUJO5oZKGpCgpSdGMLr+GH4vwKCnaOB2DRV-l+Lhv6v-go4EDAjv6we-1wH8kPUkMCt4nOjd0qGUD+AsFv3lyFUt2wEPXY02DJSlXBWy0cwe2Wjr3QxBAnRqWnQ-HfWgQVnMHKDoWhm0C9AdjAMFR+UgIR2S0MVmjgOlUQNGxcydwwKnTfVhFwMZhqD0HhArWnVMCAw3X5QHxt0YwfzNR60yk2AgDADsxtUx1E2YOfUwLYI-QPlhD3zIQrCMHcjLFv2t1tzENjSoPO2R1pGkNkLuwYOQMUIeWUOwPYPnSag3CphKFnDFzFBDGAhD0Hm7ACDwEiEiGWH8AtWiDnggkYyPFum7BVwiSTmbjKVFCGQrSIXHW6DkAEN5W8Oxl8P8H8MCOCLD0mAjyjyghjzj0T2T1QIfUQGsB0HHH8gRE-GRh0lUnMAeSqDIVIHT0-GdF7xLxTBNCyU8EwF8CQmWGt0mEXlQDCNt0iK1miKqIGWKAnxKyn3KxqIUF0DqC9FqGfBIjsG6jYFoGkPgCyBRDQ2qJwhFBkHzU2LuLuIyOpzcA8E1DCBCAuKWPDR+3cQ-FMErASMphoifHMEsGRkDF1XDCa2h3jA+JOX4BKA0jklGUECnSBCalaCXCUl0m70-GAh3EbANQOzmJPG7FhNvGeW9FIgD14KdmcQlCZjLHkA8galDCvkH1jTQDJPjl5B+EVk2KFA9H6F0AYTqkAnFDiUjVsi5Lx2Rjn2DDcI5XlgUQ8j3xqC5RPl6HXU0yEMyTA0yWFmlNzTkGZgqAVPqAWzblKGqjwlIkP3fS6MlP5nxIJn1MCUNIqkL3qOUHHXMCIUqGFE4MIXHQsDzw5msEdJHgr3Hknl6R8HdJqKWw0l6FKDqB8nOgVhDBZlU1kH4Jv0ENp11MHyJh6SvQwTnnjJ-jqQ0gUDiUUEAi6DtAoQlFDTBAWW1wrAjJp3iHniMgrLwlqG+DqlhAhCI33iahDF0BqFHEsAkD6El3zO7RxSLOejGgrI2geRqCZn0G0CrQDNP16FanNKIUYk7PAIoOHgrI-A+ElB6JuX12nTpO9HW2oUDTFD0gXNg11P1J8MGP1BmIiMmDXOrWGVfQviqScRZS6HKGsGnU6EPzqlXzXNbiLFIklG0FIlBEqA5RXw-NLyH3CyhyrxdQrK9BVQqxPmGRdhVm2L9lwrgSIt2223wsM1VFtzpxIq4MPlhGRgvgAkDAtMpnkFJ1BEEBnAUmLxgzwoYrh0IpAxYoYyY2koIpmArM9G9C4tZl4qaIEqLBNJuUkBKAuRSToq0SUr1LMqa1i0UrksvT8ICKCP8ArIsGbLqAdmnXURq0UwRPXFF0fAhC6m1ILIsvB2CqYtjFbHjHYoKTQMolkAXS6JFxhjFH4KEFvyhNCsIqJLL2ixIq9yan+MotlNMEECLDSpCpspCqyuYtstyPsuCKcpdysB0BqBRNGXXC+w3HHBBL-BIysD0PX1lwzRIphAeTarm1plInFAVnqQ0iMD9EqARnDJMsMKbTPJWorMrAeVaB4qLC6LBAvgViIRfS6D6FaHbj0JEL3Fhw2qlGGQdjmQ-Adm2LpIlC0Jmw3i6NhFv3wr-PCLt0AuisuN6CEorBKA3B6EoUUDIobzISOhq3U2DF2IkqcCyKiN-NqvyMcsBqWJgvBBlnRV6AAgEqdld23I-HLHkE0V8OGIiDGImJIq6K-GsD3gsFGW7nWNFC0O+Auj+LBMBCpt-NCL+vGLXJ5P5H5LRP2hkC-GBrkChGDHqQFrPCiHGH8BFuxrhLFr5IBElsQAP3HDz2MCsD6D4qVpiF8FiHcDVGQGWFFoaD5G1sFF1tFEAgN0AlqM9GXE5jsCAA */ - createMachine( - { - context: INITIAL_CONTEXT, - id: "ROOT", - initial: "WAITING_START", - on: { - /** - * Global event which closes the configuration - */ - QUIT: { - target: "#ROOT.CONFIGURATION_CLOSED" - } - }, - states: { - /** - * In this state the machine is waiting for the start of the configuration - */ - WAITING_START: { - tags: [LOADING_TAG], - on: { - /** - * Event which starts the configuration setting the initiative and configuration mode to the context - */ - START_CONFIGURATION: { - target: "LOADING_INITIATIVE", - actions: "startConfiguration" - } - } - }, - - /** - * In this state the machine is fetching the initiative details. - * If success the received data is set to the context. - * If error the machine sets the failure in the context and transits to the failure state - */ - LOADING_INITIATIVE: { - tags: [LOADING_TAG], - invoke: { - src: "loadInitiative", - id: "loadInitiative", - onDone: { - target: "EVALUATING_INITIATIVE_CONFIGURATION", - actions: "loadInitiativeSuccess" - }, - onError: [ - { - guard: "isSessionExpired", - target: "SESSION_EXPIRED" - }, - { - target: "CONFIGURATION_FAILURE", - actions: "setFailure" - } - ] - } - }, - - /** - * Evaluation state. Based on the received data in the previous state, the machines decides which - * state to go next. - */ - EVALUATING_INITIATIVE_CONFIGURATION: { - tags: [LOADING_TAG], - always: [ - { - /** - * Configuration in "INSTRUMENTS" mode - */ - guard: "isInstrumentsOnlyMode", - target: "CONFIGURING_INSTRUMENTS" - }, - { - /** - * Configuration in "IBAN" mode - */ - guard: "isIbanOnlyMode", - target: "CONFIGURING_IBAN" - }, - { - /** - * Configuration in "COMPLETE" mode, no iban or instruments already configured - */ - guard: "isInitiativeConfigurationNeeded", - target: "DISPLAYING_INTRO" - }, - { - /** - * Configuration not needed (instruments and/or iban already configured) - */ - target: "CONFIGURATION_NOT_NEEDED" - } - ] - }, - - /** - * Configuration intro where we show what the user needs to configure the initiaitve - */ - DISPLAYING_INTRO: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToConfigurationIntro", - on: { - /** - * Generic event to go to the next state - */ - NEXT: { - target: "CONFIGURING_IBAN" - } - } - }, - /** - * Iban configuration states. - * As soon as the machines enteres in this state, the "LOADING_IBAN_LIST" state is started - */ - CONFIGURING_IBAN: { - id: "IBAN", - initial: "LOADING_IBAN_LIST", - states: { - /** - * In this state we are fetching the iban list of the user. - * If success, the `loadIbanListSuccess` actions sets the received data to the context - */ - LOADING_IBAN_LIST: { - tags: [LOADING_TAG], - invoke: { - src: "loadIbanList", - id: "loadIbanList", - onDone: { - target: "EVALUATING_IBAN_LIST", - actions: "loadIbanListSuccess" - }, - onError: [ - { - guard: "isSessionExpired", - target: "#ROOT.SESSION_EXPIRED" - }, - { - /** - * If configuration mode is "IBAN", the machine should set the received failure to the - * context and go to the failure state - */ - guard: "isIbanOnlyMode", - target: "#ROOT.CONFIGURATION_FAILURE", - actions: "setFailure" - }, - { - /** - * If configuration mode is "COMPLETE", the machine should set the received failure to the - * context and go to the previous state - */ - target: "#ROOT.DISPLAYING_INTRO", - actions: ["setFailure", "showFailureToast"] - } - ] - } - }, - - /** - * In this state we are checking if the user has available ibans. - */ - EVALUATING_IBAN_LIST: { - tags: [LOADING_TAG], - always: [ - { - /** - * If at least one iban is present, next state should be the iban selection state. - */ - target: "DISPLAYING_IBAN_LIST", - guard: "hasIbanList" - }, - { - /** - * If no iban is present, next state should be the iban onboarding. - */ - target: "DISPLAYING_IBAN_ONBOARDING" - } - ] - }, - - /** - * In this state we are showing to the user why there is a need of an IBAN - */ - DISPLAYING_IBAN_ONBOARDING: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToIbanLandingScreen", - on: { - /** - * Generic next event to go to the next state - */ - NEXT: { - target: "DISPLAYING_IBAN_ONBOARDING_FORM" - }, - /** - * Generic back event - */ - BACK: [ - { - /** - * If configuration mode is "IBAN", the machine should go the the final state - */ - guard: "isIbanOnlyMode", - target: "#ROOT.CONFIGURATION_CLOSED" - }, - { - /** - * If configuration mode is "COMPLETE", the machine should go back to the previous state - */ - target: "#ROOT.DISPLAYING_INTRO" - } - ] - } - }, - - /** - * In this state we are showing the IBAN onboarding form - */ - DISPLAYING_IBAN_ONBOARDING_FORM: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToIbanOnboardingScreen", - on: { - /** - * This event sets the created iban to the context wiht the `confirmIbanOnboarding` actions - * and then goes to the `CONFIRMING_IBAN` state - */ - CONFIRM_IBAN: { - target: "CONFIRMING_IBAN", - actions: "confirmIbanOnboarding" - }, - /** - * Generic back event - */ - BACK: [ - { - /** - * If the user has at least one IBAN, the machine goes to the display state - */ - guard: "hasIbanList", - target: "DISPLAYING_IBAN_LIST" - }, - { - /** - * If the user does not have an IBABN, the machine goes to the previous state - */ - target: "DISPLAYING_IBAN_ONBOARDING" - } - ] - } - }, - - /** - * In this state the machine is sending the IBAN to be created. - * If success, the machine goes to the final state. - * If error, the machine returns to the form and shows a failure - */ - CONFIRMING_IBAN: { - tags: [LOADING_TAG], - invoke: { - src: "confirmIban", - id: "confirmIban", - onDone: { - target: "IBAN_CONFIGURATION_COMPLETED" - }, - onError: [ - { - guard: "isSessionExpired", - target: "#ROOT.SESSION_EXPIRED" - }, - { - target: "DISPLAYING_IBAN_ONBOARDING_FORM", - actions: ["setFailure", "showFailureToast"] - } - ] - } - }, - - /** - * In this state the machine shows the IBAN list to the user. - * On entry, it navigates to the IBAN list screen - */ - DISPLAYING_IBAN_LIST: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToIbanEnrollmentScreen", - on: { - /** - * Generic back event - */ - BACK: [ - { - /** - * If the configuration mode is "IBAN", the configuration should be closed - */ - guard: "isIbanOnlyMode", - target: "#ROOT.CONFIGURATION_CLOSED" - }, - { - /** - * If the configuration mode is "COMPLETE", the machine should go back to the previous state - */ - target: "#ROOT.DISPLAYING_INTRO" - } - ], - /** - * Event to transit to the IBAN creation - */ - NEW_IBAN_ONBOARDING: { - target: "DISPLAYING_IBAN_ONBOARDING_FORM" - }, - /** - * This event set the selected IBAN in the context and prepares it to be enrolled in the next state. - */ - ENROLL_IBAN: { - target: "ENROLLING_IBAN", - actions: "selectIban" - } - } - }, - - /** - * In this state the selected iban is being enrolled to the initiative with the `enrollIban` service - * If success, the selected iban is removed from the context. - * If error, the received failuer is put in the context and a failure toast is showed. - */ - ENROLLING_IBAN: { - tags: [UPSERTING_TAG], - invoke: { - src: "enrollIban", - id: "enrollIban", - onDone: [ - { - /** - * If success and configuration mode is "IBAN", the next state is the IBAN list - * A success toast is displayed - */ - guard: "isIbanOnlyMode", - target: "DISPLAYING_IBAN_LIST", - actions: ["enrollIbanSuccess", "showUpdateIbanToast"] - }, - { - /** - * If success and configuration mode is "COMPLETE", the next state is the final state of the IBAN - */ - target: "IBAN_CONFIGURATION_COMPLETED", - actions: "enrollIbanSuccess" - } - ], - onError: [ - { - guard: "isSessionExpired", - target: "#ROOT.SESSION_EXPIRED" - }, - { - target: "DISPLAYING_IBAN_LIST", - actions: ["setFailure", "showFailureToast"] - } - ] - } - }, - /** - * Final Iban state, it triggers the parent `onDone` - */ - IBAN_CONFIGURATION_COMPLETED: { - type: "final" - } - }, - onDone: [ - { - /** - * If configuration mode is "IBAN", the configuration si completed - */ - guard: "isIbanOnlyMode", - target: "CONFIGURATION_COMPLETED" - }, - { - /** - * If configuration mode is "COMPLETE", the next state is the instruments steps - */ - target: "#ROOT.CONFIGURING_INSTRUMENTS" - } - ] - }, - - /** - * Payment instrument configuration states. - * As soon as the machines enteres in this state, the "LOADING_INSTRUMENTS" state is started - */ - CONFIGURING_INSTRUMENTS: { - id: "INSTRUMENTS", - initial: "LOADING_INSTRUMENTS", - states: { - /** - * In this state we are fetching the user's instruments. - * This is a parallel state, which means that all the child states are executes simultaneously. - * So as soon as the machine enters in this state, both `LOADING_WALLET_INSTRUMENTS` and `LOADING_INITIATIVE_INSTRUMENTS` starts - */ - LOADING_INSTRUMENTS: { - tags: [LOADING_TAG], - entry: "navigateToInstrumentsEnrollmentScreen", - type: "parallel", - states: { - /** - * In this state we are fetching the PagoPA payment instruments of the user. - * On success we set the received instrument to the context - * - * Note: instead to have a single state we have two child states (LOADING and LOAD_SUCCESS). - * This, unfortunately, is due to a limitation (or bug) of XState. A single state does not work. - */ - LOADING_WALLET_INSTRUMENTS: { - initial: "LOADING", - states: { - LOADING: { - invoke: { - src: "loadWalletInstruments", - id: "loadWalletInstruments", - onDone: { - target: "LOAD_SUCCESS", - actions: "loadWalletInstrumentsSuccess" - }, - onError: [ - { - guard: "isSessionExpired", - target: "#ROOT.SESSION_EXPIRED" - }, - { - guard: "isInstrumentsOnlyMode", - target: "#ROOT.CONFIGURATION_FAILURE", - actions: "setFailure" - }, - { - target: "#ROOT.CONFIGURING_IBAN", - actions: ["setFailure", "showFailureToast"] - } - ] - } - }, - LOAD_SUCCESS: { - type: "final" - } - } - }, - - /** - * In this state we are fetching the IDPay payment instruments of the user. - * On success we set the received instrument to the context - * - * Note: instead to have a single state we have two child states (LOADING and LOAD_SUCCESS). - * This, unfortunately, is due to a limitation (or bug) of XState. A single state does not work. - */ - LOADING_INITIATIVE_INSTRUMENTS: { - initial: "LOADING", - states: { - LOADING: { - invoke: { - src: "loadInitiativeInstruments", - id: "loadInitiativeInstruments", - onDone: { - target: "LOAD_SUCCESS", - actions: "loadInitiativeInstrumentsSuccess" - }, - onError: [ - { - guard: "isSessionExpired", - target: "#ROOT.SESSION_EXPIRED" - }, - { - guard: "isInstrumentsOnlyMode", - target: "#ROOT.CONFIGURATION_FAILURE", - actions: "setFailure" - }, - { - target: "#ROOT.CONFIGURING_IBAN", - actions: ["setFailure", "showFailureToast"] - } - ] - } - }, - LOAD_SUCCESS: { - type: "final" - } - } - } - }, - onDone: [ - { - /** - * If the user has PagoPA instruments we go to the state where we show the instrument toggles - */ - guard: "hasInstruments", - target: "DISPLAYING_INSTRUMENTS" - }, - { - /** - * If the configuration mode is "INSTRUMENT", we go to the state where we show the instrument toggles. - * In this case we do not care if the user does not have any PagoPA instrument - */ - guard: "isInstrumentsOnlyMode", - target: "DISPLAYING_INSTRUMENTS" - }, - { - /** - * User has no instrument to show, the machine goes to the success state and inform the user that - * he should add an instrument in order to use the initiative - */ - target: "#ROOT.DISPLAYING_CONFIGURATION_SUCCESS" - } - ] - }, - - /** - * In this state we are showing the instruments list to the user. - * On entry we are updating the instrument statuses in the context to show the correct status in the instrument switch. - * This state implements a polling mechanism using XState delays (https://xstate.js.org/docs/guides/delays.html) - */ - DISPLAYING_INSTRUMENTS: { - tags: [WAITING_USER_INPUT_TAG], - entry: "updateInstrumentStatuses", - initial: "DISPLAYING", - invoke: { - id: "instrumentsEnrollmentService", - src: "instrumentsEnrollmentService" - }, - on: { - /** - * This event forwards the "ENROLL_INSTRUMENT" event to instrumentsEnrollmentService. - */ - ENROLL_INSTRUMENT: { - actions: [ - "updateInstrumentEnrollStatus", - "forwardToInstrumentsEnrollmentService" - ] - }, - - /** - * This event is called by instrumentsEnrollmentService when an instrument is enrolled successfully - */ - ENROLL_INSTRUMENT_SUCCESS: { - actions: "updateInstrumentEnrollStatusSuccess" - }, - - /** - * This event is called by instrumentsEnrollmentService when there is a failure in the instrument enrollment - */ - ENROLL_INSTRUMENT_FAILURE: { - actions: [ - "updateInstrumentEnrollStatusFailure", - "showInstrumentFailureToast" - ] - }, - - /** - * This event forwards the "DELETE_INSTRUMEMT" event to instrumentsEnrollmentService. - */ - DELETE_INSTRUMENT: { - actions: [ - "updateInstrumentDeleteStatus", - "forwardToInstrumentsEnrollmentService" - ] - }, - - /** - * This event is called by instrumentsEnrollmentService when an instrument is deactivated successfully - */ - DELETE_INSTRUMENT_SUCCESS: { - actions: "updateInstrumentDeleteStatusSuccess" - }, - - /** - * This event is called by instrumentsEnrollmentService when there is a failure in the instrument deactivation - */ - DELETE_INSTRUMENT_FAILURE: { - actions: [ - "updateInstrumentDeleteStatusFailure", - "showInstrumentFailureToast" - ] - }, - - /** - * Navigates to the payment method form - */ - ADD_PAYMENT_METHOD: { - actions: "navigateToAddPaymentMethodScreen" - }, - - /** - * Back navigation event - */ - BACK: [ - { - /** - * If we are configuring instruments only, back navigation should close the configuration flow - */ - guard: "isInstrumentsOnlyMode", - target: "#ROOT.CONFIGURATION_CLOSED" - }, - { - /** - * If we are configuring the entire initiative, back navigation should go back to previous state - */ - target: "#ROOT.CONFIGURING_IBAN" - } - ], - - /** - * Default next event, we are going to the next state which completes the instruments configurations - */ - NEXT: { - target: "INSTRUMENTS_COMPLETED" - }, - - /** - * This event is like the NEXT event, except it sets to the context the `areInstrumentsSkipped` flag. - * This flag is used to display additional CTA at the end of the configuration process (DISPLAYING_CONFIGURATION_SUCCESS) - */ - SKIP: { - target: "INSTRUMENTS_COMPLETED", - actions: "skipInstruments" - } - }, - states: { - /** - * In this state we are displaying the instruments, after REFRESHING_INSTRUMENTS_STATES delay the substate transitions to - * the REFRESHING_INSTRUMENTS_STATES state - */ - DISPLAYING: { - after: { - INSTRUMENTS_POLLING_INTERVAL: { - target: "REFRESHING_INSTRUMENTS_STATES" - } - } - }, - /** - * In this state instruments states are refreshed by invoking loadInitiativeInstruments service and then returns to the - * DISPLAYING state. - */ - REFRESHING_INSTRUMENTS_STATES: { - invoke: { - src: "loadInitiativeInstruments", - id: "loadInitiativeInstruments", - onDone: { - target: "DISPLAYING", - actions: [ - "loadInitiativeInstrumentsSuccess", - "updateInstrumentStatuses" - ] - }, - onError: [ - { - guard: "isSessionExpired", - target: "#ROOT.SESSION_EXPIRED" - }, - { - target: "DISPLAYING", - actions: ["setFailure", "showFailureToast"] - } - ] - } - } - } - }, - - /** - * Final instrument section status. It triggers the parent `onDone` - */ - INSTRUMENTS_COMPLETED: { - type: "final" - } - }, - onDone: [ - { - /** - * If we are configuring instruments, the next state is the final state - */ - guard: "isInstrumentsOnlyMode", - target: "CONFIGURATION_COMPLETED" - }, - { - /** - * If we are configuring the entire initiative, the next state is where we display the success message to the user - */ - target: "DISPLAYING_CONFIGURATION_SUCCESS" - } - ] - }, - - /** - * State where we are displaying the success message to the user. - * On entry we navigate to the success screen with the `navigateToConfigurationSuccessScreen` action - */ - DISPLAYING_CONFIGURATION_SUCCESS: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToConfigurationSuccessScreen", - on: { - /** - * Transition to the final state - */ - COMPLETE_CONFIGURATION: { - target: "CONFIGURATION_COMPLETED" - }, - - /** - * Navigation outside the configuration flow to the instrument form - */ - ADD_PAYMENT_METHOD: { - actions: "navigateToAddPaymentMethodScreen" - } - } - }, - - /** - * If the configuration is already complete, the machine transit to this state which navigates to the - * configuration success screen. - */ - CONFIGURATION_NOT_NEEDED: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToConfigurationSuccessScreen", - on: { - /** - * Transition to the final state - */ - COMPLETE_CONFIGURATION: { - target: "CONFIGURATION_COMPLETED" - } - } - }, - - /** - * Final state, it navigates back to the initiative details screen - */ - CONFIGURATION_COMPLETED: { - type: "final", - entry: "navigateToInitiativeDetailScreen" - }, - - /** - * Final state, configuration closed by the user. It closes the configuration flow - */ - CONFIGURATION_CLOSED: { - type: "final", - entry: "exitConfiguration" - }, - - /** - * Final state, configuration failure. It shows a failure to the user and exits the configuration - */ - CONFIGURATION_FAILURE: { - type: "final", - entry: ["showFailureToast", "exitConfiguration"] - }, - - SESSION_EXPIRED: { - type: "final", - entry: ["handleSessionExpired", "exitConfiguration"] - } - } - }, - { - actions: { - startConfiguration: assign((_, event) => ({})), - loadInitiativeSuccess: assign((_, event) => ({})), - loadIbanListSuccess: assign((_, event) => ({})), - selectIban: assign((_, event) => ({})), - enrollIbanSuccess: assign(context => ({})), - confirmIbanOnboarding: assign((_, event) => ({})), - loadWalletInstrumentsSuccess: assign((_, event) => ({})), - loadInitiativeInstrumentsSuccess: assign((_, event) => ({})), - updateInstrumentStatuses: assign((context, _) => ({})), - forwardToInstrumentsEnrollmentService: forwardTo( - "instrumentsEnrollmentService" - ), - updateInstrumentEnrollStatus: assign((context, event) => ({})), - updateInstrumentEnrollStatusSuccess: assign((context, event) => ({})), - updateInstrumentEnrollStatusFailure: assign((context, event) => ({})), - updateInstrumentDeleteStatus: assign((context, event) => ({})), - updateInstrumentDeleteStatusSuccess: assign((context, event) => ({})), - updateInstrumentDeleteStatusFailure: assign((context, event) => ({})), - skipInstruments: assign((_, __) => ({ - areInstrumentsSkipped: true - })), - setFailure: assign((_, event) => ({})) - }, - guards: { - isSessionExpired: (_, event) => false, - isInitiativeConfigurationNeeded: (_, event) => false, - isIbanOnlyMode: (_, event) => false, - hasIbanList: (_, event) => false, - isInstrumentsOnlyMode: (_, event) => false, - hasInstruments: (_, event) => false - }, - delays: { - /** - * Instruments statuses refresh delay (ms) - */ - INSTRUMENTS_POLLING_INTERVAL: 3000 - } - } - ); diff --git a/ts/features/idpay/configuration/xstate/selectors.ts b/ts/features/idpay/configuration/xstate/selectors.ts deleted file mode 100644 index 49a852033ed..00000000000 --- a/ts/features/idpay/configuration/xstate/selectors.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as P from "@pagopa/ts-commons/lib/pot"; -import _ from "lodash"; -import { createSelector } from "reselect"; -import { StateFrom } from "xstate"; -import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; -import { LOADING_TAG } from "../../../../xstate/utils"; -import { ConfigurationMode } from "./context"; -import { IDPayInitiativeConfigurationMachineType } from "./machine"; - -type StateWithContext = StateFrom; - -type IDPayInstrumentsByIdWallet = { - [idWallet: string]: InstrumentDTO; -}; - -const isLoadingSelector = (state: StateWithContext) => - state.hasTag(LOADING_TAG as never); - -const selectInitiativeDetails = (state: StateWithContext) => - P.getOrElse(state.context.initiative, undefined); - -const selectIsInstrumentsOnlyMode = (state: StateWithContext) => - state.context.mode === ConfigurationMode.INSTRUMENTS; - -const selectIsIbanOnlyMode = (state: StateWithContext) => - state.context.mode === ConfigurationMode.IBAN; - -const isLoadingIbanListSelector = (state: StateWithContext) => - state.matches("CONFIGURING_IBAN.LOADING_IBAN_LIST"); - -const ibanListSelector = (state: StateWithContext) => - P.getOrElse(state.context.ibanList, []); - -const isUpsertingIbanSelector = (state: StateWithContext) => - state.matches("CONFIGURING_IBAN.ENROLLING_IBAN"); - -const selectIsLoadingInstruments = (state: StateWithContext) => - state.matches("CONFIGURING_INSTRUMENTS.LOADING_INSTRUMENTS"); - -const selectAreInstrumentsSkipped = (state: StateWithContext) => - state.context.areInstrumentsSkipped ?? false; - -const selectEnrolledIban = createSelector( - selectInitiativeDetails, - ibanListSelector, - (initiative, ibanList) => { - if (initiative?.iban === undefined) { - return undefined; - } - return ibanList.find(_ => _.iban === initiative.iban); - } -); - -const selectWalletInstruments = (state: StateWithContext) => - state.context.walletInstruments; - -const selectInitiativeInstruments = (state: StateWithContext) => - state.context.initiativeInstruments; - -const initiativeInstrumentsByIdWalletSelector = createSelector( - selectInitiativeInstruments, - instruments => - instruments.reduce((acc, instrument) => { - if (instrument.idWallet !== undefined) { - // eslint-disable-next-line functional/immutable-data - acc[instrument.idWallet] = instrument; - } - return acc; - }, {}) -); - -const selectInstrumentStatuses = (state: StateWithContext) => - state.context.instrumentStatuses; - -const isUpsertingInstrumentSelector = createSelector( - selectInstrumentStatuses, - statuses => Object.values(statuses).some(P.isLoading) -); - -const instrumentStatusByIdWalletSelector = (idWallet: number) => - createSelector( - selectInstrumentStatuses, - statuses => statuses[idWallet] ?? P.some(undefined) - ); - -const failureSelector = (state: StateWithContext) => state.context.failure; - -export { - isLoadingSelector, - isLoadingIbanListSelector, - ibanListSelector, - isUpsertingIbanSelector, - selectInitiativeDetails, - selectIsIbanOnlyMode, - selectIsInstrumentsOnlyMode, - selectIsLoadingInstruments, - selectAreInstrumentsSkipped, - selectEnrolledIban, - selectWalletInstruments, - initiativeInstrumentsByIdWalletSelector, - isUpsertingInstrumentSelector, - instrumentStatusByIdWalletSelector, - failureSelector -}; diff --git a/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx b/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx index edf7fe781ff..c9dfd1dc360 100644 --- a/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx @@ -1,18 +1,16 @@ +import { ListItemNav, VSpacer } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/core"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { ListItemNav, VSpacer } from "@pagopa/io-app-design-system"; import React from "react"; import { View } from "react-native"; +import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; import { H3 } from "../../../../components/core/typography/H3"; import I18n from "../../../../i18n"; import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { Skeleton } from "../../common/components/Skeleton"; -import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; -import { - IDPayConfigurationParamsList, - IDPayConfigurationRoutes -} from "../../configuration/navigation/navigator"; +import { IdPayConfigurationParamsList } from "../../configuration/navigation/params"; +import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; type Props = { initiative: InitiativeDTO; @@ -22,16 +20,21 @@ const InitiativeDiscountSettingsComponent = (props: Props) => { const { initiative } = props; const navigation = - useNavigation>(); + useNavigation>(); const navigateToInstrumentsConfiguration = (initiative: InitiativeDTO) => { - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS, - params: { - initiativeId: initiative.initiativeId, - initiativeName: initiative.initiativeName + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS, + params: { + initiativeId: initiative.initiativeId, + initiativeName: initiative.initiativeName + }, + initiativeId: initiative.initiativeId } - }); + ); }; const instrumentsSettingsButton = pipe( diff --git a/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx b/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx index f782fad10d7..c58b9852572 100644 --- a/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx @@ -19,7 +19,8 @@ import { IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; import { Skeleton } from "../../common/components/Skeleton"; -import { IDPayConfigurationRoutes } from "../../configuration/navigation/navigator"; +import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; +import { ConfigurationMode } from "../../configuration/types"; type Props = { initiative?: InitiativeDTO; @@ -31,22 +32,29 @@ const InitiativeRefundSettingsComponent = (props: Props) => { const navigation = useNavigation>(); const navigateToInstrumentsConfiguration = (initiativeId: string) => { - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, - params: { - initiativeId + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + params: { + initiativeId, + mode: ConfigurationMode.INSTRUMENTS + } } - }); + ); }; const navigateToIbanConfiguration = (initiativeId: string) => { - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, - params: { - initiativeId + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + params: { + initiativeId, + mode: ConfigurationMode.IBAN + } } - }); + ); }; const instrumentsSettingsButton = pipe( diff --git a/ts/features/idpay/details/components/MissingConfigurationAlert.tsx b/ts/features/idpay/details/components/MissingConfigurationAlert.tsx index 5d3ac8969cd..effd733092c 100644 --- a/ts/features/idpay/details/components/MissingConfigurationAlert.tsx +++ b/ts/features/idpay/details/components/MissingConfigurationAlert.tsx @@ -4,11 +4,9 @@ import { Alert, VSpacer } from "@pagopa/io-app-design-system"; import { NavigatorScreenParams } from "@react-navigation/native"; import { StatusEnum as InitiativeStatusEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import I18n from "../../../../i18n"; -import { - IDPayConfigurationParamsList, - IDPayConfigurationRoutes -} from "../../configuration/navigation/navigator"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { IdPayConfigurationParamsList } from "../../configuration/navigation/params"; +import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; type StatusWithAlert = Exclude< InitiativeStatusEnum, @@ -37,21 +35,24 @@ const MissingConfigurationAlert = (props: Props) => { const viewRef = React.createRef(); - const screen: Record = { + const screen: Record = { NOT_REFUNDABLE_ONLY_IBAN: - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, NOT_REFUNDABLE_ONLY_INSTRUMENT: - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, - NOT_REFUNDABLE: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, + NOT_REFUNDABLE: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO }; const handleNavigation = () => { - navigation.navigate(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: screen[status] as keyof IDPayConfigurationParamsList, - params: { - initiativeId - } - } as NavigatorScreenParams); + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: screen[status] as keyof IdPayConfigurationParamsList, + params: { + initiativeId + } + } as NavigatorScreenParams + ); }; return ( diff --git a/ts/features/idpay/details/hooks/useIdPayDiscountDetailsBottomSheet.tsx b/ts/features/idpay/details/hooks/useIdPayDiscountDetailsBottomSheet.tsx index 1fda04f423d..7693a052655 100644 --- a/ts/features/idpay/details/hooks/useIdPayDiscountDetailsBottomSheet.tsx +++ b/ts/features/idpay/details/hooks/useIdPayDiscountDetailsBottomSheet.tsx @@ -7,7 +7,7 @@ import { } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { idPayGenerateBarcode } from "../../barcode/store/actions"; -import { IDPayPaymentRoutes } from "../../payment/navigation/navigator"; +import { IdPayPaymentRoutes } from "../../payment/navigation/routes"; import I18n from "../../../../i18n"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; import { idPayBarcodeSecondsTillExpireSelector } from "../../barcode/store"; @@ -21,7 +21,7 @@ export const useIdPayDiscountDetailsBottomSheet = (initiativeId: string) => { const dispatch = useIODispatch(); const navigateToPaymentAuthorization = () => { - navigation.navigate(IDPayPaymentRoutes.IDPAY_PAYMENT_CODE_SCAN); + navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_CODE_SCAN); }; const barcodePressHandler = () => { diff --git a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx index 187b322a757..f25aaad4228 100644 --- a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx @@ -32,7 +32,7 @@ import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { formatNumberAmount } from "../../../../utils/stringBuilder"; import { IdPayCodeCieBanner } from "../../code/components/IdPayCodeCieBanner"; -import { IDPayConfigurationRoutes } from "../../configuration/navigation/navigator"; +import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; import { IdPayInitiativeLastUpdateCounter } from "../components/IdPayInitiativeLastUpdateCounter"; import { InitiativeDiscountSettingsComponent } from "../components/InitiativeDiscountSettingsComponent"; import { InitiativeRefundSettingsComponent } from "../components/InitiativeRefundSettingsComponent"; @@ -49,6 +49,7 @@ import { } from "../store"; import { idpayInitiativeGet, idpayTimelinePageGet } from "../store/actions"; import { BonusStatus } from "../../../../components/BonusCard/type"; +import { ConfigurationMode } from "../../configuration/types"; export type IdPayInitiativeDetailsScreenParams = { initiativeId: string; @@ -82,9 +83,9 @@ const IdPayInitiativeDetailsScreen = () => { }; const navigateToConfiguration = () => { - navigation.push(IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, - params: { initiativeId } + navigation.push(IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + params: { initiativeId, mode: ConfigurationMode.COMPLETE } }); }; const discountBottomSheet = useIdPayDiscountDetailsBottomSheet(initiativeId); diff --git a/ts/features/idpay/onboarding/machine/actions.ts b/ts/features/idpay/onboarding/machine/actions.ts index 82f24f218ae..14c17634d9e 100644 --- a/ts/features/idpay/onboarding/machine/actions.ts +++ b/ts/features/idpay/onboarding/machine/actions.ts @@ -1,15 +1,12 @@ import * as O from "fp-ts/lib/Option"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { useIODispatch } from "../../../../store/hooks"; import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayDetailsRoutes } from "../../details/navigation"; import { IdPayOnboardingRoutes } from "../navigation/routes"; import * as Context from "./context"; const createActionsImplementation = ( - navigation: ReturnType, - dispatch: ReturnType + navigation: ReturnType ) => { const navigateToInitiativeDetailsScreen = guardedNavigationAction(() => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { @@ -67,16 +64,6 @@ const createActionsImplementation = ( }); }; - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - const closeOnboarding = () => { navigation.popToTop(); }; @@ -89,7 +76,6 @@ const createActionsImplementation = ( navigateToCompletionScreen, navigateToFailureScreen, navigateToInitiativeMonitoringScreen, - handleSessionExpired, closeOnboarding }; }; diff --git a/ts/features/idpay/onboarding/machine/actors.ts b/ts/features/idpay/onboarding/machine/actors.ts index 5b821d49842..b7dd2a32f7d 100644 --- a/ts/features/idpay/onboarding/machine/actors.ts +++ b/ts/features/idpay/onboarding/machine/actors.ts @@ -9,6 +9,8 @@ import { CodeEnum as OnboardingErrorCodeEnum } from "../../../../../definitions/ import { StatusEnum as OnboardingStatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; import { SelfConsentDTO } from "../../../../../definitions/idpay/SelfConsentDTO"; +import { useIODispatch } from "../../../../store/hooks"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayClient } from "../../common/api/client"; import { OnboardingFailure, @@ -73,13 +75,24 @@ const mapErrorCodeToFailure = ( const createActorsImplementation = ( client: IDPayClient, token: string, - language: PreferredLanguage + language: PreferredLanguage, + dispatch: ReturnType ) => { const clientOptions = { bearerAuth: token, "Accept-Language": language }; + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + const getInitiativeInfo = fromPromise( async params => { const dataResponse = await client.getInitiativeData({ @@ -96,6 +109,7 @@ const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: + handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -141,6 +155,7 @@ const createActorsImplementation = ( // Initiative not yet started by the citizen return Promise.resolve(O.none); case 401: + handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -175,6 +190,7 @@ const createActorsImplementation = ( case 403: return Promise.reject(mapErrorCodeToFailure(value.code)); case 401: + handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -214,6 +230,7 @@ const createActorsImplementation = ( case 403: return Promise.reject(mapErrorCodeToFailure(value.code)); case 401: + handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -265,6 +282,7 @@ const createActorsImplementation = ( case 202: return Promise.resolve(undefined); case 401: + handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); diff --git a/ts/features/idpay/onboarding/machine/events.ts b/ts/features/idpay/onboarding/machine/events.ts index 9d51bcb36dd..e67d436b205 100644 --- a/ts/features/idpay/onboarding/machine/events.ts +++ b/ts/features/idpay/onboarding/machine/events.ts @@ -18,4 +18,8 @@ export interface SelectMultiConsent { readonly data: SelfConsentMultiDTO; } -export type Events = GlobalEvents | SelectMultiConsent | ToggleBoolCriteria; +export type Events = + | GlobalEvents + | AutoInit + | SelectMultiConsent + | ToggleBoolCriteria; diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index 2825a13c985..94e77093bb8 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -9,6 +9,7 @@ import { WAITING_USER_INPUT_TAG, notImplementedStub } from "../../../../xstate/utils"; +import { OnboardingFailure } from "../types/OnboardingFailure"; import * as Context from "./context"; import * as Events from "./events"; import * as Input from "./input"; @@ -31,7 +32,6 @@ export const idPayOnboardingMachine = setup({ navigateToCompletionScreen: notImplementedStub, navigateToFailureScreen: notImplementedStub, navigateToInitiativeMonitoringScreen: notImplementedStub, - handleSessionExpired: notImplementedStub, closeOnboarding: notImplementedStub }, actors: { @@ -86,6 +86,9 @@ export const idPayOnboardingMachine = setup({ return event.input; }, onError: { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), target: ".OnboardingFailure" }, onDone: { @@ -130,15 +133,12 @@ export const idPayOnboardingMachine = setup({ } } }, - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "OnboardingFailure" - } - ], + onError: { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + }, onDone: { target: "DisplayingInitiativeInfo" } @@ -158,15 +158,12 @@ export const idPayOnboardingMachine = setup({ invoke: { src: "acceptTos", input: ({ context }) => selectInitiativeId(context), - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "OnboardingFailure" - } - ], + onError: { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + }, onDone: { target: "LoadingCriteria" } @@ -178,15 +175,12 @@ export const idPayOnboardingMachine = setup({ invoke: { src: "getRequiredCriteria", input: ({ context }) => selectInitiativeId(context), - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "OnboardingFailure" - } - ], + onError: { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + }, onDone: { actions: assign(({ event }) => ({ requiredCriteria: event.output @@ -324,15 +318,12 @@ export const idPayOnboardingMachine = setup({ invoke: { src: "acceptRequiredCriteria", input: ({ context }) => context, - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "OnboardingFailure" - } - ], + onError: { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + }, onDone: { target: "OnboardingCompleted" } @@ -346,6 +337,10 @@ export const idPayOnboardingMachine = setup({ OnboardingFailure: { entry: "navigateToFailureScreen", + always: { + guard: "isSessionExpired", + target: "SessionExpired" + }, on: { next: { actions: "navigateToInitiativeMonitoringScreen" @@ -354,7 +349,7 @@ export const idPayOnboardingMachine = setup({ }, SessionExpired: { - entry: ["handleSessionExpired", "closeOnboarding"] + entry: "closeOnboarding" } } }); diff --git a/ts/features/idpay/onboarding/machine/provider.tsx b/ts/features/idpay/onboarding/machine/provider.tsx index d4e03255b9a..1956d155f41 100644 --- a/ts/features/idpay/onboarding/machine/provider.tsx +++ b/ts/features/idpay/onboarding/machine/provider.tsx @@ -57,8 +57,8 @@ export const IdPayOnboardingMachineProvider = ({ children, input }: Props) => { isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); - const actors = createActorsImplementation(client, token, language); - const actions = createActionsImplementation(rootNavigation, dispatch); + const actors = createActorsImplementation(client, token, language, dispatch); + const actions = createActionsImplementation(rootNavigation); const machine = idPayOnboardingMachine.provide({ actors, diff --git a/ts/features/idpay/onboarding/machine/selectors.ts b/ts/features/idpay/onboarding/machine/selectors.ts index 3da8d688a27..efdedfb8ec6 100644 --- a/ts/features/idpay/onboarding/machine/selectors.ts +++ b/ts/features/idpay/onboarding/machine/selectors.ts @@ -2,39 +2,36 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; -import { StateFrom } from "xstate"; +import { SnapshotFrom } from "xstate"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; import { SelfDeclarationDTO } from "../../../../../definitions/idpay/SelfDeclarationDTO"; import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDeclarationMultiDTO"; -import { LOADING_TAG } from "../../../../xstate/utils"; import * as Context from "./context"; -import { IdPayOnboardingMachine } from "./machine"; +import { idPayOnboardingMachine } from "./machine"; -type StateWithContext = StateFrom; +type MachineSnapshot = SnapshotFrom; -export const selectOnboardingFailure = (state: StateWithContext) => - state.context.failure; +export const selectOnboardingFailure = (snapshot: MachineSnapshot) => + snapshot.context.failure; -const selectRequiredCriteria = (state: StateWithContext) => - state.context.requiredCriteria; +const selectRequiredCriteria = (snapshot: MachineSnapshot) => + snapshot.context.requiredCriteria; -export const selectSelfDeclarationBoolAnswers = (state: StateWithContext) => - state.context.selfDeclarationsBoolAnswers; +export const selectSelfDeclarationBoolAnswers = (snapshot: MachineSnapshot) => + snapshot.context.selfDeclarationsBoolAnswers; -const selectMultiConsents = (state: StateWithContext) => - state.context.selfDeclarationsMultiAnwsers; +const selectMultiConsents = (snapshot: MachineSnapshot) => + snapshot.context.selfDeclarationsMultiAnwsers; -const selectCurrentPage = (state: StateWithContext) => - state.context.selfDeclarationsMultiPage; +const selectCurrentPage = (snapshot: MachineSnapshot) => + snapshot.context.selfDeclarationsMultiPage; -const selectTags = (state: StateWithContext) => state.tags; +export const selectInitiative = (snapshot: MachineSnapshot) => + snapshot.context.initiative; -export const selectInitiative = (state: StateWithContext) => - state.context.initiative; - -export const selectServiceId = (state: StateWithContext) => - state.context.serviceId; +export const selectServiceId = (snapshot: MachineSnapshot) => + snapshot.context.serviceId; const filterCriteria = ( criteria: O.Option, @@ -94,10 +91,6 @@ export const prerequisiteAnswerIndexSelector = createSelector( : currentCriteria.value.indexOf(multiConsents[currentPage]?.value) ); -export const isLoadingSelector = createSelector(selectTags, tags => - tags.has(LOADING_TAG) -); - export const getMultiSelfDeclarationListFromContext = ( context: Context.Context ) => diff --git a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx index 7078d5dc9b8..ca2b7c57520 100644 --- a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx @@ -20,9 +20,9 @@ import { IdPayOnboardingMachineContext } from "../machine/provider"; import { areAllSelfDeclarationsToggledSelector, boolRequiredCriteriaSelector, - isLoadingSelector, selectSelfDeclarationBoolAnswers } from "../machine/selectors"; +import { isLoadingSelector } from "../../../../xstate/selectors"; const InitiativeSelfDeclarationsScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; diff --git a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx index e4406111a31..7d5ca12be9d 100644 --- a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx +++ b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx @@ -10,7 +10,7 @@ import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; import I18n from "../../../../i18n"; import themeVariables from "../../../../theme/variables"; import { IdPayOnboardingMachineContext } from "../machine/provider"; -import { isLoadingSelector } from "../machine/selectors"; +import { isLoadingSelector } from "../../../../xstate/selectors"; const CompletionScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 537aea7e92f..73a7af467a8 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -17,7 +17,8 @@ import { import { OnboardingPrivacyAdvice } from "../components/OnboardingPrivacyAdvice"; import { OnboardingServiceHeader } from "../components/OnboardingServiceHeader"; import { IdPayOnboardingMachineContext } from "../machine/provider"; -import { isLoadingSelector, selectInitiative } from "../machine/selectors"; +import { selectInitiative } from "../machine/selectors"; +import { isLoadingSelector } from "../../../../xstate/selectors"; export const InitiativeDetailsScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; diff --git a/ts/features/idpay/payment/machine/actions.ts b/ts/features/idpay/payment/machine/actions.ts index 544ad0856a2..9d61ca9b478 100644 --- a/ts/features/idpay/payment/machine/actions.ts +++ b/ts/features/idpay/payment/machine/actions.ts @@ -2,27 +2,14 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { useIODispatch } from "../../../../store/hooks"; import { showToast } from "../../../../utils/showToast"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayDetailsRoutes } from "../../details/navigation"; import { IdPayPaymentRoutes } from "../navigation/routes"; import { Context } from "./context"; const createActionsImplementation = ( - navigation: ReturnType, - dispatch: ReturnType + navigation: ReturnType ) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - const navigateToAuthorizationScreen = () => { navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { screen: IdPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION, @@ -53,7 +40,6 @@ const createActionsImplementation = ( }; return { - handleSessionExpired, navigateToAuthorizationScreen, navigateToResultScreen, showErrorToast, diff --git a/ts/features/idpay/payment/machine/actors.ts b/ts/features/idpay/payment/machine/actors.ts index 0d78910710e..8fe33b899ee 100644 --- a/ts/features/idpay/payment/machine/actors.ts +++ b/ts/features/idpay/payment/machine/actors.ts @@ -6,11 +6,24 @@ import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPay import { CodeEnum as TransactionErrorCodeEnum } from "../../../../../definitions/idpay/TransactionErrorDTO"; import { IDPayClient } from "../../common/api/client"; import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; +import { useIODispatch } from "../../../../store/hooks"; export const createActorsImplementation = ( client: IDPayClient, - token: string + token: string, + dispatch: ReturnType ) => { + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + const preAuthorizePayment = fromPromise( async ({ input }) => { const putPreAuthPaymentTask = (trxCode: string) => @@ -35,6 +48,7 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: + handleSessionExpired(); return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); default: return Promise.reject(mapErrorCodeToFailure(value.code)); @@ -72,6 +86,7 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: + handleSessionExpired(); return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); default: return Promise.reject(mapErrorCodeToFailure(value.code)); @@ -108,6 +123,7 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: + handleSessionExpired(); return Promise.reject(PaymentFailureEnum.SESSION_EXPIRED); default: return Promise.reject(mapErrorCodeToFailure(value.code)); diff --git a/ts/features/idpay/payment/machine/machine.ts b/ts/features/idpay/payment/machine/machine.ts index 7c42d98f797..3e640d898a0 100644 --- a/ts/features/idpay/payment/machine/machine.ts +++ b/ts/features/idpay/payment/machine/machine.ts @@ -1,4 +1,5 @@ import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import { assertEvent, assign, fromPromise, setup } from "xstate"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { @@ -7,6 +8,7 @@ import { WAITING_USER_INPUT_TAG, notImplementedStub } from "../../../../xstate/utils"; +import { PaymentFailure } from "../types/PaymentFailure"; import * as Context from "./context"; import * as Events from "./events"; import * as Input from "./input"; @@ -32,7 +34,6 @@ export const idPayPaymentMachine = setup({ actions: { navigateToAuthorizationScreen: notImplementedStub, navigateToResultScreen: notImplementedStub, - handleSessionExpired: notImplementedStub, closeAuthorization: notImplementedStub, setFailure: notImplementedStub, showErrorToast: notImplementedStub @@ -70,15 +71,12 @@ export const idPayPaymentMachine = setup({ })), target: "AwaitingConfirmation" }, - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "AuthorizationFailure" - } - ] + onError: { + actions: assign(({ event }) => ({ + failure: pipe(PaymentFailure.decode(event.error), O.fromEither) + })), + target: "AuthorizationFailure" + } } }, @@ -105,18 +103,20 @@ export const idPayPaymentMachine = setup({ }, onError: [ { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - actions: "setFailure", - guard: "isBlockingFailure", - target: "AuthorizationFailure" + actions: assign(({ event }) => ({ + failure: pipe(PaymentFailure.decode(event.error), O.fromEither) + })) }, - { - actions: ["setFailure", "showErrorToast"], - target: "AwaitingConfirmation" - } + [ + { + guard: "isBlockingFailure", + target: "AuthorizationFailure" + }, + { + actions: "showErrorToast", + target: "AwaitingConfirmation" + } + ] ] } }, @@ -132,18 +132,20 @@ export const idPayPaymentMachine = setup({ }, onError: [ { - guard: "isSessionExpired", - target: "SessionExpired" + actions: assign(({ event }) => ({ + failure: pipe(PaymentFailure.decode(event.error), O.fromEither) + })) }, - { - actions: "setFailure", - guard: "isBlockingFailure", - target: "AuthorizationFailure" - }, - { - actions: ["setFailure", "showErrorToast"], - target: "AwaitingConfirmation" - } + [ + { + guard: "isBlockingFailure", + target: "AuthorizationFailure" + }, + { + actions: "showErrorToast", + target: "AwaitingConfirmation" + } + ] ] } }, @@ -168,6 +170,10 @@ export const idPayPaymentMachine = setup({ AuthorizationFailure: { entry: "navigateToResultScreen", + always: { + guard: "isSessionExpired", + target: "SessionExpired" + }, on: { close: { actions: "closeAuthorization" @@ -176,7 +182,7 @@ export const idPayPaymentMachine = setup({ }, SessionExpired: { - entry: ["handleSessionExpired", "closeAuthorization"] + entry: "closeAuthorization" } } }); diff --git a/ts/features/idpay/payment/machine/provider.tsx b/ts/features/idpay/payment/machine/provider.tsx index 455d60a87dd..4a8328339ff 100644 --- a/ts/features/idpay/payment/machine/provider.tsx +++ b/ts/features/idpay/payment/machine/provider.tsx @@ -41,9 +41,10 @@ export const IdPayPaymentMachineProvider = (props: Props) => { const actors = createActorsImplementation( IDPayPaymentClient, - idPayTestToken ?? bpdToken + idPayTestToken ?? bpdToken, + dispatch ); - const actions = createActionsImplementation(navigation, dispatch); + const actions = createActionsImplementation(navigation); const machine = idPayPaymentMachine.provide({ actors, diff --git a/ts/features/idpay/payment/navigation/navigator.tsx b/ts/features/idpay/payment/navigation/navigator.tsx index c9cd48badb9..dcf2a1f82b9 100644 --- a/ts/features/idpay/payment/navigation/navigator.tsx +++ b/ts/features/idpay/payment/navigation/navigator.tsx @@ -9,7 +9,7 @@ import { IdPayPaymentRoutes } from "./routes"; const Stack = createStackNavigator(); -export const IDPayPaymentNavigator = () => ( +export const IdPayPaymentNavigator = () => ( { - const originalModule = jest.requireActual("../../xstate/selectors"); - return { - ...originalModule, - selectFailureOption: jest.fn(), - selectIsFailure: jest.fn(), - selectIsCancelled: jest.fn() - }; -}); - -const mockedSelectFailureOption = selectFailureOption as jest.MockedFunction< - typeof selectFailureOption ->; - -const mockedSelectIsFailure = selectIsFailure as jest.MockedFunction< - typeof selectIsFailure ->; - -const mockedSelectIsCancelled = selectIsCancelled as jest.MockedFunction< - typeof selectIsCancelled ->; - -const mockedExitAuthorization = jest.fn(); - -describe("Test IDPayPaymentResultScreen", () => { - beforeEach(() => { - jest.useFakeTimers(); - jest.clearAllMocks(); - }); - - it("should render the success screen", () => { - mockedSelectFailureOption.mockImplementation(() => O.none); - mockedSelectIsFailure.mockImplementation(() => false); - mockedSelectIsCancelled.mockImplementation(() => false); - - const { component } = renderComponent(); - expect(component).toBeTruthy(); - - component.getByTestId("paymentSuccessScreenTestID"); - }); - - it("should render the failure screen", () => { - mockedSelectFailureOption.mockImplementation(() => - O.some(PaymentFailureEnum.GENERIC) - ); - mockedSelectIsFailure.mockImplementation(() => true); - mockedSelectIsCancelled.mockImplementation(() => false); - - const { component } = renderComponent(); - expect(component).toBeTruthy(); - - component.getByTestId("paymentFailureScreenTestID"); - }); - - it("should render the cancelled screen", () => { - mockedSelectFailureOption.mockImplementation(() => O.none); - mockedSelectIsFailure.mockImplementation(() => false); - mockedSelectIsCancelled.mockImplementation(() => true); - - const { component } = renderComponent(); - expect(component).toBeTruthy(); - - component.getByTestId("paymentCancelledScreenTestID"); - }); -}); - -const renderComponent = (context?: Partial) => { - const globalState = appReducer(undefined, applicationChangeState("active")); - - const mockStore = configureMockStore(); - const store: ReturnType = mockStore({ - ...globalState - } as GlobalState); - - const mockMachine = createIDPayPaymentMachine() - .withConfig({ - services: { - preAuthorizePayment: jest.fn(), - authorizePayment: jest.fn(), - deletePayment: jest.fn() - }, - actions: { - exitAuthorization: mockedExitAuthorization, - navigateToAuthorizationScreen: jest.fn(), - navigateToResultScreen: jest.fn(), - showErrorToast: jest.fn(), - handleSessionExpired: jest.fn() - } - }) - .withContext({ - ...INITIAL_CONTEXT, - ...context - }); - - const mockService = interpret(mockMachine); - - return { - component: renderScreenWithNavigationStoreContext( - () => ( - - - - ), - IDPayPaymentRoutes.IDPAY_PAYMENT_RESULT, - {}, - store - ), - store - }; -}; diff --git a/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx b/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx index 5cbbf98d41f..af27ff66d1a 100644 --- a/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx +++ b/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx @@ -17,7 +17,7 @@ import NavigationService from "../../../../navigation/NavigationService"; import { useIOSelector } from "../../../../store/hooks"; import themeVariables from "../../../../theme/variables"; import { formatNumberAmount } from "../../../../utils/stringBuilder"; -import { IDPayConfigurationRoutes } from "../../configuration/navigation/navigator"; +import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; import { idpayInitiativeIdSelector } from "../../details/store"; import { getRefundPeriodDateString } from "../utils/strings"; @@ -39,9 +39,9 @@ const TimelineRefundDetailsComponent = (props: Props) => { // would result in an error. NavigationService.dispatchNavigationAction( CommonActions.navigate( - IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN, + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { - screen: IDPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, params: { initiativeId } diff --git a/ts/features/idpay/unsubscription/machine/actions.ts b/ts/features/idpay/unsubscription/machine/actions.ts index 2f2474f03d1..f56ea679570 100644 --- a/ts/features/idpay/unsubscription/machine/actions.ts +++ b/ts/features/idpay/unsubscription/machine/actions.ts @@ -1,33 +1,26 @@ import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import ROUTES from "../../../../navigation/routes"; -import { useIODispatch } from "../../../../store/hooks"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; -import { IDPayUnsubscriptionRoutes } from "../navigation/navigator"; +import { IdPayUnsubscriptionRoutes } from "../navigation/routes"; const createActionsImplementation = ( - navigation: ReturnType, - dispatch: ReturnType + navigation: ReturnType ) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - const navigateToConfirmationScreen = () => { - navigation.navigate(IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN, { - screen: IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION - }); + navigation.navigate( + IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR, + { + screen: IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION + } + ); }; const navigateToResultScreen = () => - navigation.navigate(IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN, { - screen: IDPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT - }); + navigation.navigate( + IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR, + { + screen: IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT + } + ); const exitUnsubscription = () => { navigation.pop(); @@ -42,7 +35,6 @@ const createActionsImplementation = ( }; return { - handleSessionExpired, navigateToConfirmationScreen, navigateToResultScreen, exitUnsubscription, diff --git a/ts/features/idpay/unsubscription/machine/actors.ts b/ts/features/idpay/unsubscription/machine/actors.ts index 5511608a796..21279019d7c 100644 --- a/ts/features/idpay/unsubscription/machine/actors.ts +++ b/ts/features/idpay/unsubscription/machine/actors.ts @@ -6,12 +6,25 @@ import { PreferredLanguage } from "../../../../../definitions/backend/PreferredL import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; import { IDPayClient } from "../../common/api/client"; import { UnsubscriptionFailureEnum } from "../types/failure"; +import { useIODispatch } from "../../../../store/hooks"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; export const createActorsImplementation = ( client: IDPayClient, token: string, - language: PreferredLanguage + language: PreferredLanguage, + dispatch: ReturnType ) => { + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + const getInitiativeInfo = fromPromise( async (params: { input: string }): Promise => { const dataResponse = await TE.tryCatch( @@ -34,6 +47,7 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: + handleSessionExpired(); return Promise.reject( UnsubscriptionFailureEnum.SESSION_EXPIRED ); @@ -70,6 +84,7 @@ export const createActorsImplementation = ( case 204: return Promise.resolve(undefined); case 401: + handleSessionExpired(); return Promise.reject( UnsubscriptionFailureEnum.SESSION_EXPIRED ); diff --git a/ts/features/idpay/unsubscription/machine/events.ts b/ts/features/idpay/unsubscription/machine/events.ts index 1b219a5e5ec..65b0da37ffa 100644 --- a/ts/features/idpay/unsubscription/machine/events.ts +++ b/ts/features/idpay/unsubscription/machine/events.ts @@ -10,4 +10,4 @@ export interface ConfirmUnsubscription { readonly type: "confirm-unsubscription"; } -export type Events = GlobalEvents | ConfirmUnsubscription; +export type Events = GlobalEvents | AutoInit | ConfirmUnsubscription; diff --git a/ts/features/idpay/unsubscription/machine/machine.ts b/ts/features/idpay/unsubscription/machine/machine.ts index 4f5e5235a8d..e9890d30171 100644 --- a/ts/features/idpay/unsubscription/machine/machine.ts +++ b/ts/features/idpay/unsubscription/machine/machine.ts @@ -19,8 +19,7 @@ export const idPayUnsubscriptionMachine = setup({ navigateToConfirmationScreen: notImplementedStub, navigateToResultScreen: notImplementedStub, exitToWallet: notImplementedStub, - exitUnsubscription: notImplementedStub, - handleSessionExpired: notImplementedStub + exitUnsubscription: notImplementedStub }, actors: { onInit: fromPromise(({ input }) => @@ -74,15 +73,9 @@ export const idPayUnsubscriptionMachine = setup({ invoke: { input: ({ context }) => context.initiativeId, src: "getInitiativeInfo", - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "UnsubscriptionFailure" - } - ], + onError: { + target: "UnsubscriptionFailure" + }, onDone: { actions: assign(({ event }) => ({ initiativeId: event.output.initiativeId, @@ -109,15 +102,9 @@ export const idPayUnsubscriptionMachine = setup({ invoke: { input: ({ context }) => context.initiativeId, src: "unsubscribeFromInitiative", - onError: [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - target: "UnsubscriptionFailure" - } - ], + onError: { + target: "UnsubscriptionFailure" + }, onDone: { target: "UnsubscriptionSuccess" } @@ -133,6 +120,10 @@ export const idPayUnsubscriptionMachine = setup({ }, UnsubscriptionFailure: { entry: "navigateToResultScreen", + always: { + guard: "isSessionExpired", + target: "SessionExpired" + }, on: { close: { actions: "exitUnsubscription" @@ -140,7 +131,7 @@ export const idPayUnsubscriptionMachine = setup({ } }, SessionExpired: { - entry: ["handleSessionExpired", "exitUnsubscription"] + entry: "exitUnsubscription" } } }); diff --git a/ts/features/idpay/unsubscription/machine/provider.tsx b/ts/features/idpay/unsubscription/machine/provider.tsx index 375b74c9964..033c627dfbd 100644 --- a/ts/features/idpay/unsubscription/machine/provider.tsx +++ b/ts/features/idpay/unsubscription/machine/provider.tsx @@ -60,9 +60,10 @@ export const IdPayUnsubscriptionMachineProvider = ({ const actors = createActorsImplementation( idPayClient, idPayTestToken ?? bpdToken, - language + language, + dispatch ); - const actions = createActionsImplementation(navigation, dispatch); + const actions = createActionsImplementation(navigation); const machine = idPayUnsubscriptionMachine.provide({ actors, actions diff --git a/ts/features/idpay/unsubscription/navigation/navigator.tsx b/ts/features/idpay/unsubscription/navigation/navigator.tsx index 54d73f5a3f9..80c37befaa4 100644 --- a/ts/features/idpay/unsubscription/navigation/navigator.tsx +++ b/ts/features/idpay/unsubscription/navigation/navigator.tsx @@ -14,7 +14,7 @@ type IdPayUnsubscriptionScreenRouteProps = RouteProp< "IDPAY_UNSUBSCRIPTION_NAVIGATOR" >; -export const IDPayUnsubscriptionNavigator = () => { +export const IdPayUnsubscriptionNavigator = () => { const { params } = useRoute(); const { initiativeId, initiativeName, initiativeType } = params; diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index f3af526cb26..e8264f4c55c 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -22,27 +22,19 @@ import { IdPayBarcodeNavigator } from "../features/idpay/barcode/navigation/navi import { IdPayBarcodeRoutes } from "../features/idpay/barcode/navigation/routes"; import { IdPayCodeNavigator } from "../features/idpay/code/navigation/navigator"; import { IdPayCodeRoutes } from "../features/idpay/code/navigation/routes"; -import { - IDPayConfigurationNavigator, - IDPayConfigurationRoutes -} from "../features/idpay/configuration/navigation/navigator"; +import { IdPayConfigurationNavigator } from "../features/idpay/configuration/navigation/navigator"; +import { IdPayConfigurationRoutes } from "../features/idpay/configuration/navigation/routes"; import { IDpayDetailsNavigator, IDPayDetailsRoutes } from "../features/idpay/details/navigation"; -import { - IdPayOnboardingNavigator, - IDPayOnboardingRoutes -} from "../features/idpay/onboarding/navigation/navigator"; -import { - IDPayPaymentNavigator, - IDPayPaymentRoutes -} from "../features/idpay/payment/navigation/navigator"; +import { IdPayOnboardingNavigator } from "../features/idpay/onboarding/navigation/navigator"; +import { IdPayOnboardingRoutes } from "../features/idpay/onboarding/navigation/routes"; +import { IdPayPaymentNavigator } from "../features/idpay/payment/navigation/navigator"; +import { IdPayPaymentRoutes } from "../features/idpay/payment/navigation/routes"; import { IDPayPaymentCodeScanScreen } from "../features/idpay/payment/screens/IDPayPaymentCodeScanScreen"; -import { - IDPayUnsubscriptionNavigator, - IDPayUnsubscriptionRoutes -} from "../features/idpay/unsubscription/navigation/navigator"; +import { IdPayUnsubscriptionNavigator } from "../features/idpay/unsubscription/navigation/navigator"; +import { IdPayUnsubscriptionRoutes } from "../features/idpay/unsubscription/navigation/routes"; import UnsupportedDeviceScreen from "../features/lollipop/screens/UnsupportedDeviceScreen"; import { MessagesStackNavigator } from "../features/messages/navigation/MessagesNavigator"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; @@ -248,7 +240,7 @@ const AuthenticatedStackNavigator = () => { {isIdPayEnabled && ( <> @@ -258,13 +250,13 @@ const AuthenticatedStackNavigator = () => { options={{ gestureEnabled: isGestureEnabled, ...hideHeaderOptions }} /> {/* @@ -272,7 +264,7 @@ const AuthenticatedStackNavigator = () => { FIXME IOBP-383: Using react-navigation 6.x we can achive this using a Stack.Group inside the IDPayPaymentNavigator */} { }} /> ; - [IDPayConfigurationRoutes.IDPAY_CONFIGURATION_MAIN]: NavigatorScreenParams; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR]: + | IdPayConfigurationNavigatorParams + | NavigatorScreenParams; [IDPayDetailsRoutes.IDPAY_DETAILS_MAIN]: NavigatorScreenParams; [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR]: | IdPayUnsubscriptionNavigatorParams diff --git a/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx b/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx index 94d8c876137..6d593a5f949 100644 --- a/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx +++ b/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx @@ -15,7 +15,7 @@ import { LabelSmall } from "../../../components/core/typography/LabelSmall"; import { Monospace } from "../../../components/core/typography/Monospace"; import { IOStyles } from "../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import { IDPayOnboardingRoutes } from "../../../features/idpay/onboarding/navigation/navigator"; +import { IdPayOnboardingRoutes } from "../../../features/idpay/onboarding/navigation/routes"; import { AppParamsList, IOStackNavigationProp @@ -27,8 +27,8 @@ const IdPayOnboardingPlayground = () => { const [serviceId, setServiceId] = React.useState(); const navigateToIDPayOnboarding = (serviceId: string) => { - navigation.navigate(IDPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { - screen: IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, params: { serviceId } diff --git a/yarn.lock b/yarn.lock index 467e6e88fbc..f21a1889126 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16669,10 +16669,10 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== ua-parser-js@^0.7.18, ua-parser-js@^0.7.30: version "0.7.33" From f64a7b7447f330de51a8a65ca7114c653444b1c2 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Thu, 2 May 2024 10:17:49 +0200 Subject: [PATCH 08/31] chore: xstatre v5 wip --- ts/features/idpay/configuration/machine/machine.ts | 10 +++++----- ts/features/idpay/onboarding/machine/machine.ts | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ts/features/idpay/configuration/machine/machine.ts b/ts/features/idpay/configuration/machine/machine.ts index b0c9119a615..5006e73702d 100644 --- a/ts/features/idpay/configuration/machine/machine.ts +++ b/ts/features/idpay/configuration/machine/machine.ts @@ -172,11 +172,11 @@ export const idPayConfigurationMachine = setup({ return event.input; }, onError: { - target: "ConfigurationFailure" + target: ".ConfigurationFailure" }, onDone: { actions: assign(event => ({ ...event.event.output })), - target: "LoadingInitiative" + target: ".LoadingInitiative" } }, initial: "Idle", @@ -260,10 +260,10 @@ export const idPayConfigurationMachine = setup({ [ { guard: "hasIbanList", - target: "DisplayingIbanList" + target: ".DisplayingIbanList" }, { - target: "DisplayingIbanOnboarding" + target: ".DisplayingIbanOnboarding" } ] ], @@ -342,7 +342,7 @@ export const idPayConfigurationMachine = setup({ on: { back: [ { - target: "#idpay-configuration.DisplayingIbanOnboarding" + target: "DisplayingIbanOnboarding" } ], "confirm-iban-onboarding": { diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index 94e77093bb8..c435ca29579 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -227,6 +227,7 @@ export const idPayOnboardingMachine = setup({ DisplayingSelfDeclarationList: { tags: [WAITING_USER_INPUT_TAG], + initial: "Evaluating", states: { Evaluating: { tags: [LOADING_TAG], @@ -277,6 +278,7 @@ export const idPayOnboardingMachine = setup({ DisplayingMultiSelfDeclarationList: { tags: [WAITING_USER_INPUT_TAG], + initial: "DisplayingMultiSelfDeclarationItem", states: { DisplayingMultiSelfDeclarationItem: { entry: "navigateToMultiSelfDeclarationListScreen", From 93421f81ddeb21901fda37ac415027f5dfdbab37 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Thu, 2 May 2024 12:08:09 +0200 Subject: [PATCH 09/31] chore: remove unsubscription machine --- ts/features/idpay/common/saga/index.ts | 7 + .../idpay/common/store/actions/index.ts | 4 +- .../idpay/common/store/reducers/index.ts | 7 +- .../components/BeneficiaryDetailsContent.tsx | 25 +++- .../idpay/unsubscription/machine/actions.ts | 45 ------ .../idpay/unsubscription/machine/actors.ts | 106 -------------- .../idpay/unsubscription/machine/context.ts | 13 -- .../idpay/unsubscription/machine/events.ts | 13 -- .../idpay/unsubscription/machine/input.ts | 11 -- .../idpay/unsubscription/machine/machine.ts | 137 ------------------ .../idpay/unsubscription/machine/provider.tsx | 80 ---------- .../idpay/unsubscription/machine/selectors.ts | 59 -------- .../unsubscription/navigation/navigator.tsx | 50 +++---- .../idpay/unsubscription/navigation/params.ts | 11 +- .../idpay/unsubscription/navigation/routes.ts | 2 +- .../unsubscription/saga/handleUnsubscribe.ts | 59 ++++++++ .../idpay/unsubscription/saga/index.ts | 20 +++ .../UnsubscriptionConfirmationScreen.tsx | 93 +++++++++--- .../screens/UnsubscriptionResultScreen.tsx | 26 +++- .../unsubscription/store/actions/index.ts | 13 ++ .../unsubscription/store/reducers/index.ts | 28 ++++ .../unsubscription/store/selectors/index.ts | 11 ++ ts/navigation/AuthenticatedStackNavigator.tsx | 2 +- ts/navigation/params/AppParamsList.ts | 27 +--- 24 files changed, 290 insertions(+), 559 deletions(-) delete mode 100644 ts/features/idpay/unsubscription/machine/actions.ts delete mode 100644 ts/features/idpay/unsubscription/machine/actors.ts delete mode 100644 ts/features/idpay/unsubscription/machine/context.ts delete mode 100644 ts/features/idpay/unsubscription/machine/events.ts delete mode 100644 ts/features/idpay/unsubscription/machine/input.ts delete mode 100644 ts/features/idpay/unsubscription/machine/machine.ts delete mode 100644 ts/features/idpay/unsubscription/machine/provider.tsx delete mode 100644 ts/features/idpay/unsubscription/machine/selectors.ts create mode 100644 ts/features/idpay/unsubscription/saga/handleUnsubscribe.ts create mode 100644 ts/features/idpay/unsubscription/saga/index.ts create mode 100644 ts/features/idpay/unsubscription/store/actions/index.ts create mode 100644 ts/features/idpay/unsubscription/store/reducers/index.ts create mode 100644 ts/features/idpay/unsubscription/store/selectors/index.ts diff --git a/ts/features/idpay/common/saga/index.ts b/ts/features/idpay/common/saga/index.ts index a85d49774e5..4b7497c9b5b 100644 --- a/ts/features/idpay/common/saga/index.ts +++ b/ts/features/idpay/common/saga/index.ts @@ -20,6 +20,7 @@ import { watchIDPayInitiativeDetailsSaga } from "../../details/saga"; import { watchIDPayTimelineSaga } from "../../timeline/saga"; import { watchIDPayInitiativeConfigurationSaga } from "../../configuration/saga"; import { watchIDPayBarcodeSaga } from "../../barcode/saga"; +import { watchIdPayUnsubscriptionSaga } from "../../unsubscription/saga"; export function* watchIDPaySaga(bpdToken: string): SagaIterator { const isPagoPATestEnabled = yield* select(isPagoPATestEnabledSelector); @@ -64,4 +65,10 @@ export function* watchIDPaySaga(bpdToken: string): SagaIterator { preferredLanguage ); yield* fork(watchIDPayBarcodeSaga, idPayClient, bearerToken); + yield* fork( + watchIdPayUnsubscriptionSaga, + idPayClient, + bearerToken, + preferredLanguage + ); } diff --git a/ts/features/idpay/common/store/actions/index.ts b/ts/features/idpay/common/store/actions/index.ts index c8088ad239a..18dc6ef4a75 100644 --- a/ts/features/idpay/common/store/actions/index.ts +++ b/ts/features/idpay/common/store/actions/index.ts @@ -3,6 +3,7 @@ import { IdPayCodeActions } from "../../../code/store/actions"; import { IDPayInitiativeConfigurationActions } from "../../../configuration/store/actions"; import { IdPayInitiativeActions } from "../../../details/store/actions"; import { IdPayTimelineActions } from "../../../timeline/store/actions"; +import { IdPayUnsubscriptionActions } from "../../../unsubscription/store/actions"; import { IdPayWalletActions } from "../../../wallet/store/actions"; export type IdPayActions = @@ -11,4 +12,5 @@ export type IdPayActions = | IdPayTimelineActions | IdPayCodeActions | IDPayInitiativeConfigurationActions - | IdPayBarcodeActions; + | IdPayBarcodeActions + | IdPayUnsubscriptionActions; diff --git a/ts/features/idpay/common/store/reducers/index.ts b/ts/features/idpay/common/store/reducers/index.ts index 2ce1e4bddc0..cf463a70506 100644 --- a/ts/features/idpay/common/store/reducers/index.ts +++ b/ts/features/idpay/common/store/reducers/index.ts @@ -13,6 +13,9 @@ import configurationReducer, { IdPayInitiativeConfigurationState } from "../../../configuration/store"; import barcodeReducer, { IdPayBarcodeState } from "../../../barcode/store"; +import unsubscriptionReducer, { + IdPayUnsubscriptionState +} from "../../../unsubscription/store/reducers"; export type IDPayState = { wallet: IdPayWalletState; @@ -21,6 +24,7 @@ export type IDPayState = { configuration: IdPayInitiativeConfigurationState; code: IdPayCodeState & PersistPartial; barcode: IdPayBarcodeState; + unsubscription: IdPayUnsubscriptionState; }; const idPayReducer = combineReducers({ @@ -29,7 +33,8 @@ const idPayReducer = combineReducers({ timeline: timelineReducer, code: codePersistor, configuration: configurationReducer, - barcode: barcodeReducer + barcode: barcodeReducer, + unsubscription: unsubscriptionReducer }); export default idPayReducer; diff --git a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx index f4cdf6305a2..7ca8186c1c0 100644 --- a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx +++ b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx @@ -1,6 +1,7 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { useNavigation } from "@react-navigation/native"; +import { sequenceS } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; @@ -183,11 +184,27 @@ const BeneficiaryDetailsContent = (props: BeneficiaryDetailsProps) => { ) ); - const handleUnsubscribePress = () => - navigation.navigate( - IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR, - { initiativeId, initiativeName, initiativeType } + const handleUnsubscribePress = () => { + pipe( + sequenceS(O.Monad)({ + initiativeName: O.fromNullable(initiativeName), + initiativeType: O.fromNullable(initiativeType) + }), + O.map(({ initiativeName, initiativeType }) => { + navigation.navigate( + IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN, + { + screen: IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION, + params: { + initiativeId, + initiativeName, + initiativeType + } + } + ); + }) ); + }; return ( <> diff --git a/ts/features/idpay/unsubscription/machine/actions.ts b/ts/features/idpay/unsubscription/machine/actions.ts deleted file mode 100644 index f56ea679570..00000000000 --- a/ts/features/idpay/unsubscription/machine/actions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import ROUTES from "../../../../navigation/routes"; -import { IdPayUnsubscriptionRoutes } from "../navigation/routes"; - -const createActionsImplementation = ( - navigation: ReturnType -) => { - const navigateToConfirmationScreen = () => { - navigation.navigate( - IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR, - { - screen: IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION - } - ); - }; - - const navigateToResultScreen = () => - navigation.navigate( - IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR, - { - screen: IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT - } - ); - - const exitUnsubscription = () => { - navigation.pop(); - }; - - const exitToWallet = () => { - navigation.popToTop(); - navigation.navigate(ROUTES.MAIN, { - screen: ROUTES.WALLET_HOME, - params: { newMethodAdded: false } - }); - }; - - return { - navigateToConfirmationScreen, - navigateToResultScreen, - exitUnsubscription, - exitToWallet - }; -}; - -export { createActionsImplementation }; diff --git a/ts/features/idpay/unsubscription/machine/actors.ts b/ts/features/idpay/unsubscription/machine/actors.ts deleted file mode 100644 index 21279019d7c..00000000000 --- a/ts/features/idpay/unsubscription/machine/actors.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as E from "fp-ts/lib/Either"; -import * as TE from "fp-ts/lib/TaskEither"; -import { flow, pipe } from "fp-ts/lib/function"; -import { fromPromise } from "xstate"; -import { PreferredLanguage } from "../../../../../definitions/backend/PreferredLanguage"; -import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; -import { IDPayClient } from "../../common/api/client"; -import { UnsubscriptionFailureEnum } from "../types/failure"; -import { useIODispatch } from "../../../../store/hooks"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; - -export const createActorsImplementation = ( - client: IDPayClient, - token: string, - language: PreferredLanguage, - dispatch: ReturnType -) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - - const getInitiativeInfo = fromPromise( - async (params: { input: string }): Promise => { - const dataResponse = await TE.tryCatch( - async () => - await client.getWalletDetail({ - bearerAuth: token, - "Accept-Language": language, - initiativeId: params.input - }), - E.toError - )(); - - return pipe( - dataResponse, - E.fold( - () => Promise.reject(UnsubscriptionFailureEnum.UNEXPECTED), - flow( - E.map(({ status, value }) => { - switch (status) { - case 200: - return Promise.resolve(value); - case 401: - handleSessionExpired(); - return Promise.reject( - UnsubscriptionFailureEnum.SESSION_EXPIRED - ); - default: - return Promise.reject(UnsubscriptionFailureEnum.GENERIC); - } - }), - E.getOrElse(() => Promise.reject(UnsubscriptionFailureEnum.GENERIC)) - ) - ) - ); - } - ); - - const unsubscribeFromInitiative = fromPromise( - async (params: { input: string }): Promise => { - const dataResponse = await TE.tryCatch( - async () => - await client.unsubscribe({ - bearerAuth: token, - "Accept-Language": language, - initiativeId: params.input - }), - E.toError - )(); - - return pipe( - dataResponse, - E.fold( - () => Promise.reject(UnsubscriptionFailureEnum.UNEXPECTED), - flow( - E.map(({ status }) => { - switch (status) { - case 204: - return Promise.resolve(undefined); - case 401: - handleSessionExpired(); - return Promise.reject( - UnsubscriptionFailureEnum.SESSION_EXPIRED - ); - default: - return Promise.reject(UnsubscriptionFailureEnum.GENERIC); - } - }), - E.getOrElse(() => Promise.reject(UnsubscriptionFailureEnum.GENERIC)) - ) - ) - ); - } - ); - - return { - getInitiativeInfo, - unsubscribeFromInitiative - }; -}; diff --git a/ts/features/idpay/unsubscription/machine/context.ts b/ts/features/idpay/unsubscription/machine/context.ts deleted file mode 100644 index f4bde1cc8c0..00000000000 --- a/ts/features/idpay/unsubscription/machine/context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; - -export interface Context { - readonly initiativeId: string; - readonly initiativeName: string | undefined; - readonly initiativeType: InitiativeRewardTypeEnum | undefined; -} - -export const Context: Context = { - initiativeId: "", - initiativeName: undefined, - initiativeType: undefined -}; diff --git a/ts/features/idpay/unsubscription/machine/events.ts b/ts/features/idpay/unsubscription/machine/events.ts deleted file mode 100644 index 65b0da37ffa..00000000000 --- a/ts/features/idpay/unsubscription/machine/events.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GlobalEvents } from "../../../../xstate/types/events"; -import * as Input from "./input"; - -export interface AutoInit { - readonly type: "xstate.init"; - readonly input: Input.Input; -} - -export interface ConfirmUnsubscription { - readonly type: "confirm-unsubscription"; -} - -export type Events = GlobalEvents | AutoInit | ConfirmUnsubscription; diff --git a/ts/features/idpay/unsubscription/machine/input.ts b/ts/features/idpay/unsubscription/machine/input.ts deleted file mode 100644 index a9901ba7742..00000000000 --- a/ts/features/idpay/unsubscription/machine/input.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; -import * as Context from "./context"; - -export interface Input { - readonly initiativeId: string; - readonly initiativeName: string | undefined; - readonly initiativeType: InitiativeRewardTypeEnum | undefined; -} - -export const Input = (input: Input): Promise => - Promise.resolve({ ...input }); diff --git a/ts/features/idpay/unsubscription/machine/machine.ts b/ts/features/idpay/unsubscription/machine/machine.ts deleted file mode 100644 index e9890d30171..00000000000 --- a/ts/features/idpay/unsubscription/machine/machine.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { assertEvent, assign, fromPromise, setup } from "xstate"; -import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; -import { - LOADING_TAG, - WAITING_USER_INPUT_TAG, - notImplementedStub -} from "../../../../xstate/utils"; -import * as Context from "./context"; -import * as Events from "./events"; -import * as Input from "./input"; - -export const idPayUnsubscriptionMachine = setup({ - types: { - input: {} as Input.Input, - context: {} as Context.Context, - events: {} as Events.Events - }, - actions: { - navigateToConfirmationScreen: notImplementedStub, - navigateToResultScreen: notImplementedStub, - exitToWallet: notImplementedStub, - exitUnsubscription: notImplementedStub - }, - actors: { - onInit: fromPromise(({ input }) => - Input.Input(input) - ), - getInitiativeInfo: fromPromise(notImplementedStub), - unsubscribeFromInitiative: fromPromise( - notImplementedStub - ) - }, - guards: { - hasMissingInitiativeData: ({ context }) => - context.initiativeName === undefined || - context.initiativeType === undefined, - isSessionExpired: () => false - } -}).createMachine({ - id: "idpay-unsubscription", - context: Context.Context, - entry: "navigateToConfirmationScreen", - invoke: { - src: "onInit", - input: ({ event }) => { - assertEvent(event, "xstate.init"); - return event.input; - }, - onError: { - target: ".UnsubscriptionFailure" - }, - onDone: { - actions: assign(event => ({ ...event.event.output })), - target: ".Idle" - } - }, - initial: "Idle", - states: { - Idle: { - tags: [LOADING_TAG], - always: [ - { - guard: "hasMissingInitiativeData", - target: "LoadingInitiativeInfo" - }, - { - target: "WaitingConfirmation" - } - ] - }, - LoadingInitiativeInfo: { - tags: [LOADING_TAG], - invoke: { - input: ({ context }) => context.initiativeId, - src: "getInitiativeInfo", - onError: { - target: "UnsubscriptionFailure" - }, - onDone: { - actions: assign(({ event }) => ({ - initiativeId: event.output.initiativeId, - initiativeName: event.output.initiativeName, - initiativeType: event.output.initiativeRewardType - })), - target: "WaitingConfirmation" - } - } - }, - WaitingConfirmation: { - tags: [WAITING_USER_INPUT_TAG], - on: { - "confirm-unsubscription": { - target: "Unsubscribing" - }, - close: { - actions: "exitUnsubscription" - } - } - }, - Unsubscribing: { - tags: [LOADING_TAG], - invoke: { - input: ({ context }) => context.initiativeId, - src: "unsubscribeFromInitiative", - onError: { - target: "UnsubscriptionFailure" - }, - onDone: { - target: "UnsubscriptionSuccess" - } - } - }, - UnsubscriptionSuccess: { - entry: "navigateToResultScreen", - on: { - close: { - actions: "exitToWallet" - } - } - }, - UnsubscriptionFailure: { - entry: "navigateToResultScreen", - always: { - guard: "isSessionExpired", - target: "SessionExpired" - }, - on: { - close: { - actions: "exitUnsubscription" - } - } - }, - SessionExpired: { - entry: "exitUnsubscription" - } - } -}); diff --git a/ts/features/idpay/unsubscription/machine/provider.tsx b/ts/features/idpay/unsubscription/machine/provider.tsx deleted file mode 100644 index 033c627dfbd..00000000000 --- a/ts/features/idpay/unsubscription/machine/provider.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { createActorContext } from "@xstate/react"; -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import React from "react"; -import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; -import { - idPayApiBaseUrl, - idPayApiUatBaseUrl, - idPayTestToken -} from "../../../../config"; -import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; -import { - isPagoPATestEnabledSelector, - preferredLanguageSelector -} from "../../../../store/reducers/persistedPreferences"; -import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; -import { createIDPayClient } from "../../common/api/client"; -import { createActionsImplementation } from "./actions"; -import { createActorsImplementation } from "./actors"; -import * as Input from "./input"; -import { idPayUnsubscriptionMachine } from "./machine"; - -type Props = { - children: React.ReactNode; - input: Input.Input; -}; - -export const IdPayUnsubscriptionMachineContext = createActorContext( - idPayUnsubscriptionMachine -); - -export const IdPayUnsubscriptionMachineProvider = ({ - children, - input -}: Props) => { - const navigation = useIONavigation(); - const dispatch = useIODispatch(); - - const sessionInfo = useIOSelector(sessionInfoSelector); - const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - const preferredLanguageOption = useIOSelector(preferredLanguageSelector); - - const language = pipe( - preferredLanguageOption, - O.map(fromLocaleToPreferredLanguage), - O.getOrElse(() => PreferredLanguageEnum.it_IT) - ); - - if (O.isNone(sessionInfo)) { - throw new Error("Session info is undefined"); - } - const { bpdToken } = sessionInfo.value; - - const idPayClient = createIDPayClient( - isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl - ); - - const actors = createActorsImplementation( - idPayClient, - idPayTestToken ?? bpdToken, - language, - dispatch - ); - const actions = createActionsImplementation(navigation); - const machine = idPayUnsubscriptionMachine.provide({ - actors, - actions - }); - - return ( - - {children} - - ); -}; diff --git a/ts/features/idpay/unsubscription/machine/selectors.ts b/ts/features/idpay/unsubscription/machine/selectors.ts deleted file mode 100644 index aab6f4717fd..00000000000 --- a/ts/features/idpay/unsubscription/machine/selectors.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { createSelector } from "reselect"; -import { SnapshotFrom } from "xstate"; -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; -import I18n from "../../../../i18n"; -import { idPayUnsubscriptionMachine } from "./machine"; - -type MachineSnapshot = SnapshotFrom; - -export const selectInitiativeName = ({ context }: MachineSnapshot) => - context.initiativeName; - -export const selectIsFailure = (snapshot: MachineSnapshot) => - snapshot.matches("UnsubscriptionFailure"); - -export const selectInitiativeType = ({ context }: MachineSnapshot) => - pipe( - context.initiativeType, - O.fromNullable, - O.getOrElse(() => InitiativeRewardTypeEnum.REFUND) - ); - -const checks = { - [InitiativeRewardTypeEnum.REFUND]: [ - { - title: I18n.t("idpay.unsubscription.checks.1.title"), - subtitle: I18n.t("idpay.unsubscription.checks.1.content") - }, - { - title: I18n.t("idpay.unsubscription.checks.2.title"), - subtitle: I18n.t("idpay.unsubscription.checks.2.content") - }, - { - title: I18n.t("idpay.unsubscription.checks.3.title"), - subtitle: I18n.t("idpay.unsubscription.checks.3.content") - }, - { - title: I18n.t("idpay.unsubscription.checks.4.title"), - subtitle: I18n.t("idpay.unsubscription.checks.4.content") - } - ], - - [InitiativeRewardTypeEnum.DISCOUNT]: [ - { - title: I18n.t("idpay.unsubscription.checks.1.title"), - subtitle: I18n.t("idpay.unsubscription.checks.1.content") - }, - { - title: I18n.t("idpay.unsubscription.checks.3.title"), - subtitle: I18n.t("idpay.unsubscription.checks.3.content") - } - ] -}; - -export const selectUnsubscriptionChecks = createSelector( - selectInitiativeType, - type => checks[type] -); diff --git a/ts/features/idpay/unsubscription/navigation/navigator.tsx b/ts/features/idpay/unsubscription/navigation/navigator.tsx index 80c37befaa4..a78e7060438 100644 --- a/ts/features/idpay/unsubscription/navigation/navigator.tsx +++ b/ts/features/idpay/unsubscription/navigation/navigator.tsx @@ -1,7 +1,5 @@ -import { RouteProp, useRoute } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; -import { IdPayUnsubscriptionMachineProvider } from "../machine/provider"; import UnsubscriptionConfirmationScreen from "../screens/UnsubscriptionConfirmationScreen"; import UnsubscriptionResultScreen from "../screens/UnsubscriptionResultScreen"; import { IdPayUnsubscriptionParamsList } from "./params"; @@ -9,34 +7,20 @@ import { IdPayUnsubscriptionRoutes } from "./routes"; const Stack = createStackNavigator(); -type IdPayUnsubscriptionScreenRouteProps = RouteProp< - IdPayUnsubscriptionParamsList, - "IDPAY_UNSUBSCRIPTION_NAVIGATOR" ->; - -export const IdPayUnsubscriptionNavigator = () => { - const { params } = useRoute(); - const { initiativeId, initiativeName, initiativeType } = params; - - return ( - - - - - - - ); -}; +export const IdPayUnsubscriptionNavigator = () => ( + + + + +); diff --git a/ts/features/idpay/unsubscription/navigation/params.ts b/ts/features/idpay/unsubscription/navigation/params.ts index 5ef8547beb1..f33e0df3873 100644 --- a/ts/features/idpay/unsubscription/navigation/params.ts +++ b/ts/features/idpay/unsubscription/navigation/params.ts @@ -1,14 +1,7 @@ -import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; +import { IdPayUnsubscriptionConfirmationScreenParams } from "../screens/UnsubscriptionConfirmationScreen"; import { IdPayUnsubscriptionRoutes } from "./routes"; -export type IdPayUnsubscriptionNavigatorParams = { - initiativeId: string; - initiativeName?: string; - initiativeType?: InitiativeRewardTypeEnum; -}; - export type IdPayUnsubscriptionParamsList = { - [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR]: IdPayUnsubscriptionNavigatorParams; - [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION]: undefined; + [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_CONFIRMATION]: IdPayUnsubscriptionConfirmationScreenParams; [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT]: undefined; }; diff --git a/ts/features/idpay/unsubscription/navigation/routes.ts b/ts/features/idpay/unsubscription/navigation/routes.ts index a688af9915f..f7943e2a6e6 100644 --- a/ts/features/idpay/unsubscription/navigation/routes.ts +++ b/ts/features/idpay/unsubscription/navigation/routes.ts @@ -1,5 +1,5 @@ export const IdPayUnsubscriptionRoutes = { - IDPAY_UNSUBSCRIPTION_NAVIGATOR: "IDPAY_UNSUBSCRIPTION_NAVIGATOR", + IDPAY_UNSUBSCRIPTION_MAIN: "IDPAY_UNSUBSCRIPTION_MAIN", IDPAY_UNSUBSCRIPTION_CONFIRMATION: "IDPAY_UNSUBSCRIPTION_CONFIRMATION", IDPAY_UNSUBSCRIPTION_RESULT: "IDPAY_UNSUBSCRIPTION_RESULT" } as const; diff --git a/ts/features/idpay/unsubscription/saga/handleUnsubscribe.ts b/ts/features/idpay/unsubscription/saga/handleUnsubscribe.ts new file mode 100644 index 00000000000..eee521376d7 --- /dev/null +++ b/ts/features/idpay/unsubscription/saga/handleUnsubscribe.ts @@ -0,0 +1,59 @@ +import * as E from "fp-ts/lib/Either"; +import { pipe } from "fp-ts/lib/function"; +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; +import { SagaCallReturnType } from "../../../../types/utils"; +import { getGenericError, getNetworkError } from "../../../../utils/errors"; +import { readablePrivacyReport } from "../../../../utils/reporters"; +import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { IDPayClient } from "../../common/api/client"; +import { idPayUnsubscribeAction } from "../store/actions"; + +export function* handleUnsubscribe( + unsubscribe: IDPayClient["unsubscribe"], + bearerToken: string, + language: PreferredLanguageEnum, + action: ActionType<(typeof idPayUnsubscribeAction)["request"]> +) { + const unsubscribeRequest = unsubscribe({ + bearerAuth: bearerToken, + "Accept-Language": language, + initiativeId: action.payload.initiativeId + }); + + try { + const getTimelineResult = (yield* call( + withRefreshApiCall, + unsubscribeRequest, + action + )) as unknown as SagaCallReturnType; + + yield pipe( + getTimelineResult, + E.fold( + error => + put( + idPayUnsubscribeAction.failure({ + ...getGenericError(new Error(readablePrivacyReport(error))) + }) + ), + response => { + if (response.status === 204) { + return put(idPayUnsubscribeAction.success()); + } else { + return put( + idPayUnsubscribeAction.failure({ + ...getGenericError( + new Error(`response status code ${response.status}`) + ) + }) + ); + } + } + ) + ); + } catch (e) { + yield* put(idPayUnsubscribeAction.failure({ ...getNetworkError(e) })); + } +} diff --git a/ts/features/idpay/unsubscription/saga/index.ts b/ts/features/idpay/unsubscription/saga/index.ts new file mode 100644 index 00000000000..69303022a8b --- /dev/null +++ b/ts/features/idpay/unsubscription/saga/index.ts @@ -0,0 +1,20 @@ +import { SagaIterator } from "redux-saga"; +import { takeLatest } from "typed-redux-saga/macro"; +import { PreferredLanguageEnum } from "../../../../../definitions/backend/PreferredLanguage"; +import { IDPayClient } from "../../common/api/client"; +import { idPayUnsubscribeAction } from "../store/actions"; +import { handleUnsubscribe } from "./handleUnsubscribe"; + +export function* watchIdPayUnsubscriptionSaga( + idPayClient: IDPayClient, + bearerToken: string, + preferredLanguage: PreferredLanguageEnum +): SagaIterator { + yield* takeLatest( + idPayUnsubscribeAction.request, + handleUnsubscribe, + idPayClient.unsubscribe, + bearerToken, + preferredLanguage + ); +} diff --git a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx index 9f91d50e1d7..1a49cffedf8 100644 --- a/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx +++ b/ts/features/idpay/unsubscription/screens/UnsubscriptionConfirmationScreen.tsx @@ -4,9 +4,11 @@ import { IconButton, VSpacer } from "@pagopa/io-app-design-system"; +import { RouteProp, useRoute } from "@react-navigation/native"; import React from "react"; import { SafeAreaView, View } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; +import { InitiativeRewardTypeEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import { Body } from "../../../../components/core/typography/Body"; import { H1 } from "../../../../components/core/typography/H1"; @@ -15,37 +17,92 @@ import BaseScreenComponent from "../../../../components/screens/BaseScreenCompon import LegacyFooterWithButtons from "../../../../components/ui/FooterWithButtons"; import { useConfirmationChecks } from "../../../../hooks/useConfirmationChecks"; import I18n from "../../../../i18n"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet"; import { UnsubscriptionCheckListItem } from "../components/UnsubscriptionCheckListItem"; -import { IdPayUnsubscriptionMachineContext } from "../machine/provider"; +import { IdPayUnsubscriptionParamsList } from "../navigation/params"; +import { idPayUnsubscribeAction } from "../store/actions"; import { - selectInitiativeName, - selectUnsubscriptionChecks -} from "../machine/selectors"; -import { isLoadingSelector } from "../../../../xstate/selectors"; + isFailureSelector, + isLoadingSelector, + isUnsubscriptionSuccessSelector +} from "../store/selectors"; +import { IdPayUnsubscriptionRoutes } from "../navigation/routes"; -const UnsubscriptionConfirmationScreen = () => { - const { useActorRef, useSelector } = IdPayUnsubscriptionMachineContext; - const machine = useActorRef(); +export type IdPayUnsubscriptionConfirmationScreenParams = { + initiativeId: string; + initiativeName: string; + initiativeType: InitiativeRewardTypeEnum; +}; + +type RouteProps = RouteProp< + IdPayUnsubscriptionParamsList, + "IDPAY_UNSUBSCRIPTION_CONFIRMATION" +>; - const isLoading = useSelector(isLoadingSelector); - const initiativeName = useSelector(selectInitiativeName); - const unsubscriptionChecks = useSelector(selectUnsubscriptionChecks); +const checksByInitiativeType = { + [InitiativeRewardTypeEnum.REFUND]: [ + { + title: I18n.t("idpay.unsubscription.checks.1.title"), + subtitle: I18n.t("idpay.unsubscription.checks.1.content") + }, + { + title: I18n.t("idpay.unsubscription.checks.2.title"), + subtitle: I18n.t("idpay.unsubscription.checks.2.content") + }, + { + title: I18n.t("idpay.unsubscription.checks.3.title"), + subtitle: I18n.t("idpay.unsubscription.checks.3.content") + }, + { + title: I18n.t("idpay.unsubscription.checks.4.title"), + subtitle: I18n.t("idpay.unsubscription.checks.4.content") + } + ], + [InitiativeRewardTypeEnum.DISCOUNT]: [ + { + title: I18n.t("idpay.unsubscription.checks.1.title"), + subtitle: I18n.t("idpay.unsubscription.checks.1.content") + }, + { + title: I18n.t("idpay.unsubscription.checks.3.title"), + subtitle: I18n.t("idpay.unsubscription.checks.3.content") + } + ] +}; + +const UnsubscriptionConfirmationScreen = () => { + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + const { params } = useRoute(); + const { initiativeId, initiativeName, initiativeType } = params; + + const isLoading = useIOSelector(isLoadingSelector); + const isSuccess = useIOSelector(isUnsubscriptionSuccessSelector); + const isFailure = useIOSelector(isFailureSelector); + const unsubscriptionChecks = checksByInitiativeType[initiativeType]; const checks = useConfirmationChecks(unsubscriptionChecks.length); - const handleClosePress = () => - machine.send({ - type: "close" - }); + const handleClosePress = () => { + dispatch(idPayUnsubscribeAction.cancel()); + navigation.pop(); + }; const handleConfirmPress = () => { - machine.send({ - type: "confirm-unsubscription" - }); + dispatch(idPayUnsubscribeAction.request({ initiativeId })); }; + React.useEffect(() => { + if (isFailure || isSuccess) { + navigation.navigate(IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN, { + screen: IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_RESULT + }); + } + }, [navigation, isFailure, isSuccess]); + const closeButton = ( { - const { useActorRef, useSelector } = IdPayUnsubscriptionMachineContext; - const machine = useActorRef(); - const isFailure = useSelector(selectIsFailure); + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + const isFailure = useIOSelector(isFailureSelector); const { pictogram, title, content, buttonLabel }: ScreenContentType = isFailure @@ -41,7 +44,18 @@ const UnsubscriptionResultScreen = () => { buttonLabel: I18n.t("idpay.unsubscription.success.button") }; - const handleButtonPress = () => machine.send({ type: "close" }); + const handleButtonPress = () => { + dispatch(idPayUnsubscribeAction.cancel()); + if (isFailure) { + navigation.pop(); + } else { + navigation.popToTop(); + navigation.navigate(ROUTES.MAIN, { + screen: ROUTES.WALLET_HOME, + params: { newMethodAdded: false } + }); + } + }; return ( diff --git a/ts/features/idpay/unsubscription/store/actions/index.ts b/ts/features/idpay/unsubscription/store/actions/index.ts new file mode 100644 index 00000000000..f7e7f969116 --- /dev/null +++ b/ts/features/idpay/unsubscription/store/actions/index.ts @@ -0,0 +1,13 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { NetworkError } from "../../../../../utils/errors"; + +export const idPayUnsubscribeAction = createAsyncAction( + "IDPAY_UNSUBSCRIBE_REQUEST", + "IDPAY_UNSUBSCRIBE_SUCCESS", + "IDPAY_UNSUBSCRIBE_FAILURE", + "IDPAY_UNSUBSCRIBE_CANCEL" +)<{ initiativeId: string }, undefined, NetworkError, undefined>(); + +export type IdPayUnsubscriptionActions = ActionType< + typeof idPayUnsubscribeAction +>; diff --git a/ts/features/idpay/unsubscription/store/reducers/index.ts b/ts/features/idpay/unsubscription/store/reducers/index.ts new file mode 100644 index 00000000000..28b662d809a --- /dev/null +++ b/ts/features/idpay/unsubscription/store/reducers/index.ts @@ -0,0 +1,28 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; +import { idPayUnsubscribeAction } from "../actions"; +import { NetworkError } from "../../../../../utils/errors"; +import { Action } from "../../../../../store/actions/types"; + +export type IdPayUnsubscriptionState = pot.Pot; + +const INITIAL_STATE: IdPayUnsubscriptionState = pot.none; + +const reducer = ( + state: IdPayUnsubscriptionState = INITIAL_STATE, + action: Action +): IdPayUnsubscriptionState => { + switch (action.type) { + case getType(idPayUnsubscribeAction.request): + return pot.toLoading(pot.none); + case getType(idPayUnsubscribeAction.success): + return pot.some(undefined); + case getType(idPayUnsubscribeAction.failure): + return pot.toError(state, action.payload); + case getType(idPayUnsubscribeAction.cancel): + return pot.none; + } + return state; +}; + +export default reducer; diff --git a/ts/features/idpay/unsubscription/store/selectors/index.ts b/ts/features/idpay/unsubscription/store/selectors/index.ts new file mode 100644 index 00000000000..ae329e3c5fa --- /dev/null +++ b/ts/features/idpay/unsubscription/store/selectors/index.ts @@ -0,0 +1,11 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { GlobalState } from "../../../../../store/reducers/types"; + +export const isUnsubscriptionSuccessSelector = (state: GlobalState) => + pot.isSome(state.features.idPay.unsubscription); + +export const isLoadingSelector = (state: GlobalState) => + pot.isLoading(state.features.idPay.unsubscription); + +export const isFailureSelector = (state: GlobalState) => + pot.isError(state.features.idPay.unsubscription); diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index e8264f4c55c..635fa577e3a 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -255,7 +255,7 @@ const AuthenticatedStackNavigator = () => { options={{ gestureEnabled: isGestureEnabled, ...hideHeaderOptions }} /> diff --git a/ts/navigation/params/AppParamsList.ts b/ts/navigation/params/AppParamsList.ts index 506643392aa..fe0e8f52e19 100644 --- a/ts/navigation/params/AppParamsList.ts +++ b/ts/navigation/params/AppParamsList.ts @@ -26,23 +26,14 @@ import { IDPayDetailsParamsList, IDPayDetailsRoutes } from "../../features/idpay/details/navigation"; -import { - IdPayOnboardingNavigatorParams, - IdPayOnboardingParamsList -} from "../../features/idpay/onboarding/navigation/params"; +import { IdPayOnboardingParamsList } from "../../features/idpay/onboarding/navigation/params"; import { IdPayOnboardingRoutes } from "../../features/idpay/onboarding/navigation/routes"; -import { - IdPayConfigurationNavigatorParams, - IdPayConfigurationParamsList -} from "../../features/idpay/configuration/navigation/params"; +import { IdPayConfigurationParamsList } from "../../features/idpay/configuration/navigation/params"; import { IdPayConfigurationRoutes } from "../../features/idpay/configuration/navigation/routes"; import { IdPayPaymentParamsList } from "../../features/idpay/payment/navigation/params"; import { IdPayPaymentRoutes } from "../../features/idpay/payment/navigation/routes"; -import { - IdPayUnsubscriptionNavigatorParams, - IdPayUnsubscriptionParamsList -} from "../../features/idpay/unsubscription/navigation/params"; +import { IdPayUnsubscriptionParamsList } from "../../features/idpay/unsubscription/navigation/params"; import { IdPayUnsubscriptionRoutes } from "../../features/idpay/unsubscription/navigation/routes"; import { MessagesParamsList } from "../../features/messages/navigation/params"; import { MESSAGES_ROUTES } from "../../features/messages/navigation/routes"; @@ -102,16 +93,10 @@ export type AppParamsList = { [FIMS_ROUTES.MAIN]: NavigatorScreenParams; [FCI_ROUTES.MAIN]: NavigatorScreenParams; - [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: - | IdPayOnboardingNavigatorParams - | NavigatorScreenParams; - [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR]: - | IdPayConfigurationNavigatorParams - | NavigatorScreenParams; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: NavigatorScreenParams; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR]: NavigatorScreenParams; [IDPayDetailsRoutes.IDPAY_DETAILS_MAIN]: NavigatorScreenParams; - [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_NAVIGATOR]: - | IdPayUnsubscriptionNavigatorParams - | NavigatorScreenParams; + [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN]: NavigatorScreenParams; [IdPayPaymentRoutes.IDPAY_PAYMENT_CODE_SCAN]: undefined; // FIXME IOBP-383: remove after react-navigation 6.x upgrade. This should be insde IDPAY_PAYMENT_MAIN [IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN]: NavigatorScreenParams; [IdPayCodeRoutes.IDPAY_CODE_MAIN]: NavigatorScreenParams; From 69c01e92d4a33257092cf816484a3364f57e7273 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Fri, 3 May 2024 15:29:04 +0200 Subject: [PATCH 10/31] chore: payment machine refinement --- package.json | 2 - ts/features/idpay/payment/machine/actions.ts | 34 +-- ts/features/idpay/payment/machine/actors.ts | 36 ++- ts/features/idpay/payment/machine/input.ts | 8 - ts/features/idpay/payment/machine/machine.ts | 85 ++++--- .../idpay/payment/machine/provider.tsx | 39 +-- .../screens/IDPayPaymentCodeInputScreen.tsx | 8 +- yarn.lock | 237 ++---------------- 8 files changed, 106 insertions(+), 343 deletions(-) delete mode 100644 ts/features/idpay/payment/machine/input.ts diff --git a/package.json b/package.json index 4e47aaa0b6e..773e82c7587 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "generate": "npm-run-all generate:*", "locales_unused": "ts-node --skip-project -O '{\"lib\":[\"es2015\"]}' scripts/unused-locales.ts", "remove_unused_locales": "ts-node --skip-project -O '{\"lib\":[\"es2015\"]}' scripts/remove-unused-locales.ts", - "generate:xstate-types": "xstate typegen \"ts/**/*.ts?(x)\"", "prepare": "husky install" }, "lint-staged": { @@ -255,7 +254,6 @@ "@types/xml2js": "^0.4.11", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^5.9.1", - "@xstate/cli": "^0.3.3", "abortcontroller-polyfill": "1.7.3", "babel-jest": "^26.6.3", "babel-plugin-macros": "^3.1.0", diff --git a/ts/features/idpay/payment/machine/actions.ts b/ts/features/idpay/payment/machine/actions.ts index 9d61ca9b478..c627b3ec261 100644 --- a/ts/features/idpay/payment/machine/actions.ts +++ b/ts/features/idpay/payment/machine/actions.ts @@ -1,15 +1,12 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; +import { useIOToast } from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { showToast } from "../../../../utils/showToast"; -import { IDPayDetailsRoutes } from "../../details/navigation"; import { IdPayPaymentRoutes } from "../navigation/routes"; -import { Context } from "./context"; -const createActionsImplementation = ( - navigation: ReturnType -) => { +export const useActionsImplementation = () => { + const navigation = useIONavigation(); + const toast = useIOToast(); + const navigateToAuthorizationScreen = () => { navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { screen: IdPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION, @@ -23,28 +20,17 @@ const createActionsImplementation = ( }); const showErrorToast = () => - showToast(I18n.t("idpay.payment.authorization.error"), "danger", "top"); + toast.error(I18n.t("idpay.payment.authorization.error")); - const exitAuthorization = (context: Context) => { - pipe( - context.transactionData, - O.map(({ initiativeId }) => { - navigation.popToTop(); - navigation.navigate(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, { - screen: IDPayDetailsRoutes.IDPAY_DETAILS_MONITORING, - params: { initiativeId } - }); - }), - O.getOrElse(() => navigation.pop()) - ); + const closeAuthorization = () => { + navigation.pop(); + navigation.pop(); }; return { navigateToAuthorizationScreen, navigateToResultScreen, showErrorToast, - exitAuthorization + closeAuthorization }; }; - -export { createActionsImplementation }; diff --git a/ts/features/idpay/payment/machine/actors.ts b/ts/features/idpay/payment/machine/actors.ts index 8fe33b899ee..17666ded47e 100644 --- a/ts/features/idpay/payment/machine/actors.ts +++ b/ts/features/idpay/payment/machine/actors.ts @@ -1,19 +1,38 @@ import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { flow, pipe } from "fp-ts/lib/function"; import { fromPromise } from "xstate"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { CodeEnum as TransactionErrorCodeEnum } from "../../../../../definitions/idpay/TransactionErrorDTO"; -import { IDPayClient } from "../../common/api/client"; -import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; +import { + idPayApiBaseUrl, + idPayApiUatBaseUrl, + idPayTestToken +} from "../../../../config"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { sessionInfoSelector } from "../../../../store/reducers/authentication"; +import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; -import { useIODispatch } from "../../../../store/hooks"; +import { createIDPayClient } from "../../common/api/client"; +import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; + +export const useActorsImplementation = () => { + const dispatch = useIODispatch(); + const sessionInfo = useIOSelector(sessionInfoSelector); + const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); + + if (O.isNone(sessionInfo)) { + throw new Error("Session info is undefined"); + } + + const { bpdToken } = sessionInfo.value; + const token = idPayTestToken ?? bpdToken; + + const client = createIDPayClient( + isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl + ); -export const createActorsImplementation = ( - client: IDPayClient, - token: string, - dispatch: ReturnType -) => { const handleSessionExpired = () => { dispatch( refreshSessionToken.request({ @@ -141,6 +160,7 @@ export const createActorsImplementation = ( deletePayment }; }; + /** * Maps the backed error codes to UI failure states * @param code Error code from backend diff --git a/ts/features/idpay/payment/machine/input.ts b/ts/features/idpay/payment/machine/input.ts deleted file mode 100644 index 3030188249b..00000000000 --- a/ts/features/idpay/payment/machine/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Context from "./context"; - -export interface Input { - readonly trxCode: string; -} - -export const Input = (input: Input): Promise => - Promise.resolve({ ...Context.Context, ...input }); diff --git a/ts/features/idpay/payment/machine/machine.ts b/ts/features/idpay/payment/machine/machine.ts index 3e640d898a0..dd24bf4f6a6 100644 --- a/ts/features/idpay/payment/machine/machine.ts +++ b/ts/features/idpay/payment/machine/machine.ts @@ -1,5 +1,6 @@ +import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; +import { flow, pipe } from "fp-ts/lib/function"; import { assertEvent, assign, fromPromise, setup } from "xstate"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { @@ -8,21 +9,17 @@ import { WAITING_USER_INPUT_TAG, notImplementedStub } from "../../../../xstate/utils"; -import { PaymentFailure } from "../types/PaymentFailure"; +import { IDPayTransactionCode } from "../common/types"; +import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; import * as Context from "./context"; import * as Events from "./events"; -import * as Input from "./input"; export const idPayPaymentMachine = setup({ types: { - input: {} as Input.Input, context: {} as Context.Context, events: {} as Events.Events }, actors: { - onInit: fromPromise(({ input }) => - Input.Input(input) - ), preAuthorizePayment: fromPromise( notImplementedStub ), @@ -35,12 +32,20 @@ export const idPayPaymentMachine = setup({ navigateToAuthorizationScreen: notImplementedStub, navigateToResultScreen: notImplementedStub, closeAuthorization: notImplementedStub, - setFailure: notImplementedStub, - showErrorToast: notImplementedStub + showErrorToast: notImplementedStub, + setFailure: (_ctx, _params: { data: any }) => notImplementedStub() }, guards: { - isSessionExpired: () => false, - isBlockingFailure: () => false + isSessionExpired: ({ context }) => + pipe( + context.failure, + O.map(failure => failure === PaymentFailureEnum.SESSION_EXPIRED), + O.getOrElse(() => false) + ), + asserTransactionCode: ({ event }) => { + assertEvent(event, "authorize-payment"); + return pipe(event.trxCode, IDPayTransactionCode.decode, E.isRight); + } } }).createMachine({ context: Context.Context, @@ -51,13 +56,14 @@ export const idPayPaymentMachine = setup({ tags: [LOADING_TAG], on: { "authorize-payment": { - target: "PreAuthorizing" + guard: "asserTransactionCode", + target: "PreAuthorizing", + actions: assign(({ event }) => ({ trxCode: event.trxCode })) } } }, PreAuthorizing: { - tags: [LOADING_TAG], - entry: "navigateToAuthorizationScreen", + tags: [UPSERTING_TAG], invoke: { id: "preAuthorizePayment", src: "preAuthorizePayment", @@ -73,7 +79,7 @@ export const idPayPaymentMachine = setup({ }, onError: { actions: assign(({ event }) => ({ - failure: pipe(PaymentFailure.decode(event.error), O.fromEither) + failure: decodeFailure(event.error) })), target: "AuthorizationFailure" } @@ -82,6 +88,7 @@ export const idPayPaymentMachine = setup({ AwaitingConfirmation: { tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToAuthorizationScreen", on: { next: { target: "Authorizing" @@ -103,20 +110,16 @@ export const idPayPaymentMachine = setup({ }, onError: [ { + guard: ({ event }) => isBlockingFalure(event.error), actions: assign(({ event }) => ({ - failure: pipe(PaymentFailure.decode(event.error), O.fromEither) - })) + failure: decodeFailure(event.error) + })), + target: "AuthorizationFailure" }, - [ - { - guard: "isBlockingFailure", - target: "AuthorizationFailure" - }, - { - actions: "showErrorToast", - target: "AwaitingConfirmation" - } - ] + { + actions: "showErrorToast", + target: "AwaitingConfirmation" + } ] } }, @@ -132,20 +135,16 @@ export const idPayPaymentMachine = setup({ }, onError: [ { + guard: ({ event }) => isBlockingFalure(event.error), actions: assign(({ event }) => ({ - failure: pipe(PaymentFailure.decode(event.error), O.fromEither) - })) + failure: decodeFailure(event.error) + })), + target: "AuthorizationFailure" }, - [ - { - guard: "isBlockingFailure", - target: "AuthorizationFailure" - }, - { - actions: "showErrorToast", - target: "AwaitingConfirmation" - } - ] + { + actions: "showErrorToast", + target: "AwaitingConfirmation" + } ] } }, @@ -186,3 +185,11 @@ export const idPayPaymentMachine = setup({ } } }); + +const decodeFailure = flow(PaymentFailure.decode, O.fromEither); + +const isBlockingFalure = flow( + decodeFailure, + O.map(failure => failure !== PaymentFailureEnum.TOO_MANY_REQUESTS), + O.getOrElse(() => false) +); diff --git a/ts/features/idpay/payment/machine/provider.tsx b/ts/features/idpay/payment/machine/provider.tsx index 4a8328339ff..829653f7e4b 100644 --- a/ts/features/idpay/payment/machine/provider.tsx +++ b/ts/features/idpay/payment/machine/provider.tsx @@ -1,19 +1,8 @@ import { createActorContext } from "@xstate/react"; -import * as O from "fp-ts/lib/Option"; import React from "react"; -import { - idPayApiBaseUrl, - idPayApiUatBaseUrl, - idPayTestToken -} from "../../../../config"; -import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; -import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; -import { createIDPayClient } from "../../common/api/client"; -import { createActionsImplementation } from "./actions"; +import { useActionsImplementation } from "./actions"; +import { useActorsImplementation } from "./actors"; import { idPayPaymentMachine } from "./machine"; -import { createActorsImplementation } from "./actors"; type Props = { children: React.ReactNode; @@ -23,28 +12,8 @@ export const IdPayPaymentMachineContext = createActorContext(idPayPaymentMachine); export const IdPayPaymentMachineProvider = (props: Props) => { - const navigation = useIONavigation(); - const dispatch = useIODispatch(); - - const sessionInfo = useIOSelector(sessionInfoSelector); - const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - - if (O.isNone(sessionInfo)) { - throw new Error("Session info is undefined"); - } - - const { bpdToken } = sessionInfo.value; - - const IDPayPaymentClient = createIDPayClient( - isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl - ); - - const actors = createActorsImplementation( - IDPayPaymentClient, - idPayTestToken ?? bpdToken, - dispatch - ); - const actions = createActionsImplementation(navigation); + const actors = useActorsImplementation(); + const actions = useActionsImplementation(); const machine = idPayPaymentMachine.provide({ actors, diff --git a/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx index 3363334e64d..f57a96e9d5d 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx @@ -17,9 +17,9 @@ import { H1 } from "../../../../components/core/typography/H1"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { isUpseringSelector } from "../../../../xstate/selectors"; import { IDPayTransactionCode } from "../common/types"; import { IdPayPaymentMachineContext } from "../machine/provider"; -import { isLoadingSelector } from "../../../../xstate/selectors"; type InputState = { value?: string; @@ -36,7 +36,7 @@ const IDPayPaymentCodeInputScreen = () => { }); const isInputValid = pipe(inputState.code, O.map(E.isRight), O.toUndefined); - const isLoading = useSelector(isLoadingSelector); + const isUpserting = useSelector(isUpseringSelector); const navigateToPaymentAuthorization = () => pipe( @@ -89,9 +89,9 @@ const IDPayPaymentCodeInputScreen = () => { buttonProps: { label: I18n.t("idpay.payment.manualInput.button"), accessibilityLabel: I18n.t("idpay.payment.manualInput.button"), - disabled: !isInputValid, + disabled: !isInputValid || isUpserting, onPress: navigateToPaymentAuthorization, - loading: isLoading + loading: isUpserting } }} /> diff --git a/yarn.lock b/yarn.lock index f21a1889126..929ee0f1604 100644 --- a/yarn.lock +++ b/yarn.lock @@ -115,11 +115,6 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== -"@babel/compat-data@^7.20.5": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" - integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== - "@babel/compat-data@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" @@ -169,27 +164,6 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.12.10": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" - "@babel/core@^7.13.16": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" @@ -319,15 +293,6 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/generator@^7.20.7": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce" - integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== - dependencies: - "@babel/types" "^7.20.7" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - "@babel/generator@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" @@ -414,17 +379,6 @@ browserslist "^4.20.2" semver "^6.3.0" -"@babel/helper-compilation-targets@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-validator-option" "^7.18.6" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - "@babel/helper-compilation-targets@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" @@ -587,14 +541,6 @@ "@babel/template" "^7.18.6" "@babel/types" "^7.18.9" -"@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" @@ -785,20 +731,6 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helper-module-transforms@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.10" - "@babel/types" "^7.20.7" - "@babel/helper-module-transforms@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" @@ -969,13 +901,6 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - "@babel/helper-simple-access@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" @@ -1172,15 +1097,6 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helpers@^7.20.7": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2" - integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.13" - "@babel/types" "^7.20.7" - "@babel/helpers@^7.23.7": version "7.23.8" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.8.tgz#fc6b2d65b16847fd50adddbd4232c76378959e34" @@ -1260,7 +1176,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.7.tgz#6099720c8839ca865a2637e6c85852ead0bdb595" integrity sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA== -"@babel/parser@^7.18.10", "@babel/parser@^7.20.13", "@babel/parser@^7.20.7": +"@babel/parser@^7.18.10", "@babel/parser@^7.20.7": version "7.20.15" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== @@ -2397,7 +2313,7 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/template@^7.18.10", "@babel/template@^7.20.7": +"@babel/template@^7.18.10": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== @@ -2486,22 +2402,6 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" - integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.13" - "@babel/types" "^7.20.7" - debug "^4.1.0" - globals "^11.1.0" - "@babel/traverse@^7.23.7": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" @@ -2567,7 +2467,7 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7": +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== @@ -4528,23 +4428,6 @@ "@typescript-eslint/types" "5.9.1" eslint-visitor-keys "^3.0.0" -"@xstate/cli@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.3.3.tgz#dec938d9cb2a7b82cc6c698664ed6d8d28d1f110" - integrity sha512-yY0CyUP+ipeSKiklNAYfY51CZObVQ0y48TJHVPfxnfGbsqwj+6rZkC1mAg9yF/Zu56DCDZTtUBKmY4BsOVDLVw== - dependencies: - "@babel/core" "^7.12.10" - "@xstate/machine-extractor" "0.7.1" - "@xstate/tools-shared" "1.2.3" - chokidar "^3.5.3" - commander "^8.0.0" - xstate "^4.29.0" - -"@xstate/machine-extractor@0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.7.1.tgz#157d5083db3f116b7ae28b5b3aef8f457f052491" - integrity sha512-dQEt6enmHXtD93vDcMefhb5bh1zh0mLCRT8CvYJjCpTjaTth7sXqlU6ri1qP0HDR6IbU9s2/WVNw7Oy7O/Sqfg== - "@xstate/react@^4.0.3": version "4.1.1" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa" @@ -4553,13 +4436,6 @@ use-isomorphic-layout-effect "^1.1.2" use-sync-external-store "^1.2.0" -"@xstate/tools-shared@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@xstate/tools-shared/-/tools-shared-1.2.3.tgz#a2e19119a7a273bbbdd35adaf6ea52cb80add064" - integrity sha512-6CL2Tz2S0+FSSP+940G9glMHG8g326amsVoU+b5wkY2KsbPrGga53r25PjiiP7bGrNtf4SCSsbYW/NClrHmZGQ== - dependencies: - "@xstate/machine-extractor" "0.7.1" - "@yarnpkg/lockfile@^1.0.0", "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -4810,14 +4686,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - appdirsjs@^1.2.4: version "1.2.5" resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.5.tgz#c9888c8a0a908014533d5176ec56f1d5a8fd3700" @@ -5739,11 +5607,6 @@ before-after-hook@^2.0.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -5827,7 +5690,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: +braces@^3.0.1, braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -5935,16 +5798,6 @@ browserslist@^4.20.2: node-releases "^2.0.6" update-browserslist-db "^1.0.5" -browserslist@^4.21.3: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== - dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - browserslist@^4.22.2: version "4.22.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" @@ -6176,11 +6029,6 @@ caniuse-lite@^1.0.30001370: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz#8e73bc3d1a4c800beb39f3163bf0190d7e5d7672" integrity sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw== -caniuse-lite@^1.0.30001449: - version "1.0.30001450" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" - integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== - caniuse-lite@^1.0.30001565: version "1.0.30001579" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz#45c065216110f46d6274311a4b3fcf6278e0852a" @@ -6302,21 +6150,6 @@ child-process-promise@^2.2.0: node-version "^1.0.0" promise-polyfill "^6.0.1" -chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -6575,11 +6408,6 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^8.0.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - commander@^9.4.0: version "9.5.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" @@ -7694,11 +7522,6 @@ electron-to-chromium@^1.4.202: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.217.tgz#f1f51b319435f4c1587a850806a0dfebe9774598" integrity sha512-iX8GbAMij7cOtJPZo02CClpaPMWjvN5meqXiJXkBgwvraNWTNH0Z7F9tkznI34JRPtWASoPM/xWamq3oNb49GA== -electron-to-chromium@^1.4.284: - version "1.4.286" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz#0e039de59135f44ab9a8ec9025e53a9135eba11f" - integrity sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ== - electron-to-chromium@^1.4.601: version "1.4.639" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.639.tgz#c6f9cc685f9efb2980d2cfc95a27f8142c9adf28" @@ -8883,11 +8706,6 @@ fsevents@^2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - fsm-iterator@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fsm-iterator/-/fsm-iterator-1.1.0.tgz#337de45de19eb205788cf02e3a955ec206760dec" @@ -9080,7 +8898,7 @@ gitlab@^10.0.1: query-string "^6.8.2" universal-url "^2.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -9832,13 +9650,6 @@ is-bigint@^1.0.1: resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-boolean-object@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" @@ -9993,7 +9804,7 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -10980,7 +10791,7 @@ json5@^2.1.0, json5@^2.1.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== -json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: +json5@^2.2.1, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -12824,7 +12635,7 @@ node-releases@^2.0.2: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== -node-releases@^2.0.6, node-releases@^2.0.8: +node-releases@^2.0.6: version "2.0.10" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== @@ -12856,7 +12667,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -13615,7 +13426,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -14739,13 +14550,6 @@ readable-stream@^2.0.0, readable-stream@^2.3.3, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - readline-sync@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" @@ -16669,10 +16473,10 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@^5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== ua-parser-js@^0.7.18, ua-parser-js@^0.7.30: version "0.7.33" @@ -16892,14 +16696,6 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -17355,11 +17151,6 @@ xss@1.0.10: commander "^2.20.3" cssfilter "0.0.10" -xstate@^4.29.0: - version "4.35.4" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.35.4.tgz#87b2a45b6c7e84d820f56378408c6531ca5c4662" - integrity sha512-mqRBYHhljP1xIItI4xnSQNHEv6CKslSM1cOGmvhmxeoDPAZgNbhSUYAL5N6DZIxRfpYY+M+bSm3mUFHD63iuvg== - xstate@^5: version "5.11.0" resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.11.0.tgz#2155f750d9ff6c4d24b3a9aee620e7e6661dae0c" From 2bd8ceaf80e64e7f37ea18a48ee02e63ab662866 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Fri, 3 May 2024 17:54:52 +0200 Subject: [PATCH 11/31] chore: onboarding wip --- .../idpay/common/navigation/linking.ts | 10 ++- .../idpay/onboarding/machine/actions.ts | 24 +++--- .../idpay/onboarding/machine/events.ts | 9 +- ts/features/idpay/onboarding/machine/input.ts | 8 -- .../idpay/onboarding/machine/machine.ts | 38 ++++----- .../idpay/onboarding/machine/provider.tsx | 10 +-- .../idpay/onboarding/navigation/navigator.tsx | 85 ++++++++----------- .../idpay/onboarding/navigation/params.ts | 8 +- .../idpay/onboarding/navigation/routes.ts | 2 +- .../screens/InitiativeDetailsScreen.tsx | 24 +++++- ts/features/idpay/payment/machine/actions.ts | 11 ++- ts/features/idpay/payment/machine/actors.ts | 33 ++----- .../idpay/payment/machine/provider.tsx | 35 +++++++- ts/navigation/AuthenticatedStackNavigator.tsx | 2 +- ts/navigation/params/AppParamsList.ts | 2 +- .../playgrounds/IdPayOnboardingPlayground.tsx | 4 +- 16 files changed, 156 insertions(+), 149 deletions(-) delete mode 100644 ts/features/idpay/onboarding/machine/input.ts diff --git a/ts/features/idpay/common/navigation/linking.ts b/ts/features/idpay/common/navigation/linking.ts index a32ef367349..e6dcdb73249 100644 --- a/ts/features/idpay/common/navigation/linking.ts +++ b/ts/features/idpay/common/navigation/linking.ts @@ -8,8 +8,14 @@ export const idPayLinkingOptions: PathConfigMap = { /** * IDPay initiative onboarding */ - [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: { - path: "idpay/onboarding/:serviceId" + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN]: { + path: "idpay/onboarding", + screens: { + /** + * Handles ioit://idpay/onboarding/{initiativeId} + */ + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: "/:serviceId" + } }, /** * IDPay initiative details diff --git a/ts/features/idpay/onboarding/machine/actions.ts b/ts/features/idpay/onboarding/machine/actions.ts index 14c17634d9e..94fd474a2bb 100644 --- a/ts/features/idpay/onboarding/machine/actions.ts +++ b/ts/features/idpay/onboarding/machine/actions.ts @@ -8,20 +8,24 @@ import * as Context from "./context"; const createActionsImplementation = ( navigation: ReturnType ) => { - const navigateToInitiativeDetailsScreen = guardedNavigationAction(() => - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { - screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS - }) - ); + const navigateToInitiativeDetailsScreen = + guardedNavigationAction(({ context }) => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, + params: { + serviceId: context.serviceId + } + }) + ); const navigateToPdndCriteriaScreen = guardedNavigationAction(() => - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE }) ); const navigateToBoolSelfDeclarationListScreen = guardedNavigationAction(() => - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS }) ); @@ -29,7 +33,7 @@ const createActionsImplementation = ( const navigateToMultiSelfDeclarationListScreen = guardedNavigationAction(({ context }) => navigation.navigate({ - name: IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, + name: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, params: { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS }, @@ -38,12 +42,12 @@ const createActionsImplementation = ( ); const navigateToCompletionScreen = () => - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION }); const navigateToFailureScreen = () => - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_FAILURE }); diff --git a/ts/features/idpay/onboarding/machine/events.ts b/ts/features/idpay/onboarding/machine/events.ts index e67d436b205..8de876c9e9c 100644 --- a/ts/features/idpay/onboarding/machine/events.ts +++ b/ts/features/idpay/onboarding/machine/events.ts @@ -1,11 +1,10 @@ import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; import { GlobalEvents } from "../../../../xstate/types/events"; -import * as Input from "./input"; -export interface AutoInit { - readonly type: "xstate.init"; - readonly input: Input.Input; +export interface StartOnboarding { + readonly type: "start-onboarding"; + readonly serviceId: string; } export interface ToggleBoolCriteria { @@ -20,6 +19,6 @@ export interface SelectMultiConsent { export type Events = | GlobalEvents - | AutoInit + | StartOnboarding | SelectMultiConsent | ToggleBoolCriteria; diff --git a/ts/features/idpay/onboarding/machine/input.ts b/ts/features/idpay/onboarding/machine/input.ts deleted file mode 100644 index bc66c18a570..00000000000 --- a/ts/features/idpay/onboarding/machine/input.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Context from "./context"; - -export interface Input { - readonly serviceId: string; -} - -export const Input = (input: Input): Promise => - Promise.resolve({ ...Context.Context, ...input }); diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index c435ca29579..ea01f0dbfc7 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -12,7 +12,6 @@ import { import { OnboardingFailure } from "../types/OnboardingFailure"; import * as Context from "./context"; import * as Events from "./events"; -import * as Input from "./input"; import { getBooleanSelfDeclarationListFromContext, getMultiSelfDeclarationListFromContext @@ -20,7 +19,6 @@ import { export const idPayOnboardingMachine = setup({ types: { - input: {} as Input.Input, context: {} as Context.Context, events: {} as Events.Events }, @@ -35,9 +33,6 @@ export const idPayOnboardingMachine = setup({ closeOnboarding: notImplementedStub }, actors: { - onInit: fromPromise(({ input }) => - Input.Input(input) - ), getInitiativeInfo: fromPromise( notImplementedStub ), @@ -55,6 +50,10 @@ export const idPayOnboardingMachine = setup({ ) }, guards: { + assertServiceId: ({ event }) => { + assertEvent(event, "start-onboarding"); + return event.serviceId.length > 0; + }, isSessionExpired: () => false, hasPdndCriteria: ({ context }) => pipe( @@ -79,23 +78,6 @@ export const idPayOnboardingMachine = setup({ }).createMachine({ id: "idpay-onboarding", context: Context.Context, - invoke: { - src: "onInit", - input: ({ event }) => { - assertEvent(event, "xstate.init"); - return event.input; - }, - onError: { - actions: assign(({ event }) => ({ - failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) - })), - target: ".OnboardingFailure" - }, - onDone: { - actions: assign(event => ({ ...event.event.output })), - target: ".LoadingInitiative" - } - }, initial: "LoadingInitiative", on: { close: { @@ -103,6 +85,18 @@ export const idPayOnboardingMachine = setup({ } }, states: { + Idle: { + tags: [LOADING_TAG], + on: { + "start-onboarding": { + guard: "assertServiceId", + actions: assign(({ event }) => ({ + serviceId: event.serviceId + })), + target: "LoadingInitiative" + } + } + }, LoadingInitiative: { tags: [LOADING_TAG], entry: "navigateToInitiativeDetailsScreen", diff --git a/ts/features/idpay/onboarding/machine/provider.tsx b/ts/features/idpay/onboarding/machine/provider.tsx index 1956d155f41..24a09970e2f 100644 --- a/ts/features/idpay/onboarding/machine/provider.tsx +++ b/ts/features/idpay/onboarding/machine/provider.tsx @@ -19,21 +19,19 @@ import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; import { createActorsImplementation } from "./actors"; -import * as Input from "./input"; import { idPayOnboardingMachine } from "./machine"; type Props = { children: React.ReactNode; - input: Input.Input; }; export const IdPayOnboardingMachineContext = createActorContext( idPayOnboardingMachine ); -export const IdPayOnboardingMachineProvider = ({ children, input }: Props) => { +export const IdPayOnboardingMachineProvider = ({ children }: Props) => { const dispatch = useIODispatch(); - const rootNavigation = useIONavigation(); + const navigation = useIONavigation(); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); const preferredLanguageOption = useIOSelector(preferredLanguageSelector); @@ -58,7 +56,7 @@ export const IdPayOnboardingMachineProvider = ({ children, input }: Props) => { ); const actors = createActorsImplementation(client, token, language, dispatch); - const actions = createActionsImplementation(rootNavigation); + const actions = createActionsImplementation(navigation); const machine = idPayOnboardingMachine.provide({ actors, @@ -66,7 +64,7 @@ export const IdPayOnboardingMachineProvider = ({ children, input }: Props) => { }); return ( - + {children} ); diff --git a/ts/features/idpay/onboarding/navigation/navigator.tsx b/ts/features/idpay/onboarding/navigation/navigator.tsx index 4721a56da10..c2ec78740f9 100644 --- a/ts/features/idpay/onboarding/navigation/navigator.tsx +++ b/ts/features/idpay/onboarding/navigation/navigator.tsx @@ -1,4 +1,3 @@ -import { RouteProp, useRoute } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; import { isGestureEnabled } from "../../../../utils/navigation"; @@ -14,50 +13,40 @@ import { IdPayOnboardingRoutes } from "./routes"; const Stack = createStackNavigator(); -type IdPayOnboardingRouteProps = RouteProp< - IdPayOnboardingParamsList, - "IDPAY_ONBOARDING_NAVIGATOR" ->; - -export const IdPayOnboardingNavigator = () => { - const { params } = useRoute(); - const { serviceId } = params; - - return ( - - - - - - - - - - - ); -}; +export const IdPayOnboardingNavigator = () => ( + + + + + + + + + + +); diff --git a/ts/features/idpay/onboarding/navigation/params.ts b/ts/features/idpay/onboarding/navigation/params.ts index 3fb0cfa452a..24bb80e46f1 100644 --- a/ts/features/idpay/onboarding/navigation/params.ts +++ b/ts/features/idpay/onboarding/navigation/params.ts @@ -1,12 +1,8 @@ +import { InitiativeDetailsScreenParams } from "../screens/InitiativeDetailsScreen"; import { IdPayOnboardingRoutes } from "./routes"; -export type IdPayOnboardingNavigatorParams = { - serviceId: string; -}; - export type IdPayOnboardingParamsList = { - [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: IdPayOnboardingNavigatorParams; - [IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: undefined; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: InitiativeDetailsScreenParams; [IdPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS]: undefined; [IdPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE]: undefined; [IdPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION]: undefined; diff --git a/ts/features/idpay/onboarding/navigation/routes.ts b/ts/features/idpay/onboarding/navigation/routes.ts index e66411a932f..bb9145b659b 100644 --- a/ts/features/idpay/onboarding/navigation/routes.ts +++ b/ts/features/idpay/onboarding/navigation/routes.ts @@ -1,5 +1,5 @@ export const IdPayOnboardingRoutes = { - IDPAY_ONBOARDING_NAVIGATOR: "IDPAY_ONBOARDING_NAVIGATOR", + IDPAY_ONBOARDING_MAIN: "IDPAY_ONBOARDING_MAIN", IDPAY_ONBOARDING_INITIATIVE_DETAILS: "IDPAY_ONBOARDING_INITIATIVE_DETAILS", IDPAY_ONBOARDING_PDNDACCEPTANCE: "IDPAY_ONBOARDING_PDNDACCEPTANCE", IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS: "IDPAY_ONBOARDING_SELF_DECLARATIONS", diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 73a7af467a8..300216a9f39 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -1,5 +1,6 @@ /* eslint-disable functional/immutable-data */ import { VSpacer } from "@pagopa/io-app-design-system"; +import { RouteProp, useRoute } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import * as React from "react"; @@ -10,6 +11,7 @@ import BaseScreenComponent from "../../../../components/screens/BaseScreenCompon import BlockButtons from "../../../../components/ui/BlockButtons"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { isLoadingSelector } from "../../../../xstate/selectors"; import { OnboardingDescriptionMarkdown, OnboardingDescriptionMarkdownSkeleton @@ -18,9 +20,20 @@ import { OnboardingPrivacyAdvice } from "../components/OnboardingPrivacyAdvice"; import { OnboardingServiceHeader } from "../components/OnboardingServiceHeader"; import { IdPayOnboardingMachineContext } from "../machine/provider"; import { selectInitiative } from "../machine/selectors"; -import { isLoadingSelector } from "../../../../xstate/selectors"; +import { IdPayOnboardingParamsList } from "../navigation/params"; + +export type InitiativeDetailsScreenParams = { + serviceId: string | undefined; +}; + +type InitiativeDetailsScreenParamsRouteProps = RouteProp< + IdPayOnboardingParamsList, + "IDPAY_ONBOARDING_INITIATIVE_DETAILS" +>; export const InitiativeDetailsScreen = () => { + const { params } = useRoute(); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; const machine = useActorRef(); @@ -31,6 +44,15 @@ export const InitiativeDetailsScreen = () => { const handleGoBackPress = () => machine.send({ type: "close" }); const handleContinuePress = () => machine.send({ type: "next" }); + React.useEffect(() => { + if (params.serviceId) { + machine.send({ + type: "start-onboarding", + serviceId: params.serviceId + }); + } + }, [machine, params]); + const onboardingPrivacyAdvice = pipe( initiative, O.map(initiative => ({ diff --git a/ts/features/idpay/payment/machine/actions.ts b/ts/features/idpay/payment/machine/actions.ts index c627b3ec261..82a54bc9c56 100644 --- a/ts/features/idpay/payment/machine/actions.ts +++ b/ts/features/idpay/payment/machine/actions.ts @@ -1,12 +1,11 @@ -import { useIOToast } from "@pagopa/io-app-design-system"; +import { IOToast } from "@pagopa/io-app-design-system"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { IdPayPaymentRoutes } from "../navigation/routes"; -export const useActionsImplementation = () => { - const navigation = useIONavigation(); - const toast = useIOToast(); - +export const createActionsImplementation = ( + navigation: ReturnType +) => { const navigateToAuthorizationScreen = () => { navigation.navigate(IdPayPaymentRoutes.IDPAY_PAYMENT_MAIN, { screen: IdPayPaymentRoutes.IDPAY_PAYMENT_AUTHORIZATION, @@ -20,7 +19,7 @@ export const useActionsImplementation = () => { }); const showErrorToast = () => - toast.error(I18n.t("idpay.payment.authorization.error")); + IOToast.error(I18n.t("idpay.payment.authorization.error")); const closeAuthorization = () => { navigation.pop(); diff --git a/ts/features/idpay/payment/machine/actors.ts b/ts/features/idpay/payment/machine/actors.ts index 17666ded47e..2606d6aeeb7 100644 --- a/ts/features/idpay/payment/machine/actors.ts +++ b/ts/features/idpay/payment/machine/actors.ts @@ -1,38 +1,19 @@ import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { flow, pipe } from "fp-ts/lib/function"; import { fromPromise } from "xstate"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; import { CodeEnum as TransactionErrorCodeEnum } from "../../../../../definitions/idpay/TransactionErrorDTO"; -import { - idPayApiBaseUrl, - idPayApiUatBaseUrl, - idPayTestToken -} from "../../../../config"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; -import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; +import { useIODispatch } from "../../../../store/hooks"; import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; -import { createIDPayClient } from "../../common/api/client"; +import { IDPayClient } from "../../common/api/client"; import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; -export const useActorsImplementation = () => { - const dispatch = useIODispatch(); - const sessionInfo = useIOSelector(sessionInfoSelector); - const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - - if (O.isNone(sessionInfo)) { - throw new Error("Session info is undefined"); - } - - const { bpdToken } = sessionInfo.value; - const token = idPayTestToken ?? bpdToken; - - const client = createIDPayClient( - isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl - ); - +export const createActorsImplementation = ( + client: IDPayClient, + token: string, + dispatch: ReturnType +) => { const handleSessionExpired = () => { dispatch( refreshSessionToken.request({ diff --git a/ts/features/idpay/payment/machine/provider.tsx b/ts/features/idpay/payment/machine/provider.tsx index 829653f7e4b..8447fe219d9 100644 --- a/ts/features/idpay/payment/machine/provider.tsx +++ b/ts/features/idpay/payment/machine/provider.tsx @@ -1,7 +1,18 @@ import { createActorContext } from "@xstate/react"; +import * as O from "fp-ts/lib/Option"; import React from "react"; -import { useActionsImplementation } from "./actions"; -import { useActorsImplementation } from "./actors"; +import { + idPayApiBaseUrl, + idPayApiUatBaseUrl, + idPayTestToken +} from "../../../../config"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { sessionInfoSelector } from "../../../../store/reducers/authentication"; +import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; +import { createIDPayClient } from "../../common/api/client"; +import { createActionsImplementation } from "./actions"; +import { createActorsImplementation } from "./actors"; import { idPayPaymentMachine } from "./machine"; type Props = { @@ -12,8 +23,24 @@ export const IdPayPaymentMachineContext = createActorContext(idPayPaymentMachine); export const IdPayPaymentMachineProvider = (props: Props) => { - const actors = useActorsImplementation(); - const actions = useActionsImplementation(); + const dispatch = useIODispatch(); + const navigation = useIONavigation(); + const sessionInfo = useIOSelector(sessionInfoSelector); + const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); + + if (O.isNone(sessionInfo)) { + throw new Error("Session info is undefined"); + } + + const { bpdToken } = sessionInfo.value; + const token = idPayTestToken ?? bpdToken; + + const idPayClient = createIDPayClient( + isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl + ); + + const actors = createActorsImplementation(idPayClient, token, dispatch); + const actions = createActionsImplementation(navigation); const machine = idPayPaymentMachine.provide({ actors, diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index 635fa577e3a..9f607e60d7d 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -240,7 +240,7 @@ const AuthenticatedStackNavigator = () => { {isIdPayEnabled && ( <> diff --git a/ts/navigation/params/AppParamsList.ts b/ts/navigation/params/AppParamsList.ts index fe0e8f52e19..fb0ca8ccfa6 100644 --- a/ts/navigation/params/AppParamsList.ts +++ b/ts/navigation/params/AppParamsList.ts @@ -93,7 +93,7 @@ export type AppParamsList = { [FIMS_ROUTES.MAIN]: NavigatorScreenParams; [FCI_ROUTES.MAIN]: NavigatorScreenParams; - [IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR]: NavigatorScreenParams; + [IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN]: NavigatorScreenParams; [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR]: NavigatorScreenParams; [IDPayDetailsRoutes.IDPAY_DETAILS_MAIN]: NavigatorScreenParams; [IdPayUnsubscriptionRoutes.IDPAY_UNSUBSCRIPTION_MAIN]: NavigatorScreenParams; diff --git a/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx b/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx index 6d593a5f949..097c08a4fc1 100644 --- a/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx +++ b/ts/screens/profile/playgrounds/IdPayOnboardingPlayground.tsx @@ -27,8 +27,8 @@ const IdPayOnboardingPlayground = () => { const [serviceId, setServiceId] = React.useState(); const navigateToIDPayOnboarding = (serviceId: string) => { - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, { - screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_NAVIGATOR, + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, params: { serviceId } From b7279f53135056a09edd8f1b52b5e0ee2bbedda9 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Sat, 4 May 2024 10:49:27 +0200 Subject: [PATCH 12/31] chore: fixed onboarding --- .../idpay/onboarding/machine/actions.ts | 25 +-- .../idpay/onboarding/machine/actors.ts | 108 +++++++------ .../idpay/onboarding/machine/machine.ts | 42 ++++- .../idpay/onboarding/machine/selectors.ts | 25 +-- .../screens/BoolValuePrerequisitesScreen.tsx | 2 +- .../screens/InitiativeDetailsScreen.tsx | 29 ++-- .../screens/MultiValuePrerequisitesScreen.tsx | 144 +++++++++++------- 7 files changed, 202 insertions(+), 173 deletions(-) diff --git a/ts/features/idpay/onboarding/machine/actions.ts b/ts/features/idpay/onboarding/machine/actions.ts index 94fd474a2bb..d797bfeb23f 100644 --- a/ts/features/idpay/onboarding/machine/actions.ts +++ b/ts/features/idpay/onboarding/machine/actions.ts @@ -5,16 +5,14 @@ import { IDPayDetailsRoutes } from "../../details/navigation"; import { IdPayOnboardingRoutes } from "../navigation/routes"; import * as Context from "./context"; -const createActionsImplementation = ( +export const createActionsImplementation = ( navigation: ReturnType ) => { const navigateToInitiativeDetailsScreen = - guardedNavigationAction(({ context }) => + guardedNavigationAction(() => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, - params: { - serviceId: context.serviceId - } + params: {} }) ); @@ -30,16 +28,11 @@ const createActionsImplementation = ( }) ); - const navigateToMultiSelfDeclarationListScreen = - guardedNavigationAction(({ context }) => - navigation.navigate({ - name: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, - params: { - screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS - }, - key: String(context.selfDeclarationsMultiPage) - }) - ); + const navigateToMultiSelfDeclarationListScreen = guardedNavigationAction(() => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS + }) + ); const navigateToCompletionScreen = () => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { @@ -83,5 +76,3 @@ const createActionsImplementation = ( closeOnboarding }; }; - -export { createActionsImplementation }; diff --git a/ts/features/idpay/onboarding/machine/actors.ts b/ts/features/idpay/onboarding/machine/actors.ts index b7dd2a32f7d..df8dbcb3b7a 100644 --- a/ts/features/idpay/onboarding/machine/actors.ts +++ b/ts/features/idpay/onboarding/machine/actors.ts @@ -19,60 +19,7 @@ import { import * as Context from "./context"; import { getBooleanSelfDeclarationListFromContext } from "./selectors"; -/** - * Maps the status of the initiative to a possibile UI failure state - * @param status The status of the initiative - * @returns A failure state enum, if any - */ -const mapOnboardingStatusToFailure = ( - status: OnboardingStatusEnum -): OnboardingFailure | undefined => { - switch (status) { - case OnboardingStatusEnum.ONBOARDING_OK: - case OnboardingStatusEnum.SUSPENDED: - return OnboardingFailureEnum.USER_ONBOARDED; - case OnboardingStatusEnum.ELIGIBLE_KO: - return OnboardingFailureEnum.NOT_ELIGIBLE; - case OnboardingStatusEnum.ON_EVALUATION: - return OnboardingFailureEnum.ON_EVALUATION; - case OnboardingStatusEnum.UNSUBSCRIBED: - return OnboardingFailureEnum.USER_UNSUBSCRIBED; - case OnboardingStatusEnum.ONBOARDING_KO: - return OnboardingFailureEnum.GENERIC; - default: - return undefined; - } -}; - -/** - * Maps the backed error codes to UI failure states - * @param code Error code from backend - * @returns The associated failure state - */ -const mapErrorCodeToFailure = ( - code: OnboardingErrorCodeEnum -): OnboardingFailure => { - switch (code) { - case OnboardingErrorCodeEnum.ONBOARDING_INITIATIVE_NOT_FOUND: - return OnboardingFailureEnum.INITIATIVE_NOT_FOUND; - case OnboardingErrorCodeEnum.ONBOARDING_UNSATISFIED_REQUIREMENTS: - return OnboardingFailureEnum.UNSATISFIED_REQUIREMENTS; - case OnboardingErrorCodeEnum.ONBOARDING_USER_NOT_IN_WHITELIST: - return OnboardingFailureEnum.USER_NOT_IN_WHITELIST; - case OnboardingErrorCodeEnum.ONBOARDING_INITIATIVE_NOT_STARTED: - return OnboardingFailureEnum.INITIATIVE_NOT_STARTED; - case OnboardingErrorCodeEnum.ONBOARDING_INITIATIVE_ENDED: - return OnboardingFailureEnum.INITIATIVE_ENDED; - case OnboardingErrorCodeEnum.ONBOARDING_BUDGET_EXHAUSTED: - return OnboardingFailureEnum.BUDGET_EXHAUSTED; - case OnboardingErrorCodeEnum.ONBOARDING_USER_UNSUBSCRIBED: - return OnboardingFailureEnum.USER_UNSUBSCRIBED; - default: - return OnboardingFailureEnum.GENERIC; - } -}; - -const createActorsImplementation = ( +export const createActorsImplementation = ( client: IDPayClient, token: string, language: PreferredLanguage, @@ -304,4 +251,55 @@ const createActorsImplementation = ( }; }; -export { createActorsImplementation }; +/** + * Maps the status of the initiative to a possibile UI failure state + * @param status The status of the initiative + * @returns A failure state enum, if any + */ +const mapOnboardingStatusToFailure = ( + status: OnboardingStatusEnum +): OnboardingFailure | undefined => { + switch (status) { + case OnboardingStatusEnum.ONBOARDING_OK: + case OnboardingStatusEnum.SUSPENDED: + return OnboardingFailureEnum.USER_ONBOARDED; + case OnboardingStatusEnum.ELIGIBLE_KO: + return OnboardingFailureEnum.NOT_ELIGIBLE; + case OnboardingStatusEnum.ON_EVALUATION: + return OnboardingFailureEnum.ON_EVALUATION; + case OnboardingStatusEnum.UNSUBSCRIBED: + return OnboardingFailureEnum.USER_UNSUBSCRIBED; + case OnboardingStatusEnum.ONBOARDING_KO: + return OnboardingFailureEnum.GENERIC; + default: + return undefined; + } +}; + +/** + * Maps the backed error codes to UI failure states + * @param code Error code from backend + * @returns The associated failure state + */ +const mapErrorCodeToFailure = ( + code: OnboardingErrorCodeEnum +): OnboardingFailure => { + switch (code) { + case OnboardingErrorCodeEnum.ONBOARDING_INITIATIVE_NOT_FOUND: + return OnboardingFailureEnum.INITIATIVE_NOT_FOUND; + case OnboardingErrorCodeEnum.ONBOARDING_UNSATISFIED_REQUIREMENTS: + return OnboardingFailureEnum.UNSATISFIED_REQUIREMENTS; + case OnboardingErrorCodeEnum.ONBOARDING_USER_NOT_IN_WHITELIST: + return OnboardingFailureEnum.USER_NOT_IN_WHITELIST; + case OnboardingErrorCodeEnum.ONBOARDING_INITIATIVE_NOT_STARTED: + return OnboardingFailureEnum.INITIATIVE_NOT_STARTED; + case OnboardingErrorCodeEnum.ONBOARDING_INITIATIVE_ENDED: + return OnboardingFailureEnum.INITIATIVE_ENDED; + case OnboardingErrorCodeEnum.ONBOARDING_BUDGET_EXHAUSTED: + return OnboardingFailureEnum.BUDGET_EXHAUSTED; + case OnboardingErrorCodeEnum.ONBOARDING_USER_UNSUBSCRIBED: + return OnboardingFailureEnum.USER_UNSUBSCRIBED; + default: + return OnboardingFailureEnum.GENERIC; + } +}; diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index ea01f0dbfc7..32695780a83 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -1,6 +1,6 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { assertEvent, assign, fromPromise, setup } from "xstate"; +import { and, assertEvent, assign, fromPromise, setup } from "xstate"; import { InitiativeDataDTO } from "../../../../../definitions/idpay/InitiativeDataDTO"; import { StatusEnum as OnboardingStatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; @@ -71,6 +71,8 @@ export const idPayOnboardingMachine = setup({ getBooleanSelfDeclarationListFromContext(context).length > 0, hasMultiSelfDeclarationList: ({ context }) => getMultiSelfDeclarationListFromContext(context).length > 0, + isFirstMultiConsentPage: ({ context }) => + context.selfDeclarationsMultiPage === 0, isLastMultiConsent: ({ context }) => context.selfDeclarationsMultiPage >= getMultiSelfDeclarationListFromContext(context).length - 1 @@ -78,7 +80,7 @@ export const idPayOnboardingMachine = setup({ }).createMachine({ id: "idpay-onboarding", context: Context.Context, - initial: "LoadingInitiative", + initial: "Idle", on: { close: { actions: "closeOnboarding" @@ -116,6 +118,7 @@ export const idPayOnboardingMachine = setup({ }, LoadingOnboardingStatus: { + type: "final", invoke: { src: "getOnboardingStatus", input: ({ context }) => selectInitiativeId(context), @@ -277,13 +280,36 @@ export const idPayOnboardingMachine = setup({ DisplayingMultiSelfDeclarationItem: { entry: "navigateToMultiSelfDeclarationListScreen", on: { - "select-multi-consent": [ + "select-multi-consent": { + actions: assign(({ context, event }) => ({ + selfDeclarationsMultiAnwsers: { + ...context.selfDeclarationsMultiAnwsers, + [context.selfDeclarationsMultiPage]: event.data + } + })), + target: "EvaluatingMultiSelfDeclarationList" + }, + back: [ + { + guard: and([ + "isFirstMultiConsentPage", + "hasBooleanSelfDeclarationList" + ]), + target: + "#idpay-onboarding.DisplayingSelfDeclarationList.DisplayingBooleanSelfDeclarationList" + }, + { + guard: and(["isFirstMultiConsentPage", "hasPdndCriteria"]), + target: "#idpay-onboarding.DisplayingPdndCriteria" + }, + { + guard: "isFirstMultiConsentPage", + target: "#idpay-onboarding.DisplayingInitiativeInfo" + }, { - actions: assign(({ context, event }) => ({ - selfDeclarationsMultiAnwsers: { - ...context.selfDeclarationsMultiAnwsers, - [context.selfDeclarationsMultiPage]: event.data - } + actions: assign(({ context }) => ({ + selfDeclarationsMultiPage: + +context.selfDeclarationsMultiPage - 1 })) } ] diff --git a/ts/features/idpay/onboarding/machine/selectors.ts b/ts/features/idpay/onboarding/machine/selectors.ts index efdedfb8ec6..ffc040de068 100644 --- a/ts/features/idpay/onboarding/machine/selectors.ts +++ b/ts/features/idpay/onboarding/machine/selectors.ts @@ -15,7 +15,7 @@ type MachineSnapshot = SnapshotFrom; export const selectOnboardingFailure = (snapshot: MachineSnapshot) => snapshot.context.failure; -const selectRequiredCriteria = (snapshot: MachineSnapshot) => +export const selectRequiredCriteria = (snapshot: MachineSnapshot) => snapshot.context.requiredCriteria; export const selectSelfDeclarationBoolAnswers = (snapshot: MachineSnapshot) => @@ -24,8 +24,9 @@ export const selectSelfDeclarationBoolAnswers = (snapshot: MachineSnapshot) => const selectMultiConsents = (snapshot: MachineSnapshot) => snapshot.context.selfDeclarationsMultiAnwsers; -const selectCurrentPage = (snapshot: MachineSnapshot) => - snapshot.context.selfDeclarationsMultiPage; +export const selectCurrentMultiSelfDeclarationPage = ( + snapshot: MachineSnapshot +) => snapshot.context.selfDeclarationsMultiPage; export const selectInitiative = (snapshot: MachineSnapshot) => snapshot.context.initiative; @@ -45,7 +46,7 @@ const filterCriteria = ( ) ) as Array; -const multiRequiredCriteriaSelector = createSelector( +export const multiRequiredCriteriaSelector = createSelector( selectRequiredCriteria, requiredCriteria => filterCriteria( @@ -63,12 +64,6 @@ export const boolRequiredCriteriaSelector = createSelector( ) ); -export const criteriaToDisplaySelector = createSelector( - multiRequiredCriteriaSelector, - selectCurrentPage, - (criteria, currentPage) => criteria[currentPage] -); - export const pdndCriteriaSelector = createSelector( selectRequiredCriteria, requiredCriteria => @@ -81,16 +76,6 @@ export const pdndCriteriaSelector = createSelector( ) ); -export const prerequisiteAnswerIndexSelector = createSelector( - criteriaToDisplaySelector, - selectMultiConsents, - selectCurrentPage, - (currentCriteria, multiConsents, currentPage) => - multiConsents[currentPage]?.value === undefined - ? undefined - : currentCriteria.value.indexOf(multiConsents[currentPage]?.value) -); - export const getMultiSelfDeclarationListFromContext = ( context: Context.Context ) => diff --git a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx index ca2b7c57520..501c448a1e6 100644 --- a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx @@ -16,13 +16,13 @@ import I18n from "../../../../i18n"; import { dpr28Dec2000Url } from "../../../../urls"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { openWebUrl } from "../../../../utils/url"; +import { isLoadingSelector } from "../../../../xstate/selectors"; import { IdPayOnboardingMachineContext } from "../machine/provider"; import { areAllSelfDeclarationsToggledSelector, boolRequiredCriteriaSelector, selectSelfDeclarationBoolAnswers } from "../machine/selectors"; -import { isLoadingSelector } from "../../../../xstate/selectors"; const InitiativeSelfDeclarationsScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 300216a9f39..59f58a1b4cf 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -23,7 +23,7 @@ import { selectInitiative } from "../machine/selectors"; import { IdPayOnboardingParamsList } from "../navigation/params"; export type InitiativeDetailsScreenParams = { - serviceId: string | undefined; + serviceId?: string; }; type InitiativeDetailsScreenParamsRouteProps = RouteProp< @@ -37,15 +37,8 @@ export const InitiativeDetailsScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; const machine = useActorRef(); - const initiative = useSelector(selectInitiative); - const isLoading = useSelector(isLoadingSelector); - const [isDescriptionLoaded, setDescriptionLoaded] = React.useState(false); - - const handleGoBackPress = () => machine.send({ type: "close" }); - const handleContinuePress = () => machine.send({ type: "next" }); - React.useEffect(() => { - if (params.serviceId) { + if (params.serviceId !== undefined) { machine.send({ type: "start-onboarding", serviceId: params.serviceId @@ -53,15 +46,23 @@ export const InitiativeDetailsScreen = () => { } }, [machine, params]); + const initiative = useSelector(selectInitiative); + const isLoading = useSelector(isLoadingSelector); + const [isDescriptionLoaded, setDescriptionLoaded] = React.useState(false); + + const handleGoBackPress = () => machine.send({ type: "close" }); + const handleContinuePress = () => machine.send({ type: "next" }); + const onboardingPrivacyAdvice = pipe( initiative, - O.map(initiative => ({ - privacyUrl: initiative.privacyLink, - tosUrl: initiative.tcLink - })), O.fold( () => null, - props => + initiative => ( + + ) ) ); diff --git a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx index 6eadd11f62b..65b10837859 100644 --- a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx @@ -10,6 +10,8 @@ import { } from "@pagopa/io-app-design-system"; import React from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; +import PagerView from "react-native-pager-view"; +import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDeclarationMultiDTO"; import { H4 } from "../../../../components/core/typography/H4"; import { Link } from "../../../../components/core/typography/Link"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; @@ -18,8 +20,8 @@ import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationS import I18n from "../../../../i18n"; import { IdPayOnboardingMachineContext } from "../machine/provider"; import { - criteriaToDisplaySelector, - prerequisiteAnswerIndexSelector + multiRequiredCriteriaSelector, + selectCurrentMultiSelfDeclarationPage } from "../machine/selectors"; type ListItemProps = { @@ -28,69 +30,74 @@ type ListItemProps = { onPress: () => void; }; -const CustomListItem = ({ text, onPress, checked }: ListItemProps) => ( - - - { + const pagerRef = React.useRef(null); + const { useActorRef, useSelector } = IdPayOnboardingMachineContext; + const machine = useActorRef(); + + const multiSelfDeclarations = useSelector(multiRequiredCriteriaSelector); + const currentPage = useSelector(selectCurrentMultiSelfDeclarationPage); + + React.useEffect(() => { + pagerRef.current?.setPage(currentPage); + }, [pagerRef, currentPage]); + + useNavigationSwipeBackListener(() => { + machine.send({ type: "back", skipNavigation: true }); + }); + + return ( + + -

- {text} -

- -
-
-
-); + {multiSelfDeclarations.map((selfDelcaration, index) => ( + + + + ))} + +
+ ); +}; -const buttonProps = { - leftButton: { title: I18n.t("global.buttons.back"), bordered: true }, - rightButton: { - title: I18n.t("global.buttons.continue"), - bordered: false - } +type MultiValuePrerequisiteItemScreenContentProps = { + selfDeclaration: SelfDeclarationMultiDTO; }; -const MultiValuePrerequisitesScreen = () => { - const { useActorRef, useSelector } = IdPayOnboardingMachineContext; +const MultiValuePrerequisiteItemScreenContent = ({ + selfDeclaration +}: MultiValuePrerequisiteItemScreenContentProps) => { + const { useActorRef } = IdPayOnboardingMachineContext; const machine = useActorRef(); - const currentPrerequisite = useSelector(criteriaToDisplaySelector); - const possiblySelectedIndex = useSelector(prerequisiteAnswerIndexSelector); - const [selectedIndex, setSelectedIndex] = React.useState( - possiblySelectedIndex + undefined ); - const continueOnPress = () => { - if (selectedIndex === undefined) { - return null; + const handleContinuePress = () => { + if (selectedIndex !== undefined) { + machine.send({ + type: "select-multi-consent", + data: { + _type: selfDeclaration._type, + value: selfDeclaration.value[selectedIndex], + code: selfDeclaration.code + } + }); } - machine.send({ - type: "select-multi-consent", - data: { - _type: currentPrerequisite._type, - value: currentPrerequisite.value[selectedIndex], - code: currentPrerequisite.code - } - }); - - return null; }; - const goBack = () => machine.send({ type: "back" }); - - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); + const handleGoBack = () => machine.send({ type: "back" }); return ( - + <> @@ -99,9 +106,9 @@ const MultiValuePrerequisitesScreen = () => { {I18n.t("idpay.onboarding.multiPrerequisites.body")} {I18n.t("idpay.onboarding.multiPrerequisites.link")} -

{currentPrerequisite.description}

+

{selfDeclaration.description}

- {currentPrerequisite.value.map((answer, index) => ( + {selfDeclaration.value.map((answer, index) => ( { -
+ ); }; +const CustomListItem = ({ text, onPress, checked }: ListItemProps) => ( + + + +

+ {text} +

+ +
+
+
+); + const styles = StyleSheet.create({ outerListItem: { borderBottomWidth: 1, From 25a1aba05514b278dff404c1aab100227f1b2692 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Sat, 4 May 2024 10:52:31 +0200 Subject: [PATCH 13/31] fix: imports --- ts/features/idpay/onboarding/machine/selectors.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ts/features/idpay/onboarding/machine/selectors.ts b/ts/features/idpay/onboarding/machine/selectors.ts index ffc040de068..26db0b84d74 100644 --- a/ts/features/idpay/onboarding/machine/selectors.ts +++ b/ts/features/idpay/onboarding/machine/selectors.ts @@ -15,15 +15,12 @@ type MachineSnapshot = SnapshotFrom; export const selectOnboardingFailure = (snapshot: MachineSnapshot) => snapshot.context.failure; -export const selectRequiredCriteria = (snapshot: MachineSnapshot) => +const selectRequiredCriteria = (snapshot: MachineSnapshot) => snapshot.context.requiredCriteria; export const selectSelfDeclarationBoolAnswers = (snapshot: MachineSnapshot) => snapshot.context.selfDeclarationsBoolAnswers; -const selectMultiConsents = (snapshot: MachineSnapshot) => - snapshot.context.selfDeclarationsMultiAnwsers; - export const selectCurrentMultiSelfDeclarationPage = ( snapshot: MachineSnapshot ) => snapshot.context.selfDeclarationsMultiPage; From 64bd64515de052c0577971f94edd2664573203db Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Sat, 4 May 2024 11:00:34 +0200 Subject: [PATCH 14/31] fix: failures --- .../idpay/onboarding/machine/machine.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index 32695780a83..32de9b4ea73 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-identical-functions */ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { and, assertEvent, assign, fromPromise, setup } from "xstate"; @@ -113,6 +114,15 @@ export const idPayOnboardingMachine = setup({ initiative: O.some(event.output) })), target: "LoadingOnboardingStatus" + }, + onError: { + actions: assign(({ event }) => ({ + failure: pipe( + OnboardingFailure.decode(event.error), + O.fromEither + ) + })), + target: "#idpay-onboarding.OnboardingFailure" } } }, @@ -126,16 +136,20 @@ export const idPayOnboardingMachine = setup({ actions: assign(({ event }) => ({ onboardingStatus: event.output })) + }, + onError: { + actions: assign(({ event }) => ({ + failure: pipe( + OnboardingFailure.decode(event.error), + O.fromEither + ) + })), + target: "#idpay-onboarding.OnboardingFailure" } } } }, - onError: { - actions: assign(({ event }) => ({ - failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) - })), - target: "OnboardingFailure" - }, + onDone: { target: "DisplayingInitiativeInfo" } From 66aa54794922d5052e8d635d423d1274e842f900 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Sat, 4 May 2024 17:31:27 +0200 Subject: [PATCH 15/31] fix: configuration machine --- .../idpay/configuration/machine/actions.ts | 10 +- .../idpay/configuration/machine/actors.ts | 4 +- .../idpay/configuration/machine/events.ts | 11 +- .../idpay/configuration/machine/input.ts | 14 - .../idpay/configuration/machine/machine.ts | 367 ++++++++---------- .../idpay/configuration/machine/provider.tsx | 16 +- .../configuration/navigation/navigator.tsx | 103 +++-- .../idpay/configuration/navigation/params.ts | 16 +- .../screens/IbanEnrollmentScreen.tsx | 26 ++ .../screens/IbanOnboardingScreen.tsx | 62 ++- .../InitiativeConfigurationIntroScreen.tsx | 130 ++++--- .../screens/InstrumentsEnrollmentScreen.tsx | 27 ++ .../screens/IdPayInitiativeDetailsScreen.tsx | 2 +- .../idpay/onboarding/machine/machine.ts | 19 +- ts/features/idpay/payment/machine/machine.ts | 3 +- 15 files changed, 404 insertions(+), 406 deletions(-) delete mode 100644 ts/features/idpay/configuration/machine/input.ts diff --git a/ts/features/idpay/configuration/machine/actions.ts b/ts/features/idpay/configuration/machine/actions.ts index ade9e86b0d5..aa5ea1f18ea 100644 --- a/ts/features/idpay/configuration/machine/actions.ts +++ b/ts/features/idpay/configuration/machine/actions.ts @@ -17,7 +17,8 @@ const createActionsImplementation = ( navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { - screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, + params: {} } ); } @@ -27,7 +28,8 @@ const createActionsImplementation = ( navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { - screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, + params: {} } ) ); @@ -55,7 +57,8 @@ const createActionsImplementation = ( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { screen: - IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, + params: {} } ) ); @@ -88,6 +91,7 @@ const createActionsImplementation = ( args.context.failure, InitiativeFailure.decode, O.fromEither, + O.map(failure => I18n.t(`idpay.configuration.failureStates.${failure}`)), O.map(IOToast.error) ); }; diff --git a/ts/features/idpay/configuration/machine/actors.ts b/ts/features/idpay/configuration/machine/actors.ts index b36b3007b8d..1595c45176a 100644 --- a/ts/features/idpay/configuration/machine/actors.ts +++ b/ts/features/idpay/configuration/machine/actors.ts @@ -20,7 +20,7 @@ import { IDPayClient } from "../../common/api/client"; import { InitiativeFailureType } from "../types/failure"; import * as Events from "./events"; -const createServicesImplementation = ( +export const createActorsImplementation = ( idPayClient: IDPayClient, paymentManagerClient: PaymentManagerClient, pmSessionManager: SessionManager, @@ -339,5 +339,3 @@ const createServicesImplementation = ( instrumentsEnrollmentLogic }; }; - -export { createServicesImplementation }; diff --git a/ts/features/idpay/configuration/machine/events.ts b/ts/features/idpay/configuration/machine/events.ts index ff8626cf1f6..8c1ede58059 100644 --- a/ts/features/idpay/configuration/machine/events.ts +++ b/ts/features/idpay/configuration/machine/events.ts @@ -1,11 +1,12 @@ import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; import { GlobalEvents } from "../../../../xstate/types/events"; -import * as Input from "./input"; +import { ConfigurationMode } from "../types"; -export interface AutoInit { - readonly type: "xstate.init"; - readonly input: Input.Input; +export interface StartConfiguration { + readonly type: "start-configuration"; + readonly initiativeId: string; + readonly mode: ConfigurationMode; } export interface ConfirmIbanOnboarding { @@ -49,7 +50,7 @@ export interface SkipInstruments { } export type Events = - | AutoInit + | StartConfiguration | NewIbanOnboarding | ConfirmIbanOnboarding | EnrollIban diff --git a/ts/features/idpay/configuration/machine/input.ts b/ts/features/idpay/configuration/machine/input.ts deleted file mode 100644 index 9f9cfe0b526..00000000000 --- a/ts/features/idpay/configuration/machine/input.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ConfigurationMode } from "../types"; -import * as Context from "./context"; - -export interface Input { - readonly initiativeId: string; - readonly mode?: ConfigurationMode; -} - -export const Input = (input: Input): Promise => - Promise.resolve({ - ...Context.Context, - initiativeId: input.initiativeId, - mode: input.mode ?? ConfigurationMode.COMPLETE - }); diff --git a/ts/features/idpay/configuration/machine/machine.ts b/ts/features/idpay/configuration/machine/machine.ts index 5006e73702d..c83b617250e 100644 --- a/ts/features/idpay/configuration/machine/machine.ts +++ b/ts/features/idpay/configuration/machine/machine.ts @@ -1,7 +1,7 @@ /* eslint-disable sonarjs/no-identical-functions */ import * as pot from "@pagopa/ts-commons/lib/pot"; import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; +import { flow, pipe } from "fp-ts/lib/function"; import { assertEvent, assign, @@ -32,12 +32,10 @@ import { ConfigurationMode, InstrumentStatusByIdWallet } from "../types"; import { InitiativeFailure, InitiativeFailureType } from "../types/failure"; import * as Context from "./context"; import * as Events from "./events"; -import * as Input from "./input"; /** PLEASE DO NO USE AUTO-LAYOUT WHEN USING VISUAL EDITOR */ export const idPayConfigurationMachine = setup({ types: { - input: {} as Input.Input, context: {} as Context.Context, events: {} as Events.Events }, @@ -117,14 +115,10 @@ export const idPayConfigurationMachine = setup({ instrumentStatuses: updatedStatuses }; }), - handleSessionExpired: notImplementedStub, showFailureToast: notImplementedStub, exitConfiguration: notImplementedStub }, actors: { - onInit: fromPromise(({ input }) => - Input.Input(input) - ), getInitiative: fromPromise(notImplementedStub), getIbanList: fromPromise(notImplementedStub), getWalletInstruments: @@ -141,12 +135,6 @@ export const idPayConfigurationMachine = setup({ >(notImplementedStub) }, guards: { - isSessionExpired: ({ context }) => - pipe( - context.failure, - O.map(failure => failure === InitiativeFailureType.SESSION_EXPIRED), - O.getOrElse(() => false) - ), isInstrumentsOnlyMode: ({ context }) => context.mode === ConfigurationMode.INSTRUMENTS, isIbanOnlyMode: ({ context }) => context.mode === ConfigurationMode.IBAN, @@ -165,20 +153,6 @@ export const idPayConfigurationMachine = setup({ }).createMachine({ context: Context.Context, id: "idpay-configuration", - invoke: { - src: "onInit", - input: ({ event }) => { - assertEvent(event, "xstate.init"); - return event.input; - }, - onError: { - target: ".ConfigurationFailure" - }, - onDone: { - actions: assign(event => ({ ...event.event.output })), - target: ".LoadingInitiative" - } - }, initial: "Idle", on: { close: { @@ -187,7 +161,17 @@ export const idPayConfigurationMachine = setup({ }, states: { Idle: { - tags: [LOADING_TAG] + tags: [LOADING_TAG], + on: { + "start-configuration": { + guard: ({ event }) => event.initiativeId.length > 0, + actions: assign(({ event }) => ({ + initiativeId: event.initiativeId, + mode: event.mode + })), + target: "LoadingInitiative" + } + } }, LoadingInitiative: { @@ -245,86 +229,61 @@ export const idPayConfigurationMachine = setup({ ConfiguringIban: { id: "configuration-iban", initial: "LoadingIbanList", + entry: "navigateToIbanEnrollmentScreen", states: { LoadingIbanList: { tags: [LOADING_TAG], invoke: { src: "getIbanList", id: "getIbanList", - onDone: [ - { - actions: assign(({ event }) => ({ - ibanList: event.output.ibanList - })) - }, - [ - { - guard: "hasIbanList", - target: ".DisplayingIbanList" - }, - { - target: ".DisplayingIbanOnboarding" - } - ] - ], + onDone: { + actions: assign(({ event }) => ({ + ibanList: event.output.ibanList + })), + target: "EvaluatingIbanList" + }, onError: [ { + guard: "isIbanOnlyMode", actions: assign(({ event }) => ({ - failure: pipe( - InitiativeFailure.decode(event.error), - O.fromEither - ) - })) + failure: decodeFailure(event.error) + })), + target: "#idpay-configuration.ConfigurationFailure" }, - [ - { - guard: "isSessionExpired", - target: "SessionExpired" - }, - { - guard: "isIbanOnlyMode", - target: "#idpay-configuration.ConfigurationFailure" - }, - { - target: "#idpay-configuration.DisplayingConfigurationIntro", - actions: "showFailureToast" - } - ] + { + actions: [ + assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + "showFailureToast" + ], + target: "#idpay-configuration.DisplayingConfigurationIntro" + } ] } }, - DisplayingIbanList: { - tags: [WAITING_USER_INPUT_TAG], - entry: "navigateToIbanEnrollmentScreen", - on: { - back: [ - { - guard: "isIbanOnlyMode", - target: "#idpay-configuration.ConfigurationClosed" - }, - { - target: "#idpay-configuration.DisplayingConfigurationIntro" - } - ], - "new-iban-onboarding": { - target: "DisplayingIbanOnboarding" + EvaluatingIbanList: { + tags: [LOADING_TAG], + always: [ + { + guard: "hasIbanList", + target: "DisplayingIbanList" }, - "enroll-iban": { - target: "EnrollingIban" + { + target: "DisplayingIbanOnboardingLanding" } - } + ] }, - DisplayingIbanOnboarding: { + DisplayingIbanOnboardingLanding: { tags: [WAITING_USER_INPUT_TAG], entry: "navigateToIbanOnboardingScreen", on: { + next: { + target: "DisplayingIbanOnboardingForm" + }, back: [ - { - guard: "hasIbanList", - target: "DisplayingIbanList" - }, { guard: "isIbanOnlyMode", target: "#idpay-configuration.ConfigurationClosed" @@ -342,7 +301,11 @@ export const idPayConfigurationMachine = setup({ on: { back: [ { - target: "DisplayingIbanOnboarding" + guard: "hasIbanList", + target: "DisplayingIbanList" + }, + { + target: "DisplayingIbanOnboardingLanding" } ], "confirm-iban-onboarding": { @@ -366,26 +329,37 @@ export const idPayConfigurationMachine = setup({ onDone: { target: "IbanConfigurationCompleted" }, - onError: [ + onError: { + actions: [ + assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + "showFailureToast" + ], + target: "DisplayingIbanOnboardingForm" + } + } + }, + + DisplayingIbanList: { + tags: [WAITING_USER_INPUT_TAG], + entry: "navigateToIbanEnrollmentScreen", + on: { + back: [ { - actions: assign(({ event }) => ({ - failure: pipe( - InitiativeFailure.decode(event.error), - O.fromEither - ) - })) + guard: "isIbanOnlyMode", + target: "#idpay-configuration.ConfigurationClosed" }, - [ - { - guard: "isSessionExpired", - target: "#idpay-configuration.SessionExpired" - }, - { - target: "DisplayingIbanOnboardingForm", - actions: "showFailureToast" - } - ] - ] + { + target: "#idpay-configuration.DisplayingConfigurationIntro" + } + ], + "new-iban-onboarding": { + target: "DisplayingIbanOnboardingForm" + }, + "enroll-iban": { + target: "EnrollingIban" + } } }, @@ -412,10 +386,6 @@ export const idPayConfigurationMachine = setup({ } ], onError: [ - { - guard: "isSessionExpired", - target: "#idpay-configuration.SessionExpired" - }, { target: "DisplayingIbanList", actions: "showFailureToast" @@ -449,78 +419,82 @@ export const idPayConfigurationMachine = setup({ type: "parallel", states: { LoadingWalletInstruments: { - initial: "LOADING", - invoke: { - src: "getWalletInstruments", - id: "getWalletInstruments", - input: ({ context }) => context.initiativeId, - onDone: { - actions: assign(({ event }) => ({ - walletInstruments: event.output - })) - }, - onError: [ - { - actions: assign(({ event }) => ({ - failure: pipe( - InitiativeFailure.decode(event.error), - O.fromEither - ) - })) - }, - [ - { - guard: "isSessionExpired", - target: "#idpay-configuration.SessionExpired" + initial: "Loading", + states: { + Loading: { + invoke: { + src: "getWalletInstruments", + id: "getWalletInstruments", + input: ({ context }) => context.initiativeId, + onDone: { + actions: assign(({ event }) => ({ + walletInstruments: event.output + })), + target: "Success" }, - { - guard: "isInstrumentsOnlyMode", - target: "#idpay-configuration.ConfigurationFailure" - }, - { - target: "#idpay-configuration.ConfiguringIban", - actions: "showFailureToast" - } - ] - ] + onError: [ + { + guard: "isInstrumentsOnlyMode", + actions: assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + target: "#idpay-configuration.ConfigurationFailure" + }, + { + actions: [ + assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + "showFailureToast" + ], + target: "#idpay-configuration.ConfiguringIban" + } + ] + } + }, + Success: { + type: "final" + } } }, LoadingInitiativeInstruments: { - initial: "LOADING", - invoke: { - src: "getInitiativeInstruments", - id: "getInitiativeInstruments", - input: ({ context }) => context.initiativeId, - onDone: { - actions: assign(({ event }) => ({ - initiativeInstruments: event.output - })) - }, - onError: [ - { - actions: assign(({ event }) => ({ - failure: pipe( - InitiativeFailure.decode(event.error), - O.fromEither - ) - })) - }, - [ - { - guard: "isSessionExpired", - target: "#idpay-configuration.SessionExpired" + initial: "Loading", + states: { + Loading: { + invoke: { + src: "getInitiativeInstruments", + id: "getInitiativeInstruments", + input: ({ context }) => context.initiativeId, + onDone: { + actions: assign(({ event }) => ({ + initiativeInstruments: event.output + })), + target: "Success" }, - { - guard: "isInstrumentsOnlyMode", - target: "#idpay-configuration.ConfigurationFailure" - }, - { - target: "#idpay-configuration.ConfiguringIban", - actions: "showFailureToast" - } - ] - ] + onError: [ + { + guard: "isInstrumentsOnlyMode", + actions: assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + target: "#idpay-configuration.ConfigurationFailure" + }, + { + actions: [ + assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + "showFailureToast" + ], + target: "#idpay-configuration.ConfiguringIban" + } + ] + } + }, + Success: { + type: "final" + } } } }, @@ -542,7 +516,7 @@ export const idPayConfigurationMachine = setup({ DisplayingInstruments: { tags: [WAITING_USER_INPUT_TAG], entry: "updateAllInstrumentsStatus", - initial: "DISPLAYING", + initial: "DisplayingInstrument", invoke: { id: "instrumentsEnrollment", src: "instrumentsEnrollmentLogic", @@ -614,23 +588,14 @@ export const idPayConfigurationMachine = setup({ }, onError: [ { - actions: assign(({ event }) => ({ - failure: pipe( - InitiativeFailure.decode(event.error), - O.fromEither - ) - })) - }, - [ - { - guard: "isSessionExpired", - target: "#idpay-configuration.SessionExpired" - }, - { - target: "DisplayingInstrument", - actions: "showFailureToast" - } - ] + target: "DisplayingInstrument", + actions: [ + assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + "showFailureToast" + ] + } ] } } @@ -685,15 +650,17 @@ export const idPayConfigurationMachine = setup({ ConfigurationFailure: { type: "final", always: { - guard: "isSessionExpired", - target: "#idpay-configuration.SessionExpired" + guard: ({ context }) => + pipe( + context.failure, + O.map(f => f === InitiativeFailureType.SESSION_EXPIRED), + O.getOrElse(() => false) + ), + actions: "exitConfiguration" }, entry: ["showFailureToast", "exitConfiguration"] - }, - - SessionExpired: { - type: "final", - entry: ["handleSessionExpired", "exitConfiguration"] } } }); + +const decodeFailure = flow(InitiativeFailure.decode, O.fromEither); diff --git a/ts/features/idpay/configuration/machine/provider.tsx b/ts/features/idpay/configuration/machine/provider.tsx index 3db13a9c33a..e3abb176329 100644 --- a/ts/features/idpay/configuration/machine/provider.tsx +++ b/ts/features/idpay/configuration/machine/provider.tsx @@ -25,23 +25,18 @@ import { defaultRetryingFetch } from "../../../../utils/fetch"; import { fromLocaleToPreferredLanguage } from "../../../../utils/locale"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; -import { createServicesImplementation } from "./actors"; +import { createActorsImplementation } from "./actors"; import { idPayConfigurationMachine } from "./machine"; -import * as Input from "./input"; type Props = { children: React.ReactNode; - input: Input.Input; }; export const IdPayConfigurationMachineContext = createActorContext( idPayConfigurationMachine ); -export const IDPayConfigurationMachineProvider = ({ - children, - input -}: Props) => { +export const IDPayConfigurationMachineProvider = ({ children }: Props) => { const dispatch = useIODispatch(); const sessionInfo = useIOSelector(sessionInfoSelector); @@ -89,7 +84,7 @@ export const IDPayConfigurationMachineProvider = ({ isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); - const actors = createServicesImplementation( + const actors = createActorsImplementation( idPayClient, paymentManagerClient, pmSessionManager, @@ -105,10 +100,7 @@ export const IDPayConfigurationMachineProvider = ({ }); return ( - + {children} ); diff --git a/ts/features/idpay/configuration/navigation/navigator.tsx b/ts/features/idpay/configuration/navigation/navigator.tsx index 6699dabf875..c5d50439e38 100644 --- a/ts/features/idpay/configuration/navigation/navigator.tsx +++ b/ts/features/idpay/configuration/navigation/navigator.tsx @@ -1,4 +1,3 @@ -import { RouteProp, useRoute } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; import { isGestureEnabled } from "../../../../utils/navigation"; @@ -15,60 +14,48 @@ import { IdPayConfigurationRoutes } from "./routes"; const Stack = createStackNavigator(); -type IdPayConfigurationRouteProps = RouteProp< - IdPayConfigurationParamsList, - "IDPAY_CONFIGURATION_NAVIGATOR" ->; - -export const IdPayConfigurationNavigator = () => { - const { params } = useRoute(); - const { initiativeId, mode } = params; - - return ( - - - - - - - - - - - - - - - - - - ); -}; +export const IdPayConfigurationNavigator = () => ( + + + + + + + + + + + + + + + + + +); diff --git a/ts/features/idpay/configuration/navigation/params.ts b/ts/features/idpay/configuration/navigation/params.ts index 6e7a4383733..ee7104ba052 100644 --- a/ts/features/idpay/configuration/navigation/params.ts +++ b/ts/features/idpay/configuration/navigation/params.ts @@ -1,19 +1,15 @@ +import { IdPayIbanEnrollmentScreenParams } from "../screens/IbanEnrollmentScreen"; import { IdPayDiscountInstrumentsScreenRouteParams } from "../screens/IdPayDiscountInstrumentsScreen"; -import { ConfigurationMode } from "../types"; +import { IdPayInitiativeConfigurationIntroScreenParams } from "../screens/InitiativeConfigurationIntroScreen"; +import { IdPayInstrumentsEnrollmentScreenParams } from "../screens/InstrumentsEnrollmentScreen"; import { IdPayConfigurationRoutes } from "./routes"; -export type IdPayConfigurationNavigatorParams = { - initiativeId: string; - mode?: ConfigurationMode; -}; - export type IdPayConfigurationParamsList = { - [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR]: IdPayConfigurationNavigatorParams; - [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO]: IdPayInitiativeConfigurationIntroScreenParams; [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING]: undefined; [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING]: undefined; - [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT]: undefined; - [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT]: undefined; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT]: IdPayIbanEnrollmentScreenParams; + [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT]: IdPayInstrumentsEnrollmentScreenParams; [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS]: IdPayDiscountInstrumentsScreenRouteParams; [IdPayConfigurationRoutes.IDPAY_CONFIGURATION_SUCCESS]: undefined; }; diff --git a/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx b/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx index 6372b90818d..68edc36a619 100644 --- a/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx @@ -1,4 +1,5 @@ import { HSpacer, Icon, VSpacer } from "@pagopa/io-app-design-system"; +import { RouteProp, useFocusEffect, useRoute } from "@react-navigation/native"; import React from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; @@ -22,8 +23,21 @@ import { selectEnrolledIban, selectIsIbanOnlyMode } from "../machine/selectors"; +import { IdPayConfigurationParamsList } from "../navigation/params"; +import { ConfigurationMode } from "../types"; + +export type IdPayIbanEnrollmentScreenParams = { + initiativeId?: string; +}; + +type RouteProps = RouteProp< + IdPayConfigurationParamsList, + "IDPAY_CONFIGURATION_IBAN_ENROLLMENT" +>; export const IbanEnrollmentScreen = () => { + const { params } = useRoute(); + const { initiativeId } = params; const { useActorRef, useSelector } = IdPayConfigurationMachineContext; const machine = useActorRef(); @@ -34,6 +48,18 @@ export const IbanEnrollmentScreen = () => { const enrolledIban = useSelector(selectEnrolledIban); const [selectedIban, setSelectedIban] = React.useState(); + useFocusEffect( + React.useCallback(() => { + if (initiativeId !== undefined) { + machine.send({ + type: "start-configuration", + initiativeId, + mode: ConfigurationMode.IBAN + }); + } + }, [machine, initiativeId]) + ); + React.useEffect(() => { if (enrolledIban) { setSelectedIban(enrolledIban); diff --git a/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx b/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx index f43a1c77701..2d50f111b80 100644 --- a/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx @@ -1,5 +1,4 @@ import { HSpacer, Icon, VSpacer } from "@pagopa/io-app-design-system"; -import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; @@ -24,33 +23,20 @@ export const IbanOnboardingScreen = () => { const machine = useActorRef(); const customGoBack = () => machine.send({ type: "back" }); - const [iban, setIban] = React.useState(undefined); - const [ibanName, setIbanName] = React.useState(undefined); - const isLoading = useSelector(isLoadingSelector); - const isIbanValid = () => - pipe( - iban, - O.fromNullable, - O.fold( - () => undefined, - iban => E.isRight(Iban.decode(iban)) - ) - ); + const [iban, setIban] = React.useState<{ + text: string; + value: O.Option; + }>({ text: "", value: O.none }); - const isIbanNameValid = () => - pipe( - ibanName, - O.fromNullable, - O.fold( - () => undefined, - ibanName => ibanName.length > 0 - ) - ); + const [ibanName, setIbanName] = React.useState(""); + const isLoading = useSelector(isLoadingSelector); useNavigationSwipeBackListener(() => { machine.send({ type: "back", skipNavigation: true }); }); + const isInputValid = O.isSome(iban.value) && ibanName.length > 0; + return ( { {I18n.t("idpay.configuration.iban.onboarding.bodyLink")} { mask: "AA99A9999999999999999999999" }, keyboardType: "default", - value: iban, - onChangeText: val => setIban(val) + value: iban.text, + onChangeText: text => + setIban({ value: pipe(Iban.decode(text), O.fromEither), text }) }} /> { title: isLoading ? "" : I18n.t("global.buttons.continue"), isLoading, onPress: () => { - const isDataSendable = - iban !== undefined && - ibanName !== undefined && - ibanName.length > 0; - if (isDataSendable) { - machine.send({ - type: "confirm-iban-onboarding", - ibanBody: { iban, description: ibanName } - }); - } else { - setIbanName(""); // force re-render to show error in the UI - } + pipe( + iban.value, + O.map(iban => + machine.send({ + type: "confirm-iban-onboarding", + ibanBody: { iban, description: ibanName || "" } + }) + ) + ); }, - disabled: isLoading || !isIbanValid() + disabled: isLoading || !isInputValid }} />
diff --git a/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx b/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx index 366bac30bb5..444bc9600e1 100644 --- a/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx +++ b/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx @@ -2,13 +2,19 @@ import { Body, H1, IOColors, + IOIcons, IOStyles, Icon, LabelSmall, VSpacer, useIOTheme } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; +import { + RouteProp, + useFocusEffect, + useNavigation, + useRoute +} from "@react-navigation/native"; import React from "react"; import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; @@ -21,35 +27,26 @@ import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { isLoadingSelector } from "../../../../xstate/selectors"; import { IdPayConfigurationMachineContext } from "../machine/provider"; +import { ConfigurationMode } from "../types"; +import { IdPayConfigurationParamsList } from "../navigation/params"; -type RequiredDataItemProps = { - icon?: React.ReactNode; - title: string; - subTitle: string; +export type IdPayInitiativeConfigurationIntroScreenParams = { + initiativeId?: string; + mode?: ConfigurationMode; }; -const RequiredDataItem = (props: RequiredDataItemProps) => ( - - {!!props.icon && {props.icon}} - -

- {props.title} -

- - {props.subTitle} - -
-
-); +type RouteProps = RouteProp< + IdPayConfigurationParamsList, + "IDPAY_CONFIGURATION_INTRO" +>; export const InitiativeConfigurationIntroScreen = () => { + const navigation = useNavigation(); + const { params } = useRoute(); + const { initiativeId, mode } = params; const { useActorRef, useSelector } = IdPayConfigurationMachineContext; const machine = useActorRef(); - const navigation = useNavigation(); - - const theme = useIOTheme(); - const isLoading = useSelector(isLoadingSelector); const handleContinuePress = () => { @@ -62,32 +59,17 @@ export const InitiativeConfigurationIntroScreen = () => { ); - const requiredDataItems: ReadonlyArray = [ - { - icon: ( - - ), - title: I18n.t("idpay.configuration.intro.requiredData.ibanTitle"), - subTitle: I18n.t("idpay.configuration.intro.requiredData.ibanSubtitle") - }, - { - icon: ( - - ), - title: I18n.t("idpay.configuration.intro.requiredData.instrumentTitle"), - subTitle: I18n.t( - "idpay.configuration.intro.requiredData.instrumentSubtitle" - ) - } - ]; + useFocusEffect( + React.useCallback(() => { + if (!!initiativeId && !!mode) { + machine.send({ + type: "start-configuration", + initiativeId, + mode + }); + } + }, [machine, initiativeId, mode]) + ); return ( { {I18n.t("idpay.configuration.intro.requiredData.title")} - {requiredDataItems.map((item, index) => ( - - ))} + + { ); }; +type RequiredDataItemProps = { + icon?: IOIcons; + title: string; + subTitle: string; +}; + +const RequiredDataItem = (props: RequiredDataItemProps) => { + const theme = useIOTheme(); + return ( + + {!!props.icon && ( + + + + )} + +

+ {props.title} +

+ + {props.subTitle} + +
+
+ ); +}; + const styles = StyleSheet.create({ listItem: { paddingVertical: 16, diff --git a/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx b/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx index 10383188b3c..e09a5ecbd79 100644 --- a/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx +++ b/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx @@ -4,6 +4,7 @@ import { IOStyles, VSpacer } from "@pagopa/io-app-design-system"; +import { RouteProp, useFocusEffect, useRoute } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; @@ -32,10 +33,24 @@ import { selectIsInstrumentsOnlyMode, selectWalletInstruments } from "../machine/selectors"; +import { IdPayConfigurationParamsList } from "../navigation/params"; import { InitiativeFailureType } from "../types/failure"; +import { ConfigurationMode } from "../types"; + +export type IdPayInstrumentsEnrollmentScreenParams = { + initiativeId?: string; +}; + +type RouteProps = RouteProp< + IdPayConfigurationParamsList, + "IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT" +>; export const InstrumentsEnrollmentScreen = () => { const navigation = useIONavigation(); + const { params } = useRoute(); + const { initiativeId } = params; + const { useActorRef, useSelector } = IdPayConfigurationMachineContext; const machine = useActorRef(); @@ -57,6 +72,18 @@ export const InstrumentsEnrollmentScreen = () => { const hasSelectedInstruments = Object.keys(initiativeInstrumentsByIdWallet).length > 0; + useFocusEffect( + React.useCallback(() => { + if (initiativeId) { + machine.send({ + type: "start-configuration", + initiativeId, + mode: ConfigurationMode.INSTRUMENTS + }); + } + }, [machine, initiativeId]) + ); + React.useEffect(() => { pipe( failure, diff --git a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx index f25aaad4228..29bd1f84a82 100644 --- a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx @@ -84,7 +84,7 @@ const IdPayInitiativeDetailsScreen = () => { const navigateToConfiguration = () => { navigation.push(IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { - screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, params: { initiativeId, mode: ConfigurationMode.COMPLETE } }); }; diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index 32de9b4ea73..f85935dba07 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -10,7 +10,10 @@ import { WAITING_USER_INPUT_TAG, notImplementedStub } from "../../../../xstate/utils"; -import { OnboardingFailure } from "../types/OnboardingFailure"; +import { + OnboardingFailure, + OnboardingFailureEnum +} from "../types/OnboardingFailure"; import * as Context from "./context"; import * as Events from "./events"; import { @@ -55,7 +58,6 @@ export const idPayOnboardingMachine = setup({ assertEvent(event, "start-onboarding"); return event.serviceId.length > 0; }, - isSessionExpired: () => false, hasPdndCriteria: ({ context }) => pipe( context.requiredCriteria, @@ -374,18 +376,19 @@ export const idPayOnboardingMachine = setup({ OnboardingFailure: { entry: "navigateToFailureScreen", always: { - guard: "isSessionExpired", - target: "SessionExpired" + guard: ({ context }) => + pipe( + context.failure, + O.map(f => f === OnboardingFailureEnum.SESSION_EXPIRED), + O.getOrElse(() => false) + ), + actions: "closeOnboarding" }, on: { next: { actions: "navigateToInitiativeMonitoringScreen" } } - }, - - SessionExpired: { - entry: "closeOnboarding" } } }); diff --git a/ts/features/idpay/payment/machine/machine.ts b/ts/features/idpay/payment/machine/machine.ts index dd24bf4f6a6..c7ec5cb7db8 100644 --- a/ts/features/idpay/payment/machine/machine.ts +++ b/ts/features/idpay/payment/machine/machine.ts @@ -32,8 +32,7 @@ export const idPayPaymentMachine = setup({ navigateToAuthorizationScreen: notImplementedStub, navigateToResultScreen: notImplementedStub, closeAuthorization: notImplementedStub, - showErrorToast: notImplementedStub, - setFailure: (_ctx, _params: { data: any }) => notImplementedStub() + showErrorToast: notImplementedStub }, guards: { isSessionExpired: ({ context }) => From 515e652b72d2fcff99526edd884da03645cb8a9f Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Fri, 30 Aug 2024 11:04:50 +0200 Subject: [PATCH 16/31] fix: types --- .../idpay/configuration/machine/machine.ts | 3 +- .../idpay/configuration/machine/selectors.ts | 6 +-- .../idpay/onboarding/machine/selectors.ts | 7 ++- yarn.lock | 44 ++++++++----------- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/ts/features/idpay/configuration/machine/machine.ts b/ts/features/idpay/configuration/machine/machine.ts index c83b617250e..ab4b34e41a8 100644 --- a/ts/features/idpay/configuration/machine/machine.ts +++ b/ts/features/idpay/configuration/machine/machine.ts @@ -1,4 +1,3 @@ -/* eslint-disable sonarjs/no-identical-functions */ import * as pot from "@pagopa/ts-commons/lib/pot"; import * as O from "fp-ts/lib/Option"; import { flow, pipe } from "fp-ts/lib/function"; @@ -664,3 +663,5 @@ export const idPayConfigurationMachine = setup({ }); const decodeFailure = flow(InitiativeFailure.decode, O.fromEither); + +export type IdPayConfigurationMachine = typeof idPayConfigurationMachine; diff --git a/ts/features/idpay/configuration/machine/selectors.ts b/ts/features/idpay/configuration/machine/selectors.ts index 73affa60f80..009b343e9e0 100644 --- a/ts/features/idpay/configuration/machine/selectors.ts +++ b/ts/features/idpay/configuration/machine/selectors.ts @@ -1,13 +1,13 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import * as O from "fp-ts/lib/Option"; import { createSelector } from "reselect"; -import { SnapshotFrom } from "xstate"; +import { StateFrom } from "xstate"; import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; import { LOADING_TAG } from "../../../../xstate/utils"; import { ConfigurationMode } from "../types"; -import { idPayConfigurationMachine } from "./machine"; +import { IdPayConfigurationMachine } from "./machine"; -type MachineSnapshot = SnapshotFrom; +type MachineSnapshot = StateFrom; type IDPayInstrumentsByIdWallet = { [idWallet: string]: InstrumentDTO; diff --git a/ts/features/idpay/onboarding/machine/selectors.ts b/ts/features/idpay/onboarding/machine/selectors.ts index 26db0b84d74..99b1084c313 100644 --- a/ts/features/idpay/onboarding/machine/selectors.ts +++ b/ts/features/idpay/onboarding/machine/selectors.ts @@ -1,16 +1,15 @@ -/* eslint-disable no-underscore-dangle */ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; -import { SnapshotFrom } from "xstate"; +import { StateFrom } from "xstate"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; import { SelfDeclarationDTO } from "../../../../../definitions/idpay/SelfDeclarationDTO"; import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDeclarationMultiDTO"; import * as Context from "./context"; -import { idPayOnboardingMachine } from "./machine"; +import { IdPayOnboardingMachine } from "./machine"; -type MachineSnapshot = SnapshotFrom; +type MachineSnapshot = StateFrom; export const selectOnboardingFailure = (snapshot: MachineSnapshot) => snapshot.context.failure; diff --git a/yarn.lock b/yarn.lock index eeac76ac557..1b785bd7313 100644 --- a/yarn.lock +++ b/yarn.lock @@ -91,7 +91,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.20.0", "@babel/core@^7.23.9", "@babel/core@^7.4.5": +"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.20.0", "@babel/core@^7.23.9", "@babel/core@^7.4.5": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== @@ -3599,10 +3599,14 @@ resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.3.3.tgz#dec938d9cb2a7b82cc6c698664ed6d8d28d1f110" integrity sha512-yY0CyUP+ipeSKiklNAYfY51CZObVQ0y48TJHVPfxnfGbsqwj+6rZkC1mAg9yF/Zu56DCDZTtUBKmY4BsOVDLVw== dependencies: - use-isomorphic-layout-effect "^1.1.2" - use-sync-external-store "^1.2.0" + "@babel/core" "^7.12.10" + "@xstate/machine-extractor" "0.7.1" + "@xstate/tools-shared" "1.2.3" + chokidar "^3.5.3" + commander "^8.0.0" + xstate "^4.29.0" -"@xstate5/react@npm:@xstate/react@4": +"@xstate/react@^4.0.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa" integrity sha512-pFp/Y+bnczfaZ0V8B4LOhx3d6Gd71YKAPbzerGqydC2nsYN/mp7RZu3q/w6/kvI2hwR/jeDeetM7xc3JFZH2NA== @@ -3825,7 +3829,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.3, anymatch@~3.1.2: +anymatch@^3.0.3: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -4891,7 +4895,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -5549,11 +5553,6 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^8.0.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - commander@^9.4.1: version "9.5.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" @@ -7831,7 +7830,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@^2.1.2, fsevents@^2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -8937,7 +8936,7 @@ is-generator-function@^1.0.10: dependencies: has-tostringtag "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -12804,7 +12803,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -16020,7 +16019,7 @@ url-parse@^1.5.9: querystringify "^2.1.1" requires-port "^1.0.0" -use-isomorphic-layout-effect@^1.0.0, use-isomorphic-layout-effect@^1.1.2: +use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== @@ -16030,7 +16029,7 @@ use-latest-callback@^0.1.5, use-latest-callback@^0.1.7: resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== -use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: +use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -16483,15 +16482,10 @@ xss@1.0.10: commander "^2.20.3" cssfilter "0.0.10" -"xstate5@npm:xstate@5": - version "5.13.0" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.13.0.tgz#7f7092d813a89d94024b083fe23a86b6cf4a323a" - integrity sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ== - -xstate@^4.29.0, xstate@^4.33.6: - version "4.35.4" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.35.4.tgz#87b2a45b6c7e84d820f56378408c6531ca5c4662" - integrity sha512-mqRBYHhljP1xIItI4xnSQNHEv6CKslSM1cOGmvhmxeoDPAZgNbhSUYAL5N6DZIxRfpYY+M+bSm3mUFHD63iuvg== +xstate@^5: + version "5.17.4" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.17.4.tgz#334ab2da123973634097f7ca48387ae1589c774e" + integrity sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA== xstate@^5.13.0: version "5.13.0" From bff44fca066c523b9acc90175173a9d8ff6d5fbc Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Sat, 31 Aug 2024 10:19:04 +0200 Subject: [PATCH 17/31] chore: remove xstate helpers --- ts/xstate/helpers/guardedNavigationAction.ts | 52 -------------------- ts/xstate/hooks/useXStateMachine.tsx | 19 ------- ts/xstate/selectors/index.ts | 15 ------ ts/xstate/types/events.ts | 13 ----- ts/xstate/utils/index.ts | 9 ---- 5 files changed, 108 deletions(-) delete mode 100644 ts/xstate/helpers/guardedNavigationAction.ts delete mode 100644 ts/xstate/hooks/useXStateMachine.tsx delete mode 100644 ts/xstate/selectors/index.ts delete mode 100644 ts/xstate/types/events.ts delete mode 100644 ts/xstate/utils/index.ts diff --git a/ts/xstate/helpers/guardedNavigationAction.ts b/ts/xstate/helpers/guardedNavigationAction.ts deleted file mode 100644 index 5737cfb795f..00000000000 --- a/ts/xstate/helpers/guardedNavigationAction.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as O from "fp-ts/lib/Option"; -import { pipe } from "fp-ts/lib/function"; -import { MachineContext } from "xstate"; -import { Back } from "../types/events"; - -type WrappedAction = (args: { - context: TContext; - event: any; -}) => void; -type Events = { type: string } | Back; - -/** - * Checks if the event is of type E_BACK - * @param event The event object to check. - * @returns True if the event is of type E_BACK, false otherwise. - */ -const isBack = (event: Events): event is Back => event.type === "BACK"; - -/** - * Checks if an E_BACK event should skip the navigation action. - * @param event The event object to check. - * @returns True if the event has a skipNavigation property set to true; otherwise, false. - */ -const skipNavigation = (event: Back) => event.skipNavigation || false; - -/** - * Wrap an action function with a guard clause that checks whether the event should be skipped. - * @param action - The original action function to wrap. - * @returns A new function that takes a context and event object - * and either skips the action or calls the original action based on the event. - */ -export const guardedNavigationAction = - (action: WrappedAction) => - (args: { context: TContext; event: any }) => - pipe( - args.event, - O.of, - O.filter(isBack), - O.filter(skipNavigation), - O.fold( - /** - * The event is not of type E_BACK and/or does not contain the skipNavigation property. - * WrappedAction should be executed. - */ - () => action(args), - /** - * The event is of type E_BACK and contains the skipNavigation property. - * No actions should be executed. - */ - () => null - ) - ); diff --git a/ts/xstate/hooks/useXStateMachine.tsx b/ts/xstate/hooks/useXStateMachine.tsx deleted file mode 100644 index a47fb37580b..00000000000 --- a/ts/xstate/hooks/useXStateMachine.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import * as X from "xstate"; - -type MachineCreatorFn = () => T; - -const useXStateMachine = ( - fn: MachineCreatorFn -): [T] => { - const machine = React.useRef(undefined); - - if (machine.current === undefined) { - // eslint-disable-next-line functional/immutable-data - machine.current = fn(); - } - - return [machine.current]; -}; - -export { useXStateMachine }; diff --git a/ts/xstate/selectors/index.ts b/ts/xstate/selectors/index.ts deleted file mode 100644 index b38ce5a029f..00000000000 --- a/ts/xstate/selectors/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createSelector } from "reselect"; -import { AnyActorLogic, SnapshotFrom } from "xstate"; -import { LOADING_TAG, UPSERTING_TAG } from "../utils"; - -type MachineSnapshot = SnapshotFrom; - -const selectTags = ({ tags }: MachineSnapshot) => tags; - -export const isLoadingSelector = createSelector(selectTags, tags => - tags.has(LOADING_TAG) -); - -export const isUpseringSelector = createSelector(selectTags, tags => - tags.has(UPSERTING_TAG) -); diff --git a/ts/xstate/types/events.ts b/ts/xstate/types/events.ts deleted file mode 100644 index a7ef155f203..00000000000 --- a/ts/xstate/types/events.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface Next { - readonly type: "next"; -} -export interface Back { - readonly type: "back"; - readonly skipNavigation?: boolean; -} - -export interface Close { - readonly type: "close"; -} - -export type GlobalEvents = Next | Back | Close; diff --git a/ts/xstate/utils/index.ts b/ts/xstate/utils/index.ts deleted file mode 100644 index 2e9ca5514ce..00000000000 --- a/ts/xstate/utils/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -const LOADING_TAG = "LOADING"; -const UPSERTING_TAG = "UPSERTING"; -const WAITING_USER_INPUT_TAG = "WAITING_USER_INPUT"; - -export { LOADING_TAG, UPSERTING_TAG, WAITING_USER_INPUT_TAG }; - -export const notImplementedStub = () => { - throw new Error("Not implemented"); -}; From eceb1b668411c38bdda4065e501dd42d8285f0df Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 11:22:04 +0200 Subject: [PATCH 18/31] chore: fix imports --- ts/features/itwallet/machine/credential/actions.ts | 2 +- ts/features/itwallet/machine/credential/actors.ts | 2 +- ts/features/itwallet/machine/credential/events.ts | 2 +- ts/features/itwallet/machine/credential/machine.ts | 2 +- ts/features/itwallet/machine/credential/selectors.ts | 2 +- ts/features/itwallet/machine/eid/__tests__/machine.ts | 2 +- ts/features/itwallet/machine/eid/actions.ts | 2 +- ts/features/itwallet/machine/eid/actors.ts | 2 +- ts/features/itwallet/machine/eid/events.ts | 2 +- ts/features/itwallet/machine/eid/machine.ts | 2 +- ts/features/itwallet/machine/eid/selectors.ts | 2 +- ts/features/itwallet/machine/provider.tsx | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ts/features/itwallet/machine/credential/actions.ts b/ts/features/itwallet/machine/credential/actions.ts index 4a97b084326..8f8d53aa924 100644 --- a/ts/features/itwallet/machine/credential/actions.ts +++ b/ts/features/itwallet/machine/credential/actions.ts @@ -1,7 +1,7 @@ import { IOToast } from "@pagopa/io-app-design-system"; import { constNull, pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import { ActionArgs } from "xstate5"; +import { ActionArgs } from "xstate"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import ROUTES from "../../../../navigation/routes"; diff --git a/ts/features/itwallet/machine/credential/actors.ts b/ts/features/itwallet/machine/credential/actors.ts index 34742a4a9cc..2c1d37a9d96 100644 --- a/ts/features/itwallet/machine/credential/actors.ts +++ b/ts/features/itwallet/machine/credential/actors.ts @@ -1,5 +1,5 @@ import * as O from "fp-ts/lib/Option"; -import { fromPromise } from "xstate5"; +import { fromPromise } from "xstate"; import { useIOStore } from "../../../../store/hooks"; import { assert } from "../../../../utils/assert"; import * as credentialIssuanceUtils from "../../common/utils/itwCredentialIssuanceUtils"; diff --git a/ts/features/itwallet/machine/credential/events.ts b/ts/features/itwallet/machine/credential/events.ts index addabfe1f8d..6f77f8cdd22 100644 --- a/ts/features/itwallet/machine/credential/events.ts +++ b/ts/features/itwallet/machine/credential/events.ts @@ -1,4 +1,4 @@ -import { ErrorActorEvent } from "xstate5"; +import { ErrorActorEvent } from "xstate"; import { CredentialType } from "../../common/utils/itwMocksUtils"; export type Reset = { diff --git a/ts/features/itwallet/machine/credential/machine.ts b/ts/features/itwallet/machine/credential/machine.ts index c0dd4a79d44..430d0a13afc 100644 --- a/ts/features/itwallet/machine/credential/machine.ts +++ b/ts/features/itwallet/machine/credential/machine.ts @@ -1,4 +1,4 @@ -import { assign, fromPromise, setup } from "xstate5"; +import { assign, fromPromise, setup } from "xstate"; import { ItwTags } from "../tags"; import { ItwSessionExpiredError } from "../../api/client"; import { diff --git a/ts/features/itwallet/machine/credential/selectors.ts b/ts/features/itwallet/machine/credential/selectors.ts index d8e3ef3285e..075059ca977 100644 --- a/ts/features/itwallet/machine/credential/selectors.ts +++ b/ts/features/itwallet/machine/credential/selectors.ts @@ -1,5 +1,5 @@ import * as O from "fp-ts/lib/Option"; -import { StateFrom } from "xstate5"; +import { StateFrom } from "xstate"; import { ItwTags } from "../tags"; import { ItwCredentialIssuanceMachine } from "./machine"; diff --git a/ts/features/itwallet/machine/eid/__tests__/machine.ts b/ts/features/itwallet/machine/eid/__tests__/machine.ts index 2fbee9984dd..7d032640979 100644 --- a/ts/features/itwallet/machine/eid/__tests__/machine.ts +++ b/ts/features/itwallet/machine/eid/__tests__/machine.ts @@ -1,6 +1,6 @@ import { waitFor } from "@testing-library/react-native"; import _ from "lodash"; -import { createActor, fromPromise, StateFrom } from "xstate5"; +import { createActor, fromPromise, StateFrom } from "xstate"; import { idps } from "../../../../../utils/idps"; import { WalletAttestationResult } from "../../../common/utils/itwAttestationUtils"; import { ItwStoredCredentialsMocks } from "../../../common/utils/itwMocksUtils"; diff --git a/ts/features/itwallet/machine/eid/actions.ts b/ts/features/itwallet/machine/eid/actions.ts index 2c709c53520..1c9e8795fa4 100644 --- a/ts/features/itwallet/machine/eid/actions.ts +++ b/ts/features/itwallet/machine/eid/actions.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { IOToast } from "@pagopa/io-app-design-system"; -import { ActionArgs } from "xstate5"; +import { ActionArgs } from "xstate"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch } from "../../../../store/hooks"; diff --git a/ts/features/itwallet/machine/eid/actors.ts b/ts/features/itwallet/machine/eid/actors.ts index 3a96fa0c58a..00a245fdaab 100644 --- a/ts/features/itwallet/machine/eid/actors.ts +++ b/ts/features/itwallet/machine/eid/actors.ts @@ -1,4 +1,4 @@ -import { fromPromise } from "xstate5"; +import { fromPromise } from "xstate"; import * as O from "fp-ts/lib/Option"; import * as issuanceUtils from "../../common/utils/itwIssuanceUtils"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; diff --git a/ts/features/itwallet/machine/eid/events.ts b/ts/features/itwallet/machine/eid/events.ts index a7a541461ad..f0b6d453d53 100644 --- a/ts/features/itwallet/machine/eid/events.ts +++ b/ts/features/itwallet/machine/eid/events.ts @@ -1,4 +1,4 @@ -import { ErrorActorEvent } from "xstate5"; +import { ErrorActorEvent } from "xstate"; import { LocalIdpsFallback } from "../../../../utils/idps"; export type IdentificationMode = "spid" | "ciePin" | "cieId"; diff --git a/ts/features/itwallet/machine/eid/machine.ts b/ts/features/itwallet/machine/eid/machine.ts index 946f7989333..b839e66962c 100644 --- a/ts/features/itwallet/machine/eid/machine.ts +++ b/ts/features/itwallet/machine/eid/machine.ts @@ -1,4 +1,4 @@ -import { assign, fromPromise, setup } from "xstate5"; +import { assign, fromPromise, setup } from "xstate"; import { StoredCredential } from "../../common/utils/itwTypesUtils"; import { WalletAttestationResult } from "../../common/utils/itwAttestationUtils"; import { assert } from "../../../../utils/assert"; diff --git a/ts/features/itwallet/machine/eid/selectors.ts b/ts/features/itwallet/machine/eid/selectors.ts index 3c863af15c0..28f8539a285 100644 --- a/ts/features/itwallet/machine/eid/selectors.ts +++ b/ts/features/itwallet/machine/eid/selectors.ts @@ -1,4 +1,4 @@ -import { StateFrom } from "xstate5"; +import { StateFrom } from "xstate"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { ItwTags } from "../tags"; diff --git a/ts/features/itwallet/machine/provider.tsx b/ts/features/itwallet/machine/provider.tsx index 4802902ecc5..57000b15149 100644 --- a/ts/features/itwallet/machine/provider.tsx +++ b/ts/features/itwallet/machine/provider.tsx @@ -1,5 +1,5 @@ import { useIOToast } from "@pagopa/io-app-design-system"; -import { createActorContext } from "@xstate5/react"; +import { createActorContext } from "@xstate/react"; import * as React from "react"; import { useIONavigation } from "../../../navigation/params/AppParamsList"; import { useIODispatch, useIOStore } from "../../../store/hooks"; From 392404d1b5e8497e56811e11238851af453f8198 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 11:22:58 +0200 Subject: [PATCH 19/31] fix: wrong import --- .../handleWalletLoadingPlaceholdersTimeout.test.ts | 6 ++---- .../saga/__tests__/handleWalletLoadingStateSaga.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ts/features/newWallet/saga/__tests__/handleWalletLoadingPlaceholdersTimeout.test.ts b/ts/features/newWallet/saga/__tests__/handleWalletLoadingPlaceholdersTimeout.test.ts index 6db6dd91675..96a630cdbef 100644 --- a/ts/features/newWallet/saga/__tests__/handleWalletLoadingPlaceholdersTimeout.test.ts +++ b/ts/features/newWallet/saga/__tests__/handleWalletLoadingPlaceholdersTimeout.test.ts @@ -1,7 +1,7 @@ import { Millisecond } from "@pagopa/ts-commons/lib/units"; import { testSaga } from "redux-saga-test-plan"; import { delay } from "typed-redux-saga"; -import { getActionType } from "xstate/lib/utils"; +import { getType } from "typesafe-actions"; import { walletResetPlaceholders, walletToggleLoadingState @@ -38,9 +38,7 @@ describe("handleWalletLoadingPlaceholdersTimeout", () => { .isDone(); }); - it(`dispatches ${getActionType( - walletResetPlaceholders - )} after timeout`, () => { + it(`dispatches ${getType(walletResetPlaceholders)} after timeout`, () => { testSaga( handleWalletPlaceholdersTimeout, LOADING_STATE_TIMEOUT, diff --git a/ts/features/newWallet/saga/__tests__/handleWalletLoadingStateSaga.test.ts b/ts/features/newWallet/saga/__tests__/handleWalletLoadingStateSaga.test.ts index 9d2f411b7bb..8746064e668 100644 --- a/ts/features/newWallet/saga/__tests__/handleWalletLoadingStateSaga.test.ts +++ b/ts/features/newWallet/saga/__tests__/handleWalletLoadingStateSaga.test.ts @@ -1,7 +1,7 @@ import { Millisecond } from "@pagopa/ts-commons/lib/units"; import { testSaga } from "redux-saga-test-plan"; import { delay } from "typed-redux-saga"; -import { getActionType } from "xstate/lib/utils"; +import { getType } from "typesafe-actions"; import { walletAddCards } from "../../store/actions/cards"; import { walletToggleLoadingState } from "../../store/actions/placeholders"; import { selectWalletCards } from "../../store/selectors"; @@ -50,7 +50,7 @@ describe("handleWalletLoadingStateSaga", () => { .isDone(); }); - it(`disables the loading state as soon as ${getActionType( + it(`disables the loading state as soon as ${getType( walletAddCards )} action is dispatched`, () => { testSaga( From a2d0de559cc36ea17e6ed5059f92234135d1ca76 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 11:23:27 +0200 Subject: [PATCH 20/31] build: remove xstate 4 --- package.json | 4 +--- yarn.lock | 17 ----------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/package.json b/package.json index 6d04ed0d2a0..34791cb5f11 100644 --- a/package.json +++ b/package.json @@ -264,7 +264,6 @@ "@types/xml2js": "^0.4.11", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^7.13.0", - "@xstate/cli": "^0.3.3", "abortcontroller-polyfill": "1.7.3", "babel-jest": "^29.2.1", "babel-plugin-macros": "^3.1.0", @@ -314,8 +313,7 @@ "@babel/preset-typescript": "^7.23.3", "@babel/plugin-transform-typescript": "^7.23.6", "@babel/plugin-syntax-typescript": "^7.23.3", - "@babel/types": "^7.23.6", - "@xstate5/react/xstate": "^5.13.0" + "@babel/types": "^7.23.6" }, "react-native": { "path": "path-browserify", diff --git a/yarn.lock b/yarn.lock index 1b785bd7313..4849821ae93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3594,18 +3594,6 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@xstate/cli@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.3.3.tgz#dec938d9cb2a7b82cc6c698664ed6d8d28d1f110" - integrity sha512-yY0CyUP+ipeSKiklNAYfY51CZObVQ0y48TJHVPfxnfGbsqwj+6rZkC1mAg9yF/Zu56DCDZTtUBKmY4BsOVDLVw== - dependencies: - "@babel/core" "^7.12.10" - "@xstate/machine-extractor" "0.7.1" - "@xstate/tools-shared" "1.2.3" - chokidar "^3.5.3" - commander "^8.0.0" - xstate "^4.29.0" - "@xstate/react@^4.0.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.1.tgz#2f580fc5f83d195f95b56df6cd8061c66660d9fa" @@ -16487,11 +16475,6 @@ xstate@^5: resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.17.4.tgz#334ab2da123973634097f7ca48387ae1589c774e" integrity sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA== -xstate@^5.13.0: - version "5.13.0" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.13.0.tgz#7f7092d813a89d94024b083fe23a86b6cf4a323a" - integrity sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ== - xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From d94c44969f93feb20f76f7c90327dadf22b91323 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 11:23:41 +0200 Subject: [PATCH 21/31] chore: remove tests --- .../xstate/__tests__/machine.test.ts | 201 ------------------ 1 file changed, 201 deletions(-) delete mode 100644 ts/features/idpay/unsubscription/xstate/__tests__/machine.test.ts diff --git a/ts/features/idpay/unsubscription/xstate/__tests__/machine.test.ts b/ts/features/idpay/unsubscription/xstate/__tests__/machine.test.ts deleted file mode 100644 index 79ddc51fa72..00000000000 --- a/ts/features/idpay/unsubscription/xstate/__tests__/machine.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* eslint-disable functional/no-let */ -import { waitFor } from "@testing-library/react-native"; -import { interpret } from "xstate"; -import { - InitiativeDTO, - StatusEnum -} from "../../../../../../definitions/idpay/InitiativeDTO"; -import { createIDPayUnsubscriptionMachine } from "../machine"; - -const T_INITIATIVE_ID = "T_INITIATIVE_ID"; -const T_INITIATIVE_NAME = "T_INITIATIVE_ID"; - -const T_INITIATIVE_DTO: InitiativeDTO = { - initiativeId: T_INITIATIVE_ID, - status: StatusEnum.NOT_REFUNDABLE, - endDate: new Date("2023-01-25T13:00:25.477Z"), - nInstr: 1 -}; - -describe("IDPay Unsubscription machine", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should transition to AWAITING_CONFIRMATION after start", () => { - // NOTE: initial state is START_UNSUBSCRIPTION but since it has an "always" transitions the transition occurs immediately - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID, - initiativeName: T_INITIATIVE_NAME - }); - - expect(machine.initialState.value).toEqual("AWAITING_CONFIRMATION"); - }); - - it("should transition to LOADING_INITIATIVE_INFO after start", () => { - // NOTE: initial state is START_UNSUBSCRIPTION but since it has an "always" transitions the transition occurs immediately - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID - }); - - expect(machine.initialState.value).toEqual("LOADING_INITIATIVE_INFO"); - }); - - it("should get initiative info if something is missing", async () => { - const mockGetInitiativeInfo = jest.fn(async () => - Promise.resolve(T_INITIATIVE_DTO) - ); - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID - }).withConfig({ - actions: { - navigateToConfirmationScreen: jest.fn(), - navigateToResultScreen: jest.fn(), - exitToWallet: jest.fn(), - exitUnsubscription: jest.fn(), - handleSessionExpired: jest.fn() - }, - services: { - getInitiativeInfo: mockGetInitiativeInfo, - unsubscribeFromInitiative: jest.fn() - } - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - await waitFor(() => expect(mockGetInitiativeInfo).toHaveBeenCalled()); - - expect(currentState.value).toEqual("AWAITING_CONFIRMATION"); - }); - - it("should allow the citizen to complete the unsubscription on happy path", async () => { - const mockUnsubscribeFromInitiative = jest.fn(async () => - Promise.resolve(undefined) - ); - - const mockNavigateToConfirmationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockExitToWallet = jest.fn(); - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID, - initiativeName: T_INITIATIVE_NAME - }).withConfig({ - actions: { - navigateToConfirmationScreen: mockNavigateToConfirmationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - exitToWallet: mockExitToWallet, - exitUnsubscription: jest.fn(), - handleSessionExpired: jest.fn() - }, - services: { - getInitiativeInfo: jest.fn(), - unsubscribeFromInitiative: mockUnsubscribeFromInitiative - } - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - await waitFor(() => - expect(mockNavigateToConfirmationScreen).toHaveBeenCalled() - ); - - expect(currentState.value).toEqual("AWAITING_CONFIRMATION"); - - service.send({ - type: "CONFIRM_UNSUBSCRIPTION" - }); - - expect(currentState.value).toEqual("UNSUBSCRIBING"); - - await waitFor(() => - expect(mockUnsubscribeFromInitiative).toHaveBeenCalled() - ); - - await waitFor(() => expect(mockNavigateToResultScreen).toHaveBeenCalled()); - - expect(currentState.value).toEqual("UNSUBSCRIPTION_SUCCESS"); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitToWallet).toHaveBeenCalled()); - }); - - it("should show failure if unsubscription fails", async () => { - const mockUnsubscribeFromInitiative = jest.fn(async () => - Promise.reject(undefined) - ); - - const mockNavigateToConfirmationScreen = jest.fn(); - const mockNavigateToResultScreen = jest.fn(); - const mockExitUnsubscription = jest.fn(); - - const machine = createIDPayUnsubscriptionMachine({ - initiativeId: T_INITIATIVE_ID, - initiativeName: T_INITIATIVE_NAME - }).withConfig({ - actions: { - navigateToConfirmationScreen: mockNavigateToConfirmationScreen, - navigateToResultScreen: mockNavigateToResultScreen, - exitToWallet: jest.fn(), - exitUnsubscription: mockExitUnsubscription, - handleSessionExpired: jest.fn() - }, - services: { - getInitiativeInfo: jest.fn(), - unsubscribeFromInitiative: mockUnsubscribeFromInitiative - } - }); - - let currentState = machine.initialState; - - const service = interpret(machine).onTransition(state => { - currentState = state; - }); - - service.start(); - - await waitFor(() => - expect(mockNavigateToConfirmationScreen).toHaveBeenCalled() - ); - - expect(currentState.value).toEqual("AWAITING_CONFIRMATION"); - - service.send({ - type: "CONFIRM_UNSUBSCRIPTION" - }); - - expect(currentState.value).toEqual("UNSUBSCRIBING"); - - await waitFor(() => - expect(mockUnsubscribeFromInitiative).toHaveBeenCalled() - ); - - await waitFor(() => expect(mockNavigateToResultScreen).toHaveBeenCalled()); - - expect(currentState.value).toEqual("UNSUBSCRIPTION_FAILURE"); - - service.send({ - type: "EXIT" - }); - - await waitFor(() => expect(mockExitUnsubscription).toHaveBeenCalled()); - }); -}); From 1142e06693c01269ea137358a5b7b2736e84c8cd Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 11:26:10 +0200 Subject: [PATCH 22/31] fix: machines --- ts/features/idpay/common/machine/tags.ts | 5 + .../idpay/configuration/machine/actions.ts | 64 +++---- .../idpay/configuration/machine/actors.ts | 22 +-- .../idpay/configuration/machine/context.ts | 6 +- .../idpay/configuration/machine/events.ts | 55 +++--- .../idpay/configuration/machine/machine.ts | 133 +++++++++------ .../idpay/configuration/machine/provider.tsx | 5 +- .../idpay/onboarding/machine/actions.ts | 50 +++--- .../idpay/onboarding/machine/actors.ts | 20 +-- .../idpay/onboarding/machine/context.ts | 6 +- .../idpay/onboarding/machine/events.ts | 33 ++-- .../idpay/onboarding/machine/machine.ts | 156 ++++++++++-------- .../idpay/onboarding/machine/provider.tsx | 4 +- 13 files changed, 302 insertions(+), 257 deletions(-) create mode 100644 ts/features/idpay/common/machine/tags.ts diff --git a/ts/features/idpay/common/machine/tags.ts b/ts/features/idpay/common/machine/tags.ts new file mode 100644 index 00000000000..af2c172d7bb --- /dev/null +++ b/ts/features/idpay/common/machine/tags.ts @@ -0,0 +1,5 @@ +export enum IdPayTags { + Loading = "Loading", + Upserting = "Upserting", + WaitingUserInput = "WaitingUserInput" +} diff --git a/ts/features/idpay/configuration/machine/actions.ts b/ts/features/idpay/configuration/machine/actions.ts index aa5ea1f18ea..c1222d43228 100644 --- a/ts/features/idpay/configuration/machine/actions.ts +++ b/ts/features/idpay/configuration/machine/actions.ts @@ -3,56 +3,49 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; +import { useIODispatch } from "../../../../store/hooks"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayDetailsRoutes } from "../../details/navigation"; import { IdPayConfigurationRoutes } from "../navigation/routes"; import { InitiativeFailure } from "../types/failure"; import * as Context from "./context"; const createActionsImplementation = ( - navigation: ReturnType + navigation: ReturnType, + dispatch: ReturnType ) => { - const navigateToConfigurationIntro = guardedNavigationAction( - () => { - navigation.navigate( - IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, - { - screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, - params: {} - } - ); - } - ); - - const navigateToIbanEnrollmentScreen = guardedNavigationAction(() => + const navigateToConfigurationIntro = () => { + navigation.navigate( + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + { + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO, + params: {} + } + ); + }; + const navigateToIbanEnrollmentScreen = () => navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, params: {} } - ) - ); - - const navigateToIbanOnboardingScreen = guardedNavigationAction(() => + ); + const navigateToIbanOnboardingScreen = () => navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_LANDING } - ) - ); - - const navigateToIbanOnboardingFormScreen = guardedNavigationAction(() => + ); + const navigateToIbanOnboardingFormScreen = () => navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ONBOARDING } - ) - ); - - const navigateToInstrumentsEnrollmentScreen = guardedNavigationAction(() => + ); + const navigateToInstrumentsEnrollmentScreen = () => navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { @@ -60,9 +53,7 @@ const createActionsImplementation = ( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, params: {} } - ) - ); - + ); const navigateToConfigurationSuccessScreen = () => { navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, @@ -100,6 +91,16 @@ const createActionsImplementation = ( navigation.pop(); }; + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + return { navigateToConfigurationIntro, navigateToIbanEnrollmentScreen, @@ -110,7 +111,8 @@ const createActionsImplementation = ( navigateToInitiativeDetailScreen, showUpdateIbanToast, showFailureToast, - exitConfiguration + exitConfiguration, + handleSessionExpired }; }; diff --git a/ts/features/idpay/configuration/machine/actors.ts b/ts/features/idpay/configuration/machine/actors.ts index 1595c45176a..dafdcbe4b11 100644 --- a/ts/features/idpay/configuration/machine/actors.ts +++ b/ts/features/idpay/configuration/machine/actors.ts @@ -11,11 +11,9 @@ import { InitiativeDTO } from "../../../../../definitions/idpay/InitiativeDTO"; import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; import { TypeEnum } from "../../../../../definitions/pagopa/Wallet"; import { PaymentManagerClient } from "../../../../api/pagopa"; -import { useIODispatch } from "../../../../store/hooks"; import { PaymentManagerToken, Wallet } from "../../../../types/pagopa"; import { SessionManager } from "../../../../utils/SessionManager"; import { convertWalletV2toWalletV1 } from "../../../../utils/walletv2"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayClient } from "../../common/api/client"; import { InitiativeFailureType } from "../types/failure"; import * as Events from "./events"; @@ -25,19 +23,8 @@ export const createActorsImplementation = ( paymentManagerClient: PaymentManagerClient, pmSessionManager: SessionManager, bearerToken: string, - language: PreferredLanguageEnum, - dispatch: ReturnType + language: PreferredLanguageEnum ) => { - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - const getInitiative = fromPromise(async params => { const response = await idPayClient.getWalletDetail({ initiativeId: params.input, @@ -54,7 +41,6 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject(InitiativeFailureType.GENERIC); @@ -88,7 +74,6 @@ export const createActorsImplementation = ( ); return Promise.resolve({ ibanList: uniqueIbanList }); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -125,7 +110,6 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(undefined); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -167,7 +151,6 @@ export const createActorsImplementation = ( return Promise.resolve(wallet); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -201,7 +184,6 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value.instrumentList); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -236,7 +218,6 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(undefined); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( @@ -274,7 +255,6 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(undefined); case 401: - handleSessionExpired(); return Promise.reject(InitiativeFailureType.SESSION_EXPIRED); default: return Promise.reject( diff --git a/ts/features/idpay/configuration/machine/context.ts b/ts/features/idpay/configuration/machine/context.ts index 0d3fa3a9f3a..9682a080d6e 100644 --- a/ts/features/idpay/configuration/machine/context.ts +++ b/ts/features/idpay/configuration/machine/context.ts @@ -6,7 +6,7 @@ import { Wallet } from "../../../../types/pagopa"; import { ConfigurationMode, InstrumentStatusByIdWallet } from "../types"; import { InitiativeFailureType } from "../types/failure"; -export interface Context { +export type Context = { readonly initiativeId: string; readonly mode: ConfigurationMode; readonly initiative: O.Option; @@ -16,9 +16,9 @@ export interface Context { readonly instrumentStatuses: InstrumentStatusByIdWallet; readonly areInstrumentsSkipped: boolean; readonly failure: O.Option; -} +}; -export const Context: Context = { +export const InitialContext: Context = { initiativeId: "", mode: ConfigurationMode.COMPLETE, initiative: O.none, diff --git a/ts/features/idpay/configuration/machine/events.ts b/ts/features/idpay/configuration/machine/events.ts index 8c1ede58059..bd6fca16229 100644 --- a/ts/features/idpay/configuration/machine/events.ts +++ b/ts/features/idpay/configuration/machine/events.ts @@ -1,55 +1,66 @@ import { IbanDTO } from "../../../../../definitions/idpay/IbanDTO"; import { IbanPutDTO } from "../../../../../definitions/idpay/IbanPutDTO"; -import { GlobalEvents } from "../../../../xstate/types/events"; import { ConfigurationMode } from "../types"; -export interface StartConfiguration { +export type StartConfiguration = { readonly type: "start-configuration"; readonly initiativeId: string; readonly mode: ConfigurationMode; -} +}; -export interface ConfirmIbanOnboarding { +export type ConfirmIbanOnboarding = { readonly type: "confirm-iban-onboarding"; readonly ibanBody: IbanPutDTO; -} +}; -export interface NewIbanOnboarding { +export type NewIbanOnboarding = { readonly type: "new-iban-onboarding"; -} +}; -export interface EnrollIban { +export type EnrollIban = { readonly type: "enroll-iban"; readonly iban: IbanDTO; -} +}; -export interface EnrollInstrument { +export type EnrollInstrument = { readonly type: "enroll-instrument"; readonly walletId: string; -} +}; -export interface DeleteInstrument { +export type DeleteInstrument = { readonly type: "delete-instrument"; readonly instrumentId: string; readonly walletId: string; -} +}; -export interface UpdateInstrumentSuccess { +export type UpdateInstrumentSuccess = { readonly type: "update-instrument-success"; readonly walletId: string; readonly enrolling: boolean; -} +}; -export interface UpdateInstrumentFailure { +export type UpdateInstrumentFailure = { readonly type: "update-instrument-failure"; readonly walletId: string; -} +}; -export interface SkipInstruments { +export type SkipInstruments = { readonly type: "skip-instruments"; -} +}; -export type Events = +export type Next = { + readonly type: "next"; +}; + +export type Back = { + readonly type: "back"; +}; + +export type Close = { + readonly type: "close"; +}; + +export type IdPayConfigurationEvents = | StartConfiguration | NewIbanOnboarding | ConfirmIbanOnboarding @@ -59,4 +70,6 @@ export type Events = | UpdateInstrumentSuccess | UpdateInstrumentFailure | SkipInstruments - | GlobalEvents; + | Next + | Back + | Close; diff --git a/ts/features/idpay/configuration/machine/machine.ts b/ts/features/idpay/configuration/machine/machine.ts index ab4b34e41a8..507903c1eee 100644 --- a/ts/features/idpay/configuration/machine/machine.ts +++ b/ts/features/idpay/configuration/machine/machine.ts @@ -21,22 +21,20 @@ import { StatusEnum as InstrumentStatusEnum } from "../../../../../definitions/idpay/InstrumentDTO"; import { Wallet } from "../../../../types/pagopa"; -import { - LOADING_TAG, - UPSERTING_TAG, - WAITING_USER_INPUT_TAG, - notImplementedStub -} from "../../../../xstate/utils"; +import { IdPayTags } from "../../common/machine/tags"; import { ConfigurationMode, InstrumentStatusByIdWallet } from "../types"; import { InitiativeFailure, InitiativeFailureType } from "../types/failure"; -import * as Context from "./context"; -import * as Events from "./events"; +import { Context, InitialContext } from "./context"; +import { IdPayConfigurationEvents } from "./events"; + +const notImplementedStub = () => { + throw new Error("Not implemented"); +}; -/** PLEASE DO NO USE AUTO-LAYOUT WHEN USING VISUAL EDITOR */ export const idPayConfigurationMachine = setup({ types: { - context: {} as Context.Context, - events: {} as Events.Events + context: {} as Context, + events: {} as IdPayConfigurationEvents }, actions: { navigateToConfigurationIntro: notImplementedStub, @@ -115,7 +113,8 @@ export const idPayConfigurationMachine = setup({ }; }), showFailureToast: notImplementedStub, - exitConfiguration: notImplementedStub + exitConfiguration: notImplementedStub, + handleSessionExpired: notImplementedStub }, actors: { getInitiative: fromPromise(notImplementedStub), @@ -125,7 +124,7 @@ export const idPayConfigurationMachine = setup({ getInitiativeInstruments: fromPromise, string>( notImplementedStub ), - instrumentsEnrollmentLogic: fromCallback( + instrumentsEnrollmentLogic: fromCallback( notImplementedStub ), enrollIban: fromPromise< @@ -144,13 +143,15 @@ export const idPayConfigurationMachine = setup({ O.getOrElse(() => false) ), hasIbanList: ({ context }) => context.ibanList.length > 0, - hasInstruments: ({ context }) => context.walletInstruments.length > 0 + hasInstruments: ({ context }) => context.walletInstruments.length > 0, + isSessionExpired: ({ event }: { event: IdPayConfigurationEvents }) => + "error" in event && event.error === InitiativeFailureType.SESSION_EXPIRED }, delays: { INSTRUMENTS_POLLING_INTERVAL: 3000 } }).createMachine({ - context: Context.Context, + context: InitialContext, id: "idpay-configuration", initial: "Idle", on: { @@ -160,7 +161,7 @@ export const idPayConfigurationMachine = setup({ }, states: { Idle: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], on: { "start-configuration": { guard: ({ event }) => event.initiativeId.length > 0, @@ -174,7 +175,7 @@ export const idPayConfigurationMachine = setup({ }, LoadingInitiative: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], invoke: { src: "getInitiative", id: "getInitiative", @@ -185,17 +186,23 @@ export const idPayConfigurationMachine = setup({ })), target: "EvaluatingInitiativeConfiguration" }, - onError: { - actions: assign(({ event }) => ({ - failure: pipe(InitiativeFailure.decode(event.error), O.fromEither) - })), - target: "ConfigurationFailure" - } + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: assign(({ event }) => ({ + failure: pipe(InitiativeFailure.decode(event.error), O.fromEither) + })), + target: "ConfigurationFailure" + } + ] } }, EvaluatingInitiativeConfiguration: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], always: [ { guard: "isInstrumentsOnlyMode", @@ -216,7 +223,7 @@ export const idPayConfigurationMachine = setup({ }, DisplayingConfigurationIntro: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToConfigurationIntro", on: { next: { @@ -231,7 +238,7 @@ export const idPayConfigurationMachine = setup({ entry: "navigateToIbanEnrollmentScreen", states: { LoadingIbanList: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], invoke: { src: "getIbanList", id: "getIbanList", @@ -242,6 +249,10 @@ export const idPayConfigurationMachine = setup({ target: "EvaluatingIbanList" }, onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, { guard: "isIbanOnlyMode", actions: assign(({ event }) => ({ @@ -263,7 +274,7 @@ export const idPayConfigurationMachine = setup({ }, EvaluatingIbanList: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], always: [ { guard: "hasIbanList", @@ -276,7 +287,7 @@ export const idPayConfigurationMachine = setup({ }, DisplayingIbanOnboardingLanding: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToIbanOnboardingScreen", on: { next: { @@ -295,7 +306,7 @@ export const idPayConfigurationMachine = setup({ }, DisplayingIbanOnboardingForm: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToIbanOnboardingFormScreen", on: { back: [ @@ -314,7 +325,7 @@ export const idPayConfigurationMachine = setup({ }, OnboardingNewIban: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], invoke: { src: "enrollIban", id: "enrollIban", @@ -328,20 +339,26 @@ export const idPayConfigurationMachine = setup({ onDone: { target: "IbanConfigurationCompleted" }, - onError: { - actions: [ - assign(({ event }) => ({ - failure: decodeFailure(event.error) - })), - "showFailureToast" - ], - target: "DisplayingIbanOnboardingForm" - } + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: [ + assign(({ event }) => ({ + failure: decodeFailure(event.error) + })), + "showFailureToast" + ], + target: "DisplayingIbanOnboardingForm" + } + ] } }, DisplayingIbanList: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToIbanEnrollmentScreen", on: { back: [ @@ -363,7 +380,7 @@ export const idPayConfigurationMachine = setup({ }, EnrollingIban: { - tags: [UPSERTING_TAG], + tags: [IdPayTags.Upserting], invoke: { src: "enrollIban", id: "enrollIban", @@ -385,6 +402,10 @@ export const idPayConfigurationMachine = setup({ } ], onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, { target: "DisplayingIbanList", actions: "showFailureToast" @@ -413,7 +434,7 @@ export const idPayConfigurationMachine = setup({ initial: "LoadingInstruments", states: { LoadingInstruments: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], entry: "navigateToInstrumentsEnrollmentScreen", type: "parallel", states: { @@ -432,6 +453,10 @@ export const idPayConfigurationMachine = setup({ target: "Success" }, onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, { guard: "isInstrumentsOnlyMode", actions: assign(({ event }) => ({ @@ -472,6 +497,10 @@ export const idPayConfigurationMachine = setup({ target: "Success" }, onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, { guard: "isInstrumentsOnlyMode", actions: assign(({ event }) => ({ @@ -513,7 +542,7 @@ export const idPayConfigurationMachine = setup({ }, DisplayingInstruments: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "updateAllInstrumentsStatus", initial: "DisplayingInstrument", invoke: { @@ -617,7 +646,7 @@ export const idPayConfigurationMachine = setup({ }, DisplayingConfigurationSuccess: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToConfigurationSuccessScreen", on: { next: { @@ -627,7 +656,7 @@ export const idPayConfigurationMachine = setup({ }, ConfigurationNotNeeded: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToConfigurationSuccessScreen", on: { next: { @@ -648,16 +677,12 @@ export const idPayConfigurationMachine = setup({ ConfigurationFailure: { type: "final", - always: { - guard: ({ context }) => - pipe( - context.failure, - O.map(f => f === InitiativeFailureType.SESSION_EXPIRED), - O.getOrElse(() => false) - ), - actions: "exitConfiguration" - }, entry: ["showFailureToast", "exitConfiguration"] + }, + + SessionExpired: { + entry: ["handleSessionExpired"], + always: { target: "LoadingInitiative" } } } }); diff --git a/ts/features/idpay/configuration/machine/provider.tsx b/ts/features/idpay/configuration/machine/provider.tsx index e3abb176329..e9f4cd8cff3 100644 --- a/ts/features/idpay/configuration/machine/provider.tsx +++ b/ts/features/idpay/configuration/machine/provider.tsx @@ -89,10 +89,9 @@ export const IDPayConfigurationMachineProvider = ({ children }: Props) => { paymentManagerClient, pmSessionManager, idPayToken, - language, - dispatch + language ); - const actions = createActionsImplementation(navigation); + const actions = createActionsImplementation(navigation, dispatch); const machine = idPayConfigurationMachine.provide({ actors, diff --git a/ts/features/idpay/onboarding/machine/actions.ts b/ts/features/idpay/onboarding/machine/actions.ts index d797bfeb23f..1146ee815fe 100644 --- a/ts/features/idpay/onboarding/machine/actions.ts +++ b/ts/features/idpay/onboarding/machine/actions.ts @@ -1,39 +1,32 @@ import * as O from "fp-ts/lib/Option"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; -import { guardedNavigationAction } from "../../../../xstate/helpers/guardedNavigationAction"; +import { useIODispatch } from "../../../../store/hooks"; +import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayDetailsRoutes } from "../../details/navigation"; import { IdPayOnboardingRoutes } from "../navigation/routes"; import * as Context from "./context"; export const createActionsImplementation = ( - navigation: ReturnType + navigation: ReturnType, + dispatch: ReturnType ) => { - const navigateToInitiativeDetailsScreen = - guardedNavigationAction(() => - navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { - screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, - params: {} - }) - ); - - const navigateToPdndCriteriaScreen = guardedNavigationAction(() => + const navigateToInitiativeDetailsScreen = () => + navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { + screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS, + params: {} + }); + const navigateToPdndCriteriaScreen = () => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_PDNDACCEPTANCE - }) - ); - - const navigateToBoolSelfDeclarationListScreen = guardedNavigationAction(() => + }); + const navigateToBoolSelfDeclarationListScreen = () => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_BOOL_SELF_DECLARATIONS - }) - ); - - const navigateToMultiSelfDeclarationListScreen = guardedNavigationAction(() => + }); + const navigateToMultiSelfDeclarationListScreen = () => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_MULTI_SELF_DECLARATIONS - }) - ); - + }); const navigateToCompletionScreen = () => navigation.navigate(IdPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN, { screen: IdPayOnboardingRoutes.IDPAY_ONBOARDING_COMPLETION @@ -65,6 +58,16 @@ export const createActionsImplementation = ( navigation.popToTop(); }; + const handleSessionExpired = () => { + dispatch( + refreshSessionToken.request({ + withUserInteraction: true, + showIdentificationModalAtStartup: false, + showLoader: true + }) + ); + }; + return { navigateToInitiativeDetailsScreen, navigateToPdndCriteriaScreen, @@ -73,6 +76,7 @@ export const createActionsImplementation = ( navigateToCompletionScreen, navigateToFailureScreen, navigateToInitiativeMonitoringScreen, - closeOnboarding + closeOnboarding, + handleSessionExpired }; }; diff --git a/ts/features/idpay/onboarding/machine/actors.ts b/ts/features/idpay/onboarding/machine/actors.ts index df8dbcb3b7a..6cd0f332977 100644 --- a/ts/features/idpay/onboarding/machine/actors.ts +++ b/ts/features/idpay/onboarding/machine/actors.ts @@ -9,8 +9,6 @@ import { CodeEnum as OnboardingErrorCodeEnum } from "../../../../../definitions/ import { StatusEnum as OnboardingStatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; import { SelfConsentDTO } from "../../../../../definitions/idpay/SelfConsentDTO"; -import { useIODispatch } from "../../../../store/hooks"; -import { refreshSessionToken } from "../../../fastLogin/store/actions/tokenRefreshActions"; import { IDPayClient } from "../../common/api/client"; import { OnboardingFailure, @@ -22,24 +20,13 @@ import { getBooleanSelfDeclarationListFromContext } from "./selectors"; export const createActorsImplementation = ( client: IDPayClient, token: string, - language: PreferredLanguage, - dispatch: ReturnType + language: PreferredLanguage ) => { const clientOptions = { bearerAuth: token, "Accept-Language": language }; - const handleSessionExpired = () => { - dispatch( - refreshSessionToken.request({ - withUserInteraction: true, - showIdentificationModalAtStartup: false, - showLoader: true - }) - ); - }; - const getInitiativeInfo = fromPromise( async params => { const dataResponse = await client.getInitiativeData({ @@ -56,7 +43,6 @@ export const createActorsImplementation = ( case 200: return Promise.resolve(value); case 401: - handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -102,7 +88,6 @@ export const createActorsImplementation = ( // Initiative not yet started by the citizen return Promise.resolve(O.none); case 401: - handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -137,7 +122,6 @@ export const createActorsImplementation = ( case 403: return Promise.reject(mapErrorCodeToFailure(value.code)); case 401: - handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -177,7 +161,6 @@ export const createActorsImplementation = ( case 403: return Promise.reject(mapErrorCodeToFailure(value.code)); case 401: - handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); @@ -229,7 +212,6 @@ export const createActorsImplementation = ( case 202: return Promise.resolve(undefined); case 401: - handleSessionExpired(); return Promise.reject(OnboardingFailureEnum.SESSION_EXPIRED); default: return Promise.reject(OnboardingFailureEnum.GENERIC); diff --git a/ts/features/idpay/onboarding/machine/context.ts b/ts/features/idpay/onboarding/machine/context.ts index 3c007805ad7..7d2c8d2204a 100644 --- a/ts/features/idpay/onboarding/machine/context.ts +++ b/ts/features/idpay/onboarding/machine/context.ts @@ -5,7 +5,7 @@ import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCr import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; import { OnboardingFailure } from "../types/OnboardingFailure"; -export interface Context { +export type Context = { readonly serviceId: string; readonly initiative: O.Option; readonly onboardingStatus: O.Option; @@ -14,9 +14,9 @@ export interface Context { readonly selfDeclarationsMultiAnwsers: Record; readonly selfDeclarationsBoolAnswers: Record; readonly failure: O.Option; -} +}; -export const Context: Context = { +export const InitialContext: Context = { serviceId: "", initiative: O.none, onboardingStatus: O.none, diff --git a/ts/features/idpay/onboarding/machine/events.ts b/ts/features/idpay/onboarding/machine/events.ts index 8de876c9e9c..f3a078e6452 100644 --- a/ts/features/idpay/onboarding/machine/events.ts +++ b/ts/features/idpay/onboarding/machine/events.ts @@ -1,24 +1,37 @@ import { SelfConsentMultiDTO } from "../../../../../definitions/idpay/SelfConsentMultiDTO"; import { SelfDeclarationBoolDTO } from "../../../../../definitions/idpay/SelfDeclarationBoolDTO"; -import { GlobalEvents } from "../../../../xstate/types/events"; -export interface StartOnboarding { +export type StartOnboarding = { readonly type: "start-onboarding"; readonly serviceId: string; -} +}; -export interface ToggleBoolCriteria { +export type ToggleBoolCriteria = { readonly type: "toggle-bool-criteria"; readonly criteria: SelfDeclarationBoolDTO; -} +}; -export interface SelectMultiConsent { +export type SelectMultiConsent = { readonly type: "select-multi-consent"; readonly data: SelfConsentMultiDTO; -} +}; -export type Events = - | GlobalEvents +export type Next = { + readonly type: "next"; +}; + +export type Back = { + readonly type: "back"; +}; + +export type Close = { + readonly type: "close"; +}; + +export type IdPayOnboardingEvents = | StartOnboarding | SelectMultiConsent - | ToggleBoolCriteria; + | ToggleBoolCriteria + | Next + | Back + | Close; diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index f85935dba07..71a3d797857 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -5,26 +5,27 @@ import { and, assertEvent, assign, fromPromise, setup } from "xstate"; import { InitiativeDataDTO } from "../../../../../definitions/idpay/InitiativeDataDTO"; import { StatusEnum as OnboardingStatusEnum } from "../../../../../definitions/idpay/OnboardingStatusDTO"; import { RequiredCriteriaDTO } from "../../../../../definitions/idpay/RequiredCriteriaDTO"; -import { - LOADING_TAG, - WAITING_USER_INPUT_TAG, - notImplementedStub -} from "../../../../xstate/utils"; +import { IdPayTags } from "../../common/machine/tags"; +import { InitiativeFailureType } from "../../configuration/types/failure"; import { OnboardingFailure, OnboardingFailureEnum } from "../types/OnboardingFailure"; -import * as Context from "./context"; -import * as Events from "./events"; +import { Context, InitialContext } from "./context"; +import { IdPayOnboardingEvents } from "./events"; import { getBooleanSelfDeclarationListFromContext, getMultiSelfDeclarationListFromContext } from "./selectors"; +const notImplementedStub = () => { + throw new Error("Not implemented"); +}; + export const idPayOnboardingMachine = setup({ types: { - context: {} as Context.Context, - events: {} as Events.Events + context: {} as Context, + events: {} as IdPayOnboardingEvents }, actions: { navigateToInitiativeDetailsScreen: notImplementedStub, @@ -34,7 +35,8 @@ export const idPayOnboardingMachine = setup({ navigateToCompletionScreen: notImplementedStub, navigateToFailureScreen: notImplementedStub, navigateToInitiativeMonitoringScreen: notImplementedStub, - closeOnboarding: notImplementedStub + closeOnboarding: notImplementedStub, + handleSessionExpired: notImplementedStub }, actors: { getInitiativeInfo: fromPromise( @@ -49,9 +51,7 @@ export const idPayOnboardingMachine = setup({ O.Option, O.Option >(notImplementedStub), - acceptRequiredCriteria: fromPromise( - notImplementedStub - ) + acceptRequiredCriteria: fromPromise(notImplementedStub) }, guards: { assertServiceId: ({ event }) => { @@ -78,11 +78,13 @@ export const idPayOnboardingMachine = setup({ context.selfDeclarationsMultiPage === 0, isLastMultiConsent: ({ context }) => context.selfDeclarationsMultiPage >= - getMultiSelfDeclarationListFromContext(context).length - 1 + getMultiSelfDeclarationListFromContext(context).length - 1, + isSessionExpired: ({ event }: { event: IdPayOnboardingEvents }) => + "error" in event && event.error === InitiativeFailureType.SESSION_EXPIRED } }).createMachine({ id: "idpay-onboarding", - context: Context.Context, + context: InitialContext, initial: "Idle", on: { close: { @@ -91,7 +93,7 @@ export const idPayOnboardingMachine = setup({ }, states: { Idle: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], on: { "start-onboarding": { guard: "assertServiceId", @@ -103,7 +105,7 @@ export const idPayOnboardingMachine = setup({ } }, LoadingInitiative: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], entry: "navigateToInitiativeDetailsScreen", initial: "LoadingInitiativeInfo", states: { @@ -117,15 +119,21 @@ export const idPayOnboardingMachine = setup({ })), target: "LoadingOnboardingStatus" }, - onError: { - actions: assign(({ event }) => ({ - failure: pipe( - OnboardingFailure.decode(event.error), - O.fromEither - ) - })), - target: "#idpay-onboarding.OnboardingFailure" - } + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: assign(({ event }) => ({ + failure: pipe( + OnboardingFailure.decode(event.error), + O.fromEither + ) + })), + target: "#idpay-onboarding.OnboardingFailure" + } + ] } }, @@ -158,7 +166,7 @@ export const idPayOnboardingMachine = setup({ }, DisplayingInitiativeInfo: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], on: { next: { target: "AcceptingTos" @@ -167,39 +175,51 @@ export const idPayOnboardingMachine = setup({ }, AcceptingTos: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], invoke: { src: "acceptTos", input: ({ context }) => selectInitiativeId(context), - onError: { - actions: assign(({ event }) => ({ - failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) - })), - target: "OnboardingFailure" - }, onDone: { target: "LoadingCriteria" - } + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + } + ] } }, LoadingCriteria: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], invoke: { src: "getRequiredCriteria", input: ({ context }) => selectInitiativeId(context), - onError: { - actions: assign(({ event }) => ({ - failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) - })), - target: "OnboardingFailure" - }, onDone: { actions: assign(({ event }) => ({ requiredCriteria: event.output })), target: "EvaluatingRequiredCriteria" - } + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + } + ] } }, @@ -220,7 +240,7 @@ export const idPayOnboardingMachine = setup({ }, DisplayingPdndCriteria: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToPdndCriteriaScreen", on: { next: [ @@ -239,11 +259,11 @@ export const idPayOnboardingMachine = setup({ }, DisplayingSelfDeclarationList: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], initial: "Evaluating", states: { Evaluating: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], always: [ { guard: "hasBooleanSelfDeclarationList", @@ -257,7 +277,7 @@ export const idPayOnboardingMachine = setup({ }, DisplayingBooleanSelfDeclarationList: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToBoolSelfDeclarationListScreen", on: { back: [ @@ -290,7 +310,7 @@ export const idPayOnboardingMachine = setup({ }, DisplayingMultiSelfDeclarationList: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], initial: "DisplayingMultiSelfDeclarationItem", states: { DisplayingMultiSelfDeclarationItem: { @@ -352,48 +372,50 @@ export const idPayOnboardingMachine = setup({ }, AcceptingCriteria: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], invoke: { src: "acceptRequiredCriteria", input: ({ context }) => context, - onError: { - actions: assign(({ event }) => ({ - failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) - })), - target: "OnboardingFailure" - }, onDone: { target: "OnboardingCompleted" - } + }, + onError: [ + { + guard: "isSessionExpired", + target: "SessionExpired" + }, + { + actions: assign(({ event }) => ({ + failure: pipe(OnboardingFailure.decode(event.error), O.fromEither) + })), + target: "OnboardingFailure" + } + ] } }, OnboardingCompleted: { - tags: [WAITING_USER_INPUT_TAG], + tags: [IdPayTags.WaitingUserInput], entry: "navigateToCompletionScreen" }, OnboardingFailure: { entry: "navigateToFailureScreen", - always: { - guard: ({ context }) => - pipe( - context.failure, - O.map(f => f === OnboardingFailureEnum.SESSION_EXPIRED), - O.getOrElse(() => false) - ), - actions: "closeOnboarding" - }, on: { next: { actions: "navigateToInitiativeMonitoringScreen" } } + }, + + SessionExpired: { + entry: ["handleSessionExpired"], + always: { target: "LoadingInitiative" } } } }); -const selectInitiativeId = (context: Context.Context) => +const selectInitiativeId = (context: Context) => pipe( context.initiative, O.map(initiative => initiative.initiativeId) diff --git a/ts/features/idpay/onboarding/machine/provider.tsx b/ts/features/idpay/onboarding/machine/provider.tsx index 24a09970e2f..13ffc62716e 100644 --- a/ts/features/idpay/onboarding/machine/provider.tsx +++ b/ts/features/idpay/onboarding/machine/provider.tsx @@ -55,8 +55,8 @@ export const IdPayOnboardingMachineProvider = ({ children }: Props) => { isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); - const actors = createActorsImplementation(client, token, language, dispatch); - const actions = createActionsImplementation(navigation); + const actors = createActorsImplementation(client, token, language); + const actions = createActionsImplementation(navigation, dispatch); const machine = idPayOnboardingMachine.provide({ actors, From 0862620407e18d0cf13a4b6c9a96ae6f058d4ed3 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 16:45:05 +0200 Subject: [PATCH 23/31] chore: fix selectors --- ts/features/idpay/common/machine/selectors.ts | 8 ++ ts/features/idpay/common/machine/tags.ts | 3 +- .../idpay/configuration/machine/actors.ts | 85 ++++++++++--------- .../idpay/configuration/machine/machine.ts | 21 ++--- .../idpay/configuration/machine/selectors.ts | 5 -- .../configuration/navigation/navigator.tsx | 26 +++++- .../IbanConfigurationLandingScreen.tsx | 5 -- .../screens/IbanEnrollmentScreen.tsx | 13 ++- .../screens/IbanOnboardingScreen.tsx | 7 +- .../InitiativeConfigurationIntroScreen.tsx | 4 +- .../screens/InstrumentsEnrollmentScreen.tsx | 7 +- .../InitiativeDiscountSettingsComponent.tsx | 18 ++-- .../InitiativeRefundSettingsComponent.tsx | 12 ++- .../components/MissingConfigurationAlert.tsx | 4 +- .../idpay/onboarding/machine/machine.ts | 11 +-- .../idpay/onboarding/navigation/navigator.tsx | 25 +++++- .../screens/BoolValuePrerequisitesScreen.tsx | 9 +- .../onboarding/screens/CompletionScreen.tsx | 2 +- .../screens/InitiativeDetailsScreen.tsx | 4 +- .../screens/MultiValuePrerequisitesScreen.tsx | 8 +- .../screens/PDNDPrerequisitesScreen.tsx | 9 +- ts/features/idpay/payment/machine/events.ts | 20 +++-- ts/features/idpay/payment/machine/machine.ts | 20 ++--- .../idpay/payment/navigation/navigator.tsx | 59 ++++++++----- .../IDPayPaymentAuthorizationScreen.tsx | 10 +-- .../screens/IDPayPaymentCodeInputScreen.tsx | 8 +- 26 files changed, 211 insertions(+), 192 deletions(-) create mode 100644 ts/features/idpay/common/machine/selectors.ts diff --git a/ts/features/idpay/common/machine/selectors.ts b/ts/features/idpay/common/machine/selectors.ts new file mode 100644 index 00000000000..41e76741f5c --- /dev/null +++ b/ts/features/idpay/common/machine/selectors.ts @@ -0,0 +1,8 @@ +import { AnyMachineSnapshot } from "xstate"; +import { IdPayTags } from "./tags"; + +export const isLoadingSelector = (snapshot: AnyMachineSnapshot) => + snapshot.hasTag(IdPayTags.Loading); + +export const isUpsertingSelector = (snapshot: AnyMachineSnapshot) => + snapshot.hasTag(IdPayTags.Upserting); diff --git a/ts/features/idpay/common/machine/tags.ts b/ts/features/idpay/common/machine/tags.ts index af2c172d7bb..00510d84117 100644 --- a/ts/features/idpay/common/machine/tags.ts +++ b/ts/features/idpay/common/machine/tags.ts @@ -1,5 +1,4 @@ export enum IdPayTags { Loading = "Loading", - Upserting = "Upserting", - WaitingUserInput = "WaitingUserInput" + Upserting = "Upserting" } diff --git a/ts/features/idpay/configuration/machine/actors.ts b/ts/features/idpay/configuration/machine/actors.ts index dafdcbe4b11..0b507a7d3c1 100644 --- a/ts/features/idpay/configuration/machine/actors.ts +++ b/ts/features/idpay/configuration/machine/actors.ts @@ -16,7 +16,7 @@ import { SessionManager } from "../../../../utils/SessionManager"; import { convertWalletV2toWalletV1 } from "../../../../utils/walletv2"; import { IDPayClient } from "../../common/api/client"; import { InitiativeFailureType } from "../types/failure"; -import * as Events from "./events"; +import { IdPayConfigurationEvents } from "./events"; export const createActorsImplementation = ( idPayClient: IDPayClient, @@ -268,47 +268,48 @@ export const createActorsImplementation = ( return data; }; - const instrumentsEnrollmentLogic = fromCallback( - ({ sendBack, receive, input }) => { - receive(event => { - switch (event.type) { - case "delete-instrument": - deleteInstrument(input, event.instrumentId) - .then(() => - sendBack({ - ...event, - type: "update-instrument-success" - }) - ) - .catch(() => - sendBack({ - ...event, - type: "update-instrument-failure" - }) - ); - break; - case "enroll-instrument": - enrollInstrument(input, event.walletId) - .then(() => - sendBack({ - ...event, - type: "update-instrument-success", - enrolling: true - }) - ) - .catch(() => - sendBack({ - ...event, - type: "update-instrument-failure" - }) - ); - break; - default: - break; - } - }); - } - ); + const instrumentsEnrollmentLogic = fromCallback< + IdPayConfigurationEvents, + string + >(({ sendBack, receive, input }) => { + receive(event => { + switch (event.type) { + case "delete-instrument": + deleteInstrument(input, event.instrumentId) + .then(() => + sendBack({ + ...event, + type: "update-instrument-success" + }) + ) + .catch(() => + sendBack({ + ...event, + type: "update-instrument-failure" + }) + ); + break; + case "enroll-instrument": + enrollInstrument(input, event.walletId) + .then(() => + sendBack({ + ...event, + type: "update-instrument-success", + enrolling: true + }) + ) + .catch(() => + sendBack({ + ...event, + type: "update-instrument-failure" + }) + ); + break; + default: + break; + } + }); + }); return { getInitiative, diff --git a/ts/features/idpay/configuration/machine/machine.ts b/ts/features/idpay/configuration/machine/machine.ts index 507903c1eee..de4e13dd18d 100644 --- a/ts/features/idpay/configuration/machine/machine.ts +++ b/ts/features/idpay/configuration/machine/machine.ts @@ -143,7 +143,7 @@ export const idPayConfigurationMachine = setup({ O.getOrElse(() => false) ), hasIbanList: ({ context }) => context.ibanList.length > 0, - hasInstruments: ({ context }) => context.walletInstruments.length > 0, + hasInstruments: ({ context }) => context.walletInstruments?.length > 0, isSessionExpired: ({ event }: { event: IdPayConfigurationEvents }) => "error" in event && event.error === InitiativeFailureType.SESSION_EXPIRED }, @@ -189,7 +189,7 @@ export const idPayConfigurationMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-configuration.SessionExpired" }, { actions: assign(({ event }) => ({ @@ -223,7 +223,6 @@ export const idPayConfigurationMachine = setup({ }, DisplayingConfigurationIntro: { - tags: [IdPayTags.WaitingUserInput], entry: "navigateToConfigurationIntro", on: { next: { @@ -251,7 +250,7 @@ export const idPayConfigurationMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-configuration.SessionExpired" }, { guard: "isIbanOnlyMode", @@ -287,7 +286,6 @@ export const idPayConfigurationMachine = setup({ }, DisplayingIbanOnboardingLanding: { - tags: [IdPayTags.WaitingUserInput], entry: "navigateToIbanOnboardingScreen", on: { next: { @@ -306,7 +304,6 @@ export const idPayConfigurationMachine = setup({ }, DisplayingIbanOnboardingForm: { - tags: [IdPayTags.WaitingUserInput], entry: "navigateToIbanOnboardingFormScreen", on: { back: [ @@ -342,7 +339,7 @@ export const idPayConfigurationMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-configuration.SessionExpired" }, { actions: [ @@ -358,7 +355,6 @@ export const idPayConfigurationMachine = setup({ }, DisplayingIbanList: { - tags: [IdPayTags.WaitingUserInput], entry: "navigateToIbanEnrollmentScreen", on: { back: [ @@ -404,7 +400,7 @@ export const idPayConfigurationMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-configuration.SessionExpired" }, { target: "DisplayingIbanList", @@ -455,7 +451,7 @@ export const idPayConfigurationMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-configuration.SessionExpired" }, { guard: "isInstrumentsOnlyMode", @@ -499,7 +495,7 @@ export const idPayConfigurationMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-configuration.SessionExpired" }, { guard: "isInstrumentsOnlyMode", @@ -542,7 +538,6 @@ export const idPayConfigurationMachine = setup({ }, DisplayingInstruments: { - tags: [IdPayTags.WaitingUserInput], entry: "updateAllInstrumentsStatus", initial: "DisplayingInstrument", invoke: { @@ -646,7 +641,6 @@ export const idPayConfigurationMachine = setup({ }, DisplayingConfigurationSuccess: { - tags: [IdPayTags.WaitingUserInput], entry: "navigateToConfigurationSuccessScreen", on: { next: { @@ -656,7 +650,6 @@ export const idPayConfigurationMachine = setup({ }, ConfigurationNotNeeded: { - tags: [IdPayTags.WaitingUserInput], entry: "navigateToConfigurationSuccessScreen", on: { next: { diff --git a/ts/features/idpay/configuration/machine/selectors.ts b/ts/features/idpay/configuration/machine/selectors.ts index 009b343e9e0..3806a0cea1e 100644 --- a/ts/features/idpay/configuration/machine/selectors.ts +++ b/ts/features/idpay/configuration/machine/selectors.ts @@ -3,7 +3,6 @@ import * as O from "fp-ts/lib/Option"; import { createSelector } from "reselect"; import { StateFrom } from "xstate"; import { InstrumentDTO } from "../../../../../definitions/idpay/InstrumentDTO"; -import { LOADING_TAG } from "../../../../xstate/utils"; import { ConfigurationMode } from "../types"; import { IdPayConfigurationMachine } from "./machine"; @@ -13,9 +12,6 @@ type IDPayInstrumentsByIdWallet = { [idWallet: string]: InstrumentDTO; }; -const isLoadingSelector = (snapshot: MachineSnapshot) => - snapshot.hasTag(LOADING_TAG as never); - const selectInitiativeDetails = (snapshot: MachineSnapshot) => snapshot.context.initiative; @@ -86,7 +82,6 @@ export { initiativeInstrumentsByIdWalletSelector, instrumentStatusByIdWalletSelector, isLoadingIbanListSelector, - isLoadingSelector, isUpsertingInstrumentSelector, selectAreInstrumentsSkipped, selectEnrolledIban, diff --git a/ts/features/idpay/configuration/navigation/navigator.tsx b/ts/features/idpay/configuration/navigation/navigator.tsx index c5d50439e38..9b7cd213795 100644 --- a/ts/features/idpay/configuration/navigation/navigator.tsx +++ b/ts/features/idpay/configuration/navigation/navigator.tsx @@ -1,7 +1,10 @@ import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; import { isGestureEnabled } from "../../../../utils/navigation"; -import { IDPayConfigurationMachineProvider } from "../machine/provider"; +import { + IdPayConfigurationMachineContext, + IDPayConfigurationMachineProvider +} from "../machine/provider"; import { ConfigurationSuccessScreen } from "../screens/ConfigurationSuccessScreen"; import { IbanConfigurationLanding } from "../screens/IbanConfigurationLandingScreen"; import { IbanEnrollmentScreen } from "../screens/IbanEnrollmentScreen"; @@ -16,9 +19,26 @@ const Stack = createStackNavigator(); export const IdPayConfigurationNavigator = () => ( + + +); + +const InnerNavigator = () => { + const idPayConfigurationMachineRef = + IdPayConfigurationMachineContext.useActorRef(); + + return ( { + // Read more on https://reactnavigation.org/docs/preventing-going-back/ + // Whenever we have a back navigation action we send a "back" event to the machine. + // Since the back event is accepted only by specific states, we can safely send a back event to each machine + idPayConfigurationMachineRef.send({ type: "back" }); + } + }} > ( component={IdPayDiscountInstrumentsScreen} /> - -); + ); +}; diff --git a/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx b/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx index 51d8a604ed8..3507eb1338a 100644 --- a/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanConfigurationLandingScreen.tsx @@ -11,7 +11,6 @@ import { Body } from "../../../../components/core/typography/Body"; import { H3 } from "../../../../components/core/typography/H3"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { isSettingsVisibleAndHideProfileSelector } from "../../../../store/reducers/backendStatus"; @@ -28,10 +27,6 @@ export const IbanConfigurationLanding = () => { const customGoBack = () => machine.send({ type: "back" }); - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - const { bottomSheet, dismiss, present } = useIOBottomSheetAutoresizableModal( { title: I18n.t("idpay.configuration.iban.landing.modal.title"), diff --git a/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx b/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx index a46a16d9a8f..22e6bb44cdc 100644 --- a/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanEnrollmentScreen.tsx @@ -15,17 +15,18 @@ import { LabelSmall } from "../../../../components/core/typography/LabelSmall"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import ListItemComponent from "../../../../components/screens/ListItemComponent"; -import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { isSettingsVisibleAndHideProfileSelector } from "../../../../store/reducers/backendStatus"; import customVariables from "../../../../theme/variables"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { isUpseringSelector } from "../../../../xstate/selectors"; +import { + isLoadingSelector, + isUpsertingSelector +} from "../../common/machine/selectors"; import { IdPayConfigurationMachineContext } from "../machine/provider"; import { ibanListSelector, - isLoadingSelector, selectEnrolledIban, selectIsIbanOnlyMode } from "../machine/selectors"; @@ -53,7 +54,7 @@ export const IbanEnrollmentScreen = () => { const isIbanOnly = IdPayConfigurationMachineContext.useSelector(selectIsIbanOnlyMode); const isUpsertingIban = - IdPayConfigurationMachineContext.useSelector(isUpseringSelector); + IdPayConfigurationMachineContext.useSelector(isUpsertingSelector); const enrolledIban = IdPayConfigurationMachineContext.useSelector(selectEnrolledIban); @@ -106,10 +107,6 @@ export const IbanEnrollmentScreen = () => { machine.send({ type: "new-iban-onboarding" }); }; - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - const renderFooter = () => { if (isIbanOnly) { return ( diff --git a/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx b/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx index 815c4d28f86..ce5c513e90d 100644 --- a/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx +++ b/ts/features/idpay/configuration/screens/IbanOnboardingScreen.tsx @@ -16,13 +16,12 @@ import { LabelSmall } from "../../../../components/core/typography/LabelSmall"; import { Link } from "../../../../components/core/typography/Link"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { useIOSelector } from "../../../../store/hooks"; import { isSettingsVisibleAndHideProfileSelector } from "../../../../store/reducers/backendStatus"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { isLoadingSelector } from "../../common/machine/selectors"; import { IdPayConfigurationMachineContext } from "../machine/provider"; -import { isLoadingSelector } from "../machine/selectors"; export const IbanOnboardingScreen = () => { const machine = IdPayConfigurationMachineContext.useActorRef(); @@ -40,10 +39,6 @@ export const IbanOnboardingScreen = () => { isSettingsVisibleAndHideProfileSelector ); - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - const isInputValid = O.isSome(iban.value) && ibanName.length > 0; return ( diff --git a/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx b/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx index 12e75858805..600c0329166 100644 --- a/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx +++ b/ts/features/idpay/configuration/screens/InitiativeConfigurationIntroScreen.tsx @@ -25,10 +25,10 @@ import { H4 } from "../../../../components/core/typography/H4"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { isLoadingSelector } from "../../../../xstate/selectors"; import { IdPayConfigurationMachineContext } from "../machine/provider"; -import { ConfigurationMode } from "../types"; import { IdPayConfigurationParamsList } from "../navigation/params"; +import { ConfigurationMode } from "../types"; +import { isLoadingSelector } from "../../common/machine/selectors"; export type IdPayInitiativeConfigurationIntroScreenParams = { initiativeId?: string; diff --git a/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx b/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx index a322e154024..0a010a11096 100644 --- a/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx +++ b/ts/features/idpay/configuration/screens/InstrumentsEnrollmentScreen.tsx @@ -13,7 +13,6 @@ import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay" import { Body } from "../../../../components/core/typography/Body"; import { H1 } from "../../../../components/core/typography/H1"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import ROUTES from "../../../../navigation/routes"; @@ -25,7 +24,6 @@ import { IdPayConfigurationMachineContext } from "../machine/provider"; import { failureSelector, initiativeInstrumentsByIdWalletSelector, - isLoadingSelector, isUpsertingInstrumentSelector, selectInitiativeDetails, selectIsInstrumentsOnlyMode, @@ -34,6 +32,7 @@ import { import { IdPayConfigurationParamsList } from "../navigation/params"; import { ConfigurationMode } from "../types"; import { InitiativeFailureType } from "../types/failure"; +import { isLoadingSelector } from "../../common/machine/selectors"; export type IdPayInstrumentsEnrollmentScreenParams = { initiativeId?: string; @@ -230,10 +229,6 @@ export const InstrumentsEnrollmentScreen = () => { ); }; - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - const handleInstrumentValueChange = (wallet: Wallet) => (value: boolean) => { if (value) { setStagedWalletId(wallet.idWallet); diff --git a/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx b/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx index c9dfd1dc360..91afdd0dac0 100644 --- a/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx @@ -22,17 +22,19 @@ const InitiativeDiscountSettingsComponent = (props: Props) => { const navigation = useNavigation>(); - const navigateToInstrumentsConfiguration = (initiative: InitiativeDTO) => { + const navigateToInstrumentsConfiguration = ({ + initiativeId, + initiativeName + }: InitiativeDTO) => { navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_DISCOUNT_INSTRUMENTS, params: { - initiativeId: initiative.initiativeId, - initiativeName: initiative.initiativeName - }, - initiativeId: initiative.initiativeId + initiativeId, + initiativeName + } } ); }; @@ -53,15 +55,15 @@ const InitiativeDiscountSettingsComponent = (props: Props) => { onPress={() => null} /> ), - initiative => { + ({ nInstr }) => { const methodCountString = I18n.t( `idpay.initiative.details.initiativeDetailsScreen.configured.settings.methods`, { defaultValue: I18n.t( `idpay.initiative.details.initiativeDetailsScreen.configured.settings.methods.other`, - { count: initiative.nInstr } + { count: nInstr } ), - count: initiative.nInstr + count: nInstr } ); return ( diff --git a/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx b/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx index c58b9852572..cdd89191411 100644 --- a/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx @@ -20,7 +20,6 @@ import { } from "../../../../navigation/params/AppParamsList"; import { Skeleton } from "../../common/components/Skeleton"; import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; -import { ConfigurationMode } from "../../configuration/types"; type Props = { initiative?: InitiativeDTO; @@ -35,10 +34,10 @@ const InitiativeRefundSettingsComponent = (props: Props) => { navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { - screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + screen: + IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INSTRUMENTS_ENROLLMENT, params: { - initiativeId, - mode: ConfigurationMode.INSTRUMENTS + initiativeId } } ); @@ -48,10 +47,9 @@ const InitiativeRefundSettingsComponent = (props: Props) => { navigation.navigate( IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, { - screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, + screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_IBAN_ENROLLMENT, params: { - initiativeId, - mode: ConfigurationMode.IBAN + initiativeId } } ); diff --git a/ts/features/idpay/details/components/MissingConfigurationAlert.tsx b/ts/features/idpay/details/components/MissingConfigurationAlert.tsx index ed69a23627e..0aa1dc82255 100644 --- a/ts/features/idpay/details/components/MissingConfigurationAlert.tsx +++ b/ts/features/idpay/details/components/MissingConfigurationAlert.tsx @@ -1,6 +1,7 @@ -import React from "react"; import { Alert, VSpacer } from "@pagopa/io-app-design-system"; import { NavigatorScreenParams } from "@react-navigation/native"; +import React from "react"; +import { View } from "react-native"; import { StatusEnum as InitiativeStatusEnum } from "../../../../../definitions/idpay/InitiativeDTO"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; @@ -57,6 +58,7 @@ const MissingConfigurationAlert = (props: Props) => { return ( <> (); export const IdPayOnboardingNavigator = () => ( + + +); + +export const InnerNavigator = () => { + const idPayOnboardingMachineRef = IdPayOnboardingMachineContext.useActorRef(); + + return ( { + // Read more on https://reactnavigation.org/docs/preventing-going-back/ + // Whenever we have a back navigation action we send a "back" event to the machine. + // Since the back event is accepted only by specific states, we can safely send a back event to each machine + idPayOnboardingMachineRef.send({ type: "back" }); + } + }} > ( options={{ gestureEnabled: false }} /> - -); + ); +}; diff --git a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx index 9fe6d8a2478..fac529c42b4 100644 --- a/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/BoolValuePrerequisitesScreen.tsx @@ -9,19 +9,18 @@ import { H1 } from "../../../../components/core/typography/H1"; import { Link } from "../../../../components/core/typography/Link"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import ListItemComponent from "../../../../components/screens/ListItemComponent"; -import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; +import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; import { dpr28Dec2000Url } from "../../../../urls"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; import { openWebUrl } from "../../../../utils/url"; -import { isLoadingSelector } from "../../../../xstate/selectors"; import { IdPayOnboardingMachineContext } from "../machine/provider"; import { areAllSelfDeclarationsToggledSelector, boolRequiredCriteriaSelector, selectSelfDeclarationBoolAnswers } from "../machine/selectors"; -import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; +import { isLoadingSelector } from "../../common/machine/selectors"; const InitiativeSelfDeclarationsScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; @@ -48,10 +47,6 @@ const InitiativeSelfDeclarationsScreen = () => { const getSelfCriteriaBoolAnswer = (criteria: SelfDeclarationBoolDTO) => selfCriteriaBoolAnswers[criteria.code] ?? false; - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - useHeaderSecondLevel({ title: I18n.t("idpay.onboarding.navigation.header"), contextualHelp: emptyContextualHelp, diff --git a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx index 039779cdd9c..83aaab45a3a 100644 --- a/ts/features/idpay/onboarding/screens/CompletionScreen.tsx +++ b/ts/features/idpay/onboarding/screens/CompletionScreen.tsx @@ -12,8 +12,8 @@ import { IOStyles } from "../../../../components/core/variables/IOStyles"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../i18n"; import themeVariables from "../../../../theme/variables"; -import { isLoadingSelector } from "../../../../xstate/selectors"; import { IdPayOnboardingMachineContext } from "../machine/provider"; +import { isLoadingSelector } from "../../common/machine/selectors"; const CompletionScreen = () => { const { useActorRef, useSelector } = IdPayOnboardingMachineContext; diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 612ee100bce..00f664ea7ca 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -6,10 +6,11 @@ import * as React from "react"; import { StyleSheet, View } from "react-native"; import { ForceScrollDownView } from "../../../../components/ForceScrollDownView"; import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; +import { FooterActions } from "../../../../components/ui/FooterActions"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { isLoadingSelector } from "../../../../xstate/selectors"; +import { isLoadingSelector } from "../../common/machine/selectors"; import { OnboardingDescriptionMarkdown, OnboardingDescriptionMarkdownSkeleton @@ -19,7 +20,6 @@ import { OnboardingServiceHeader } from "../components/OnboardingServiceHeader"; import { IdPayOnboardingMachineContext } from "../machine/provider"; import { selectInitiative } from "../machine/selectors"; import { IdPayOnboardingParamsList } from "../navigation/params"; -import { FooterActions } from "../../../../components/ui/FooterActions"; export type InitiativeDetailsScreenParams = { serviceId?: string; diff --git a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx index f829b20fc5c..082fae82e57 100644 --- a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx @@ -16,7 +16,6 @@ import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDe import { H4 } from "../../../../components/core/typography/H4"; import { Link } from "../../../../components/core/typography/Link"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; -import { useNavigationSwipeBackListener } from "../../../../hooks/useNavigationSwipeBackListener"; import I18n from "../../../../i18n"; import { IdPayOnboardingMachineContext } from "../machine/provider"; import { @@ -32,8 +31,7 @@ type ListItemProps = { const MultiValuePrerequisitesScreen = () => { const pagerRef = React.useRef(null); - const { useActorRef, useSelector } = IdPayOnboardingMachineContext; - const machine = useActorRef(); + const { useSelector } = IdPayOnboardingMachineContext; const multiSelfDeclarations = useSelector(multiRequiredCriteriaSelector); const currentPage = useSelector(selectCurrentMultiSelfDeclarationPage); @@ -42,10 +40,6 @@ const MultiValuePrerequisitesScreen = () => { pagerRef.current?.setPage(currentPage); }, [pagerRef, currentPage]); - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - return ( { const pdndCriteria = useSelector(pdndCriteriaSelector); - useNavigationSwipeBackListener(() => { - machine.send({ type: "back", skipNavigation: true }); - }); - useHeaderSecondLevel({ title: I18n.t("idpay.onboarding.navigation.header"), contextualHelp: emptyContextualHelp, diff --git a/ts/features/idpay/payment/machine/events.ts b/ts/features/idpay/payment/machine/events.ts index 0f5fad3b042..075266e13ec 100644 --- a/ts/features/idpay/payment/machine/events.ts +++ b/ts/features/idpay/payment/machine/events.ts @@ -1,8 +1,18 @@ -import { GlobalEvents } from "../../../../xstate/types/events"; - -export interface AuthorizePayment { +export type AuthorizePayment = { readonly type: "authorize-payment"; readonly trxCode: string; -} +}; + +export type Next = { + readonly type: "next"; +}; + +export type Back = { + readonly type: "back"; +}; + +export type Close = { + readonly type: "close"; +}; -export type Events = GlobalEvents | AuthorizePayment; +export type Events = Next | Back | Close | AuthorizePayment; diff --git a/ts/features/idpay/payment/machine/machine.ts b/ts/features/idpay/payment/machine/machine.ts index c7ec5cb7db8..e66fe743a10 100644 --- a/ts/features/idpay/payment/machine/machine.ts +++ b/ts/features/idpay/payment/machine/machine.ts @@ -3,17 +3,16 @@ import * as O from "fp-ts/lib/Option"; import { flow, pipe } from "fp-ts/lib/function"; import { assertEvent, assign, fromPromise, setup } from "xstate"; import { AuthPaymentResponseDTO } from "../../../../../definitions/idpay/AuthPaymentResponseDTO"; -import { - LOADING_TAG, - UPSERTING_TAG, - WAITING_USER_INPUT_TAG, - notImplementedStub -} from "../../../../xstate/utils"; +import { IdPayTags } from "../../common/machine/tags"; import { IDPayTransactionCode } from "../common/types"; import { PaymentFailure, PaymentFailureEnum } from "../types/PaymentFailure"; import * as Context from "./context"; import * as Events from "./events"; +const notImplementedStub = () => { + throw new Error("Not implemented"); +}; + export const idPayPaymentMachine = setup({ types: { context: {} as Context.Context, @@ -52,7 +51,7 @@ export const idPayPaymentMachine = setup({ initial: "Idle", states: { Idle: { - tags: [LOADING_TAG], + tags: [IdPayTags.Loading], on: { "authorize-payment": { guard: "asserTransactionCode", @@ -62,7 +61,7 @@ export const idPayPaymentMachine = setup({ } }, PreAuthorizing: { - tags: [UPSERTING_TAG], + tags: [IdPayTags.Loading], invoke: { id: "preAuthorizePayment", src: "preAuthorizePayment", @@ -86,7 +85,6 @@ export const idPayPaymentMachine = setup({ }, AwaitingConfirmation: { - tags: [WAITING_USER_INPUT_TAG], entry: "navigateToAuthorizationScreen", on: { next: { @@ -99,7 +97,7 @@ export const idPayPaymentMachine = setup({ }, Cancelling: { - tags: [UPSERTING_TAG], + tags: [IdPayTags.Loading], invoke: { id: "deletePayment", src: "deletePayment", @@ -124,7 +122,7 @@ export const idPayPaymentMachine = setup({ }, Authorizing: { - tags: [UPSERTING_TAG], + tags: [IdPayTags.Loading], invoke: { id: "authorizePayment", src: "authorizePayment", diff --git a/ts/features/idpay/payment/navigation/navigator.tsx b/ts/features/idpay/payment/navigation/navigator.tsx index dcf2a1f82b9..24389edb81f 100644 --- a/ts/features/idpay/payment/navigation/navigator.tsx +++ b/ts/features/idpay/payment/navigation/navigator.tsx @@ -1,6 +1,9 @@ import { createStackNavigator } from "@react-navigation/stack"; import React from "react"; -import { IdPayPaymentMachineProvider } from "../machine/provider"; +import { + IdPayPaymentMachineContext, + IdPayPaymentMachineProvider +} from "../machine/provider"; import { IDPayPaymentAuthorizationScreen } from "../screens/IDPayPaymentAuthorizationScreen"; import { IDPayPaymentCodeInputScreen } from "../screens/IDPayPaymentCodeInputScreen"; import { IDPayPaymentResultScreen } from "../screens/IDPayPaymentResultScreen"; @@ -11,23 +14,41 @@ const Stack = createStackNavigator(); export const IdPayPaymentNavigator = () => ( - - - - - + ); + +const InnerNavigation = () => { + const idPayPaymentMachineRef = IdPayPaymentMachineContext.useActorRef(); + + return ( + + { + // Read more on https://reactnavigation.org/docs/preventing-going-back/ + // Whenever we have a back navigation action we send a "back" event to the machine. + // Since the back event is accepted only by specific states, we can safely send a back event to each machine + idPayPaymentMachineRef.send({ type: "back" }); + } + }} + > + + + + + + ); +}; diff --git a/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx index 2dffd987b18..de4ee8b300c 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentAuthorizationScreen.tsx @@ -22,10 +22,6 @@ import I18n from "../../../../i18n"; import { identificationRequest } from "../../../../store/actions/identification"; import { useIODispatch } from "../../../../store/hooks"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { - isLoadingSelector, - isUpseringSelector -} from "../../../../xstate/selectors"; import { Skeleton } from "../../common/components/Skeleton"; import { formatDateOrDefault, @@ -39,6 +35,7 @@ import { transactionDataSelector } from "../machine/selectors"; import { IdPayPaymentParamsList } from "../navigation/params"; +import { isLoadingSelector } from "../../common/machine/selectors"; export type IDPayPaymentAuthorizationScreenRouteParams = { trxCode?: string; @@ -65,7 +62,6 @@ const IDPayPaymentAuthorizationScreen = () => { const transactionData = useSelector(transactionDataSelector); const isLoading = useSelector(isLoadingSelector); - const isUpserting = useSelector(isUpseringSelector); const isAuthorizing = useSelector(isAuthorizingSelector); const isCancelling = useSelector(isCancellingSelector); @@ -119,7 +115,7 @@ const IDPayPaymentAuthorizationScreen = () => { buttonProps: { label: isCancelling ? "" : I18n.t("global.buttons.deny"), onPress: handleCancel, - disabled: isUpserting || isLoading + disabled: isLoading } }} secondary={{ @@ -128,7 +124,7 @@ const IDPayPaymentAuthorizationScreen = () => { label: I18n.t("global.buttons.confirm"), onPress: handleConfirm, loading: isAuthorizing, - disabled: isUpserting || isLoading + disabled: isLoading } }} /> diff --git a/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx b/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx index f57a96e9d5d..00aa57f8dd3 100644 --- a/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx +++ b/ts/features/idpay/payment/screens/IDPayPaymentCodeInputScreen.tsx @@ -17,7 +17,7 @@ import { H1 } from "../../../../components/core/typography/H1"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import I18n from "../../../../i18n"; import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; -import { isUpseringSelector } from "../../../../xstate/selectors"; +import { isLoadingSelector } from "../../common/machine/selectors"; import { IDPayTransactionCode } from "../common/types"; import { IdPayPaymentMachineContext } from "../machine/provider"; @@ -36,7 +36,7 @@ const IDPayPaymentCodeInputScreen = () => { }); const isInputValid = pipe(inputState.code, O.map(E.isRight), O.toUndefined); - const isUpserting = useSelector(isUpseringSelector); + const isLoading = useSelector(isLoadingSelector); const navigateToPaymentAuthorization = () => pipe( @@ -89,9 +89,9 @@ const IDPayPaymentCodeInputScreen = () => { buttonProps: { label: I18n.t("idpay.payment.manualInput.button"), accessibilityLabel: I18n.t("idpay.payment.manualInput.button"), - disabled: !isInputValid || isUpserting, + disabled: !isInputValid || isLoading, onPress: navigateToPaymentAuthorization, - loading: isUpserting + loading: isLoading } }} /> From 328a2791822c5bd1445ce636f5ec9558e3200445 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 2 Sep 2024 17:30:33 +0200 Subject: [PATCH 24/31] chore: add config machine tests --- .../machine/__tests__/machine.test.ts | 807 ++++++++++++++++++ 1 file changed, 807 insertions(+) create mode 100644 ts/features/idpay/configuration/machine/__tests__/machine.test.ts diff --git a/ts/features/idpay/configuration/machine/__tests__/machine.test.ts b/ts/features/idpay/configuration/machine/__tests__/machine.test.ts new file mode 100644 index 00000000000..a204a361751 --- /dev/null +++ b/ts/features/idpay/configuration/machine/__tests__/machine.test.ts @@ -0,0 +1,807 @@ +import { waitFor } from "@testing-library/react-native"; +import * as O from "fp-ts/lib/Option"; +import { createActor, fromCallback, fromPromise } from "xstate"; +import { IbanDTO } from "../../../../../../definitions/idpay/IbanDTO"; +import { IbanListDTO } from "../../../../../../definitions/idpay/IbanListDTO"; +import { IbanPutDTO } from "../../../../../../definitions/idpay/IbanPutDTO"; +import { + InitiativeDTO, + StatusEnum +} from "../../../../../../definitions/idpay/InitiativeDTO"; +import { + InstrumentDTO, + InstrumentTypeEnum +} from "../../../../../../definitions/idpay/InstrumentDTO"; +import { TypeEnum } from "../../../../../../definitions/pagopa/Wallet"; +import { Wallet } from "../../../../../types/pagopa"; +import { IdPayTags } from "../../../common/machine/tags"; +import { ConfigurationMode } from "../../types"; +import { Context, InitialContext } from "../context"; +import { IdPayConfigurationEvents } from "../events"; +import { idPayConfigurationMachine } from "../machine"; +import { InitiativeFailureType } from "../../types/failure"; + +export const T_INITIATIVE_ID = "123456"; +export const T_IBAN = "IT60X0542811101000000123456"; +export const T_INSTRUMENT_ID = "123456"; + +export const T_WALLET: Wallet = { + idWallet: 123, + type: TypeEnum.CREDIT_CARD, + favourite: false, + creditCard: undefined, + psp: undefined, + idPsp: undefined, + pspEditable: false, + lastUsage: undefined, + isPspToIgnore: false, + registeredNexi: false, + saved: true +}; + +export const T_INSTRUMENT_DTO: InstrumentDTO = { + instrumentId: "1234", + idWallet: "12345", + instrumentType: InstrumentTypeEnum.CARD +}; + +export const T_NOT_REFUNDABLE_INITIATIVE_DTO: InitiativeDTO = { + initiativeId: T_INITIATIVE_ID, + status: StatusEnum.NOT_REFUNDABLE, + endDate: new Date("2023-01-25T13:00:25.477Z"), + nInstr: 1 +}; + +export const T_REFUNDABLE_INITIATIVE_DTO: InitiativeDTO = { + initiativeId: T_INITIATIVE_ID, + status: StatusEnum.REFUNDABLE, + endDate: new Date("2023-01-25T13:00:25.477Z"), + nInstr: 1 +}; + +export const T_IBAN_LIST: IbanListDTO["ibanList"] = [ + { + channel: "IO", + checkIbanStatus: "", + description: "Test", + iban: T_IBAN + } +]; + +export const T_PAGOPA_INSTRUMENTS = [T_WALLET]; + +const T_IBAN_ENROLL: IbanDTO = { + channel: "IO", + checkIbanStatus: "", + description: "Test", + iban: T_IBAN +}; + +describe("IDPay configuration machine", () => { + const exitConfiguration = jest.fn(); + const navigateToConfigurationIntro = jest.fn(); + const navigateToIbanEnrollmentScreen = jest.fn(); + const navigateToIbanOnboardingScreen = jest.fn(); + const navigateToIbanOnboardingFormScreen = jest.fn(); + const showUpdateIbanToast = jest.fn(); + const navigateToInstrumentsEnrollmentScreen = jest.fn(); + const navigateToConfigurationSuccessScreen = jest.fn(); + const navigateToInitiativeDetailScreen = jest.fn(); + const handleSessionExpired = jest.fn(); + const showFailureToast = jest.fn(); + + const getInitiative = jest.fn(); + const getIbanList = jest.fn(); + const enrollIban = jest.fn(); + const getWalletInstruments = jest.fn(); + const getInitiativeInstruments = jest.fn(); + const instrumentsEnrollmentLogic = jest.fn(); + + const mockedMachine = idPayConfigurationMachine.provide({ + actions: { + exitConfiguration, + navigateToConfigurationIntro, + navigateToIbanEnrollmentScreen, + navigateToIbanOnboardingScreen, + navigateToIbanOnboardingFormScreen, + showUpdateIbanToast, + navigateToInstrumentsEnrollmentScreen, + navigateToConfigurationSuccessScreen, + navigateToInitiativeDetailScreen, + handleSessionExpired, + showFailureToast + }, + actors: { + getInitiative: fromPromise(getInitiative), + getIbanList: fromPromise(getIbanList), + enrollIban: fromPromise< + undefined, + { initiativeId: string; iban: IbanDTO | IbanPutDTO } + >(enrollIban), + getWalletInstruments: + fromPromise>(getWalletInstruments), + getInitiativeInstruments: fromPromise< + ReadonlyArray, + string + >(getInitiativeInstruments), + instrumentsEnrollmentLogic: fromCallback< + IdPayConfigurationEvents, + string + >(instrumentsEnrollmentLogic) + } + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should start with the initial state", () => { + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toStrictEqual("Idle"); + expect(actor.getSnapshot().context).toStrictEqual(InitialContext); + expect(actor.getSnapshot().tags).toStrictEqual( + new Set([IdPayTags.Loading]) + ); + }); + + it("should not allow the citizen to configure an initiative if it's already configured", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_REFUNDABLE_INITIATIVE_DTO) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toStrictEqual("Idle"); + expect(actor.getSnapshot().context).toStrictEqual(InitialContext); + expect(actor.getSnapshot().tags).toStrictEqual( + new Set([IdPayTags.Loading]) + ); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().context).toMatchObject>({ + initiative: O.some(T_REFUNDABLE_INITIATIVE_DTO) + }); + + await waitFor(() => + expect(navigateToConfigurationSuccessScreen).toHaveBeenCalledTimes(1) + ); + + expect(actor.getSnapshot().value).toMatch("ConfigurationNotNeeded"); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + + actor.send({ + type: "next" + }); + + expect(actor.getSnapshot().value).toMatch("ConfigurationCompleted"); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + + await waitFor(() => + expect(navigateToInitiativeDetailScreen).toHaveBeenCalledTimes(1) + ); + }); + + it("should allow the citizen to configure an initiative", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: T_IBAN_LIST }) + ); + + enrollIban.mockImplementation(async () => Promise.resolve(undefined)); + + getWalletInstruments.mockImplementation(async () => + Promise.resolve(T_PAGOPA_INSTRUMENTS) + ); + + getInitiativeInstruments.mockImplementation(async () => + Promise.resolve([]) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toEqual("DisplayingConfigurationIntro"); + expect(actor.getSnapshot().context).toMatchObject>({ + initiative: O.some(T_NOT_REFUNDABLE_INITIATIVE_DTO) + }); + expect(actor.getSnapshot().tags).toStrictEqual(new Set()); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + expect(actor.getSnapshot().context).toMatchObject>({ + ibanList: T_IBAN_LIST + }); + + await waitFor( + // Called twice: once from the parent state, once from the child state + () => expect(navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes(2) + ); + + actor.send({ + type: "enroll-iban", + iban: T_IBAN_ENROLL + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + await waitFor(() => expect(getWalletInstruments).toHaveBeenCalledTimes(1)); + + await waitFor(() => + expect(getInitiativeInstruments).toHaveBeenCalledTimes(1) + ); + + expect(actor.getSnapshot().context).toMatchObject>({ + walletInstruments: T_PAGOPA_INSTRUMENTS + }); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringInstruments: { + DisplayingInstruments: "DisplayingInstrument" + } + }); + + await waitFor(() => + expect(navigateToInstrumentsEnrollmentScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "enroll-instrument", + walletId: T_WALLET.idWallet.toString() + }); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringInstruments: { + DisplayingInstruments: "DisplayingInstrument" + } + }); + + await waitFor(() => + expect(navigateToInstrumentsEnrollmentScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "delete-instrument", + walletId: T_WALLET.idWallet.toString(), + instrumentId: T_INSTRUMENT_DTO.instrumentId + }); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringInstruments: { + DisplayingInstruments: "DisplayingInstrument" + } + }); + + await waitFor(() => + expect(navigateToInstrumentsEnrollmentScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "next" + }); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationSuccess"); + + await waitFor(() => + expect(navigateToConfigurationSuccessScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "next" + }); + + expect(actor.getSnapshot().value).toMatch("ConfigurationCompleted"); + + await waitFor(() => + expect(navigateToInitiativeDetailScreen).toHaveBeenCalledTimes(1) + ); + }); + + it("should allow a citizen without any IBAN to configure an initiative", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: [] }) + ); + + enrollIban.mockImplementation(async () => Promise.resolve()); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanOnboardingLanding" + }); + + await waitFor(() => + expect(navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanOnboardingForm" + }); + + await waitFor(() => + expect(navigateToIbanOnboardingScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "confirm-iban-onboarding", + ibanBody: { + description: "Test", + iban: T_IBAN + } + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + // From here same as previous test case + }); + + it("should allow a citizen without any instrument to configure an initiative", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: T_IBAN_LIST }) + ); + + enrollIban.mockImplementation(async () => Promise.resolve(undefined)); + + getWalletInstruments.mockImplementation(async () => Promise.resolve([])); + + getInitiativeInstruments.mockImplementation(async () => + Promise.resolve([]) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + + await waitFor(() => + expect(navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes(2) + ); + + actor.send({ + type: "enroll-iban", + iban: T_IBAN_ENROLL + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + await waitFor(() => + expect(navigateToInstrumentsEnrollmentScreen).toHaveBeenCalledTimes(1) + ); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationSuccess"); + }); + + it("should allow the citizen to configure an initiative skipping the instrument step", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: T_IBAN_LIST }) + ); + + enrollIban.mockImplementation(async () => Promise.resolve(undefined)); + + getWalletInstruments.mockImplementation(async () => + Promise.resolve(T_PAGOPA_INSTRUMENTS) + ); + + getInitiativeInstruments.mockImplementation(async () => + Promise.resolve([]) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + + await waitFor(() => + expect(navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes(2) + ); + + actor.send({ + type: "enroll-iban", + iban: T_IBAN_ENROLL + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + await waitFor(() => expect(getWalletInstruments).toHaveBeenCalledTimes(1)); + + await waitFor(() => + expect(getInitiativeInstruments).toHaveBeenCalledTimes(1) + ); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringInstruments: { + DisplayingInstruments: "DisplayingInstrument" + } + }); + + await waitFor(() => + expect(navigateToInstrumentsEnrollmentScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "skip-instruments" + }); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationSuccess"); + + await waitFor(() => + expect(navigateToConfigurationSuccessScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "next" + }); + + expect(actor.getSnapshot().value).toMatch("ConfigurationCompleted"); + + await waitFor(() => + expect(navigateToInitiativeDetailScreen).toHaveBeenCalledTimes(1) + ); + }); + + it("should go to CONFIGURATION_FAILURE if initiative fails to load", async () => { + getInitiative.mockImplementation(async () => + Promise.reject(InitiativeFailureType.GENERIC) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toEqual("ConfigurationFailure"); + }); + + it("should show a failure toast if IBAN list fails to load", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.reject(InitiativeFailureType.IBAN_LIST_LOAD_FAILURE) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + await waitFor(() => expect(showFailureToast).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + }); + + it("should show a failure toast if IBAN fails to enroll", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: T_IBAN_LIST }) + ); + + enrollIban.mockImplementation(async () => + Promise.reject(InitiativeFailureType.IBAN_ENROLL_FAILURE) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + + await waitFor(() => + expect(navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes(2) + ); + + actor.send({ + type: "enroll-iban", + iban: T_IBAN_ENROLL + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + await waitFor(() => expect(showFailureToast).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + }); + + it("should show a failure toast if IBAN fails to add", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: [] }) + ); + + enrollIban.mockImplementation(async () => + Promise.reject(InitiativeFailureType.IBAN_ENROLL_FAILURE) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanOnboardingLanding" + }); + + await waitFor(() => + expect(navigateToIbanOnboardingScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanOnboardingForm" + }); + + await waitFor(() => + expect(navigateToIbanOnboardingScreen).toHaveBeenCalledTimes(1) + ); + + actor.send({ + type: "confirm-iban-onboarding", + ibanBody: T_IBAN_ENROLL + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + await waitFor(() => expect(showFailureToast).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanOnboardingForm" + }); + }); + + it("should show a failure toast if instrument list fails to load", async () => { + getInitiative.mockImplementation(async () => + Promise.resolve(T_NOT_REFUNDABLE_INITIATIVE_DTO) + ); + + getIbanList.mockImplementation(async () => + Promise.resolve({ ibanList: T_IBAN_LIST }) + ); + + enrollIban.mockImplementation(async () => Promise.resolve(undefined)); + + getWalletInstruments.mockImplementation(async () => + Promise.reject(InitiativeFailureType.INSTRUMENTS_LIST_LOAD_FAILURE) + ); + + const actor = createActor(mockedMachine); + actor.start(); + + expect(actor.getSnapshot().value).toEqual("Idle"); + + actor.send({ + type: "start-configuration", + initiativeId: T_INITIATIVE_ID, + mode: ConfigurationMode.COMPLETE + }); + + await waitFor(() => expect(getInitiative).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatch("DisplayingConfigurationIntro"); + + await waitFor(() => + expect(navigateToConfigurationIntro).toHaveBeenCalledTimes(1) + ); + + actor.send({ type: "next" }); + + await waitFor(() => expect(getIbanList).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + + await waitFor(() => + expect(navigateToIbanEnrollmentScreen).toHaveBeenCalledTimes(2) + ); + + actor.send({ + type: "enroll-iban", + iban: T_IBAN_ENROLL + }); + + await waitFor(() => expect(enrollIban).toHaveBeenCalledTimes(1)); + + await waitFor(() => expect(getWalletInstruments).toHaveBeenCalledTimes(1)); + + await waitFor(() => + expect(getInitiativeInstruments).toHaveBeenCalledTimes(1) + ); + + await waitFor(() => expect(showFailureToast).toHaveBeenCalledTimes(1)); + + expect(actor.getSnapshot().value).toMatchObject({ + ConfiguringIban: "DisplayingIbanList" + }); + }); +}); From 28d46e1cb18c1a2e10fed2fcc24da9f7279fe766 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Tue, 10 Sep 2024 14:49:08 +0200 Subject: [PATCH 25/31] fix: import --- .../itwallet/machine/credential/__tests__/machine.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/features/itwallet/machine/credential/__tests__/machine.test.ts b/ts/features/itwallet/machine/credential/__tests__/machine.test.ts index e2be2cfc845..8a086586be1 100644 --- a/ts/features/itwallet/machine/credential/__tests__/machine.test.ts +++ b/ts/features/itwallet/machine/credential/__tests__/machine.test.ts @@ -2,7 +2,7 @@ import { AuthorizationDetail } from "@pagopa/io-react-native-wallet"; import { waitFor } from "@testing-library/react-native"; import _ from "lodash"; -import { createActor, fromPromise, StateFrom } from "xstate5"; +import { createActor, fromPromise, StateFrom } from "xstate"; import { WalletAttestationResult } from "../../../common/utils/itwAttestationUtils"; import { ItwStoredCredentialsMocks } from "../../../common/utils/itwMocksUtils"; import { From df1d9b4812d6f3a71a114e9defe534b1cb3a0a19 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Tue, 10 Sep 2024 14:55:02 +0200 Subject: [PATCH 26/31] chore: add BDP token selector --- .../idpay/configuration/machine/provider.tsx | 29 +++++++++---------- .../idpay/onboarding/machine/provider.tsx | 16 +++++----- .../idpay/payment/machine/provider.tsx | 11 ++++--- ts/store/reducers/authentication.ts | 5 ++++ 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/ts/features/idpay/configuration/machine/provider.tsx b/ts/features/idpay/configuration/machine/provider.tsx index 192fdf99483..80d69e8bd9b 100644 --- a/ts/features/idpay/configuration/machine/provider.tsx +++ b/ts/features/idpay/configuration/machine/provider.tsx @@ -15,7 +15,10 @@ import { } from "../../../../config"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; +import { + bpdTokenSelector, + walletTokenSelector +} from "../../../../store/reducers/authentication"; import { isPagoPATestEnabledSelector, preferredLanguageSelector @@ -38,8 +41,10 @@ export const IdPayConfigurationMachineContext = createActorContext( export const IDPayConfigurationMachineProvider = ({ children }: Props) => { const dispatch = useIODispatch(); + const navigation = useIONavigation(); - const sessionInfo = useIOSelector(sessionInfoSelector); + const walletToken = useIOSelector(walletTokenSelector); + const bpdToken = useIOSelector(bpdTokenSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); const language = pipe( @@ -48,23 +53,15 @@ export const IDPayConfigurationMachineProvider = ({ children }: Props) => { O.getOrElse(() => PreferredLanguageEnum.it_IT) ); - const navigation = useIONavigation(); - - if ( - O.isNone(sessionInfo) || - (O.isSome(sessionInfo) && - (sessionInfo.value.walletToken === undefined || - sessionInfo.value.bpdToken === undefined)) - ) { - throw new Error("Session info is undefined"); + if (!bpdToken) { + throw new Error("BDP token is undefined"); } - // Here we are sure that walletToken is defined - const walletToken = sessionInfo.value.walletToken as string; - // Here we are sure that bpdToken is defined - const bpdToken = sessionInfo.value.bpdToken as string; + if (!walletToken) { + throw new Error("Wallet token is undefined"); + } - const idPayToken = idPayTestToken !== undefined ? idPayTestToken : bpdToken; + const idPayToken = idPayTestToken ?? bpdToken; const paymentManagerClient = PaymentManagerClient( isPagoPATestEnabled ? pagoPaApiUrlPrefixTest : pagoPaApiUrlPrefix, diff --git a/ts/features/idpay/onboarding/machine/provider.tsx b/ts/features/idpay/onboarding/machine/provider.tsx index 13ffc62716e..1e3d1e2b9bf 100644 --- a/ts/features/idpay/onboarding/machine/provider.tsx +++ b/ts/features/idpay/onboarding/machine/provider.tsx @@ -10,7 +10,10 @@ import { } from "../../../../config"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; +import { + bpdTokenSelector, + sessionInfoSelector +} from "../../../../store/reducers/authentication"; import { isPagoPATestEnabledSelector, preferredLanguageSelector @@ -33,6 +36,7 @@ export const IdPayOnboardingMachineProvider = ({ children }: Props) => { const dispatch = useIODispatch(); const navigation = useIONavigation(); + const bpdToken = useIOSelector(bpdTokenSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); const preferredLanguageOption = useIOSelector(preferredLanguageSelector); @@ -42,15 +46,11 @@ export const IdPayOnboardingMachineProvider = ({ children }: Props) => { O.getOrElse(() => PreferredLanguageEnum.it_IT) ); - const sessionInfo = useIOSelector(sessionInfoSelector); - - if (O.isNone(sessionInfo)) { - throw new Error("Session info is undefined"); + if (!bpdToken) { + throw new Error("BDP token is undefined"); } - const { bpdToken } = sessionInfo.value; - - const token = idPayTestToken !== undefined ? idPayTestToken : bpdToken; + const token = idPayTestToken ?? bpdToken; const client = createIDPayClient( isPagoPATestEnabled ? idPayApiUatBaseUrl : idPayApiBaseUrl ); diff --git a/ts/features/idpay/payment/machine/provider.tsx b/ts/features/idpay/payment/machine/provider.tsx index 8447fe219d9..1c9e0b7426a 100644 --- a/ts/features/idpay/payment/machine/provider.tsx +++ b/ts/features/idpay/payment/machine/provider.tsx @@ -1,5 +1,4 @@ import { createActorContext } from "@xstate/react"; -import * as O from "fp-ts/lib/Option"; import React from "react"; import { idPayApiBaseUrl, @@ -8,7 +7,7 @@ import { } from "../../../../config"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { sessionInfoSelector } from "../../../../store/reducers/authentication"; +import { bpdTokenSelector } from "../../../../store/reducers/authentication"; import { isPagoPATestEnabledSelector } from "../../../../store/reducers/persistedPreferences"; import { createIDPayClient } from "../../common/api/client"; import { createActionsImplementation } from "./actions"; @@ -25,14 +24,14 @@ export const IdPayPaymentMachineContext = export const IdPayPaymentMachineProvider = (props: Props) => { const dispatch = useIODispatch(); const navigation = useIONavigation(); - const sessionInfo = useIOSelector(sessionInfoSelector); + + const bpdToken = useIOSelector(bpdTokenSelector); const isPagoPATestEnabled = useIOSelector(isPagoPATestEnabledSelector); - if (O.isNone(sessionInfo)) { - throw new Error("Session info is undefined"); + if (!bpdToken) { + throw new Error("BDP token is undefined"); } - const { bpdToken } = sessionInfo.value; const token = idPayTestToken ?? bpdToken; const idPayClient = createIDPayClient( diff --git a/ts/store/reducers/authentication.ts b/ts/store/reducers/authentication.ts index 7e9e1ea40cb..3299827b639 100644 --- a/ts/store/reducers/authentication.ts +++ b/ts/store/reducers/authentication.ts @@ -191,6 +191,11 @@ export const walletTokenSelector = (state: GlobalState): string | undefined => ? state.authentication.sessionInfo.walletToken : undefined; +export const bpdTokenSelector = (state: GlobalState): string | undefined => + isLoggedInWithSessionInfo(state.authentication) + ? state.authentication.sessionInfo.bpdToken + : undefined; + export const loggedInIdpSelector = (state: GlobalState) => isLoggedIn(state.authentication) ? state.authentication.idp : undefined; From c7084ad831f3daa6ffaf0e85de94339b9fa4fa2f Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Tue, 10 Sep 2024 14:57:40 +0200 Subject: [PATCH 27/31] fix: imports --- ts/features/idpay/onboarding/machine/provider.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ts/features/idpay/onboarding/machine/provider.tsx b/ts/features/idpay/onboarding/machine/provider.tsx index 1e3d1e2b9bf..eb9123e7a6e 100644 --- a/ts/features/idpay/onboarding/machine/provider.tsx +++ b/ts/features/idpay/onboarding/machine/provider.tsx @@ -10,10 +10,7 @@ import { } from "../../../../config"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { - bpdTokenSelector, - sessionInfoSelector -} from "../../../../store/reducers/authentication"; +import { bpdTokenSelector } from "../../../../store/reducers/authentication"; import { isPagoPATestEnabledSelector, preferredLanguageSelector From 21512689366267bfa0395208ea4b59118dc2aab9 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 16 Sep 2024 12:48:06 +0200 Subject: [PATCH 28/31] fix: fixed machine transition --- ts/features/idpay/onboarding/machine/machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/features/idpay/onboarding/machine/machine.ts b/ts/features/idpay/onboarding/machine/machine.ts index 69f65c60be2..acac925b5d4 100644 --- a/ts/features/idpay/onboarding/machine/machine.ts +++ b/ts/features/idpay/onboarding/machine/machine.ts @@ -119,7 +119,7 @@ export const idPayOnboardingMachine = setup({ onError: [ { guard: "isSessionExpired", - target: "SessionExpired" + target: "#idpay-onboarding.SessionExpired" }, { actions: assign(({ event }) => ({ From 7780262ce2a6134b8be1e0ab5dda5f026a7befc2 Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 16 Sep 2024 18:06:54 +0200 Subject: [PATCH 29/31] fix: UI fixes --- .../screens/InitiativeDetailsScreen.tsx | 33 ++++++------ .../screens/MultiValuePrerequisitesScreen.tsx | 50 +++++++++---------- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx index 00f664ea7ca..9755eb21792 100644 --- a/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx +++ b/ts/features/idpay/onboarding/screens/InitiativeDetailsScreen.tsx @@ -84,7 +84,7 @@ export const InitiativeDetailsScreen = () => { return ( @@ -97,23 +97,22 @@ export const InitiativeDetailsScreen = () => { {onboardingPrivacyAdvice} - - - + ); }; diff --git a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx index 082fae82e57..67bbee7065b 100644 --- a/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx +++ b/ts/features/idpay/onboarding/screens/MultiValuePrerequisitesScreen.tsx @@ -10,7 +10,7 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import { default as React } from "react"; -import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native"; +import { ScrollView, StyleSheet, View } from "react-native"; import PagerView from "react-native-pager-view"; import { SelfDeclarationMultiDTO } from "../../../../../definitions/idpay/SelfDeclarationMultiDTO"; import { H4 } from "../../../../components/core/typography/H4"; @@ -31,32 +31,33 @@ type ListItemProps = { const MultiValuePrerequisitesScreen = () => { const pagerRef = React.useRef(null); - const { useSelector } = IdPayOnboardingMachineContext; - const multiSelfDeclarations = useSelector(multiRequiredCriteriaSelector); - const currentPage = useSelector(selectCurrentMultiSelfDeclarationPage); + const multiSelfDeclarations = IdPayOnboardingMachineContext.useSelector( + multiRequiredCriteriaSelector + ); + const currentPage = IdPayOnboardingMachineContext.useSelector( + selectCurrentMultiSelfDeclarationPage + ); React.useEffect(() => { pagerRef.current?.setPage(currentPage); }, [pagerRef, currentPage]); return ( - - - {multiSelfDeclarations.map((selfDelcaration, index) => ( - - - - ))} - - + + {multiSelfDeclarations.map((selfDelcaration, index) => ( + + + + ))} + ); }; @@ -67,8 +68,7 @@ type MultiValuePrerequisiteItemScreenContentProps = { const MultiValuePrerequisiteItemScreenContent = ({ selfDeclaration }: MultiValuePrerequisiteItemScreenContentProps) => { - const { useActorRef } = IdPayOnboardingMachineContext; - const machine = useActorRef(); + const machine = IdPayOnboardingMachineContext.useActorRef(); const [selectedIndex, setSelectedIndex] = React.useState( undefined @@ -101,7 +101,7 @@ const MultiValuePrerequisiteItemScreenContent = ({ {I18n.t("idpay.onboarding.multiPrerequisites.link")}

{selfDeclaration.description}

- + {selfDeclaration.value.map((answer, index) => ( Date: Tue, 17 Sep 2024 15:37:42 +0200 Subject: [PATCH 30/31] fix: rename test file --- ...odeOnboardingScreen.tsx => IdPayCodeOnboardingScreen.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ts/features/idpay/code/screens/__tests__/{IdPayCodeOnboardingScreen.tsx => IdPayCodeOnboardingScreen.test.tsx} (100%) diff --git a/ts/features/idpay/code/screens/__tests__/IdPayCodeOnboardingScreen.tsx b/ts/features/idpay/code/screens/__tests__/IdPayCodeOnboardingScreen.test.tsx similarity index 100% rename from ts/features/idpay/code/screens/__tests__/IdPayCodeOnboardingScreen.tsx rename to ts/features/idpay/code/screens/__tests__/IdPayCodeOnboardingScreen.test.tsx From 5871e661212d8c55617e1657aea55abf58270bbe Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Thu, 26 Sep 2024 10:13:40 +0200 Subject: [PATCH 31/31] fix: post merge --- .../idpay/details/screens/IdPayInitiativeDetailsScreen.tsx | 1 + ts/features/itwallet/machine/credential/actions.ts | 3 +-- ts/features/itwallet/machine/credential/events.ts | 2 +- yarn.lock | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx index e123a9d3fec..ac362d6ec2c 100644 --- a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx @@ -51,6 +51,7 @@ import { initiativeNeedsConfigurationSelector } from "../store"; import { idpayInitiativeGet, idpayTimelinePageGet } from "../store/actions"; +import { ConfigurationMode } from "../../configuration/types"; export type IdPayInitiativeDetailsScreenParams = { initiativeId: string; diff --git a/ts/features/itwallet/machine/credential/actions.ts b/ts/features/itwallet/machine/credential/actions.ts index 3b98458738e..932065d868e 100644 --- a/ts/features/itwallet/machine/credential/actions.ts +++ b/ts/features/itwallet/machine/credential/actions.ts @@ -1,8 +1,7 @@ import { IOToast } from "@pagopa/io-app-design-system"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; -import { ActionArgs } from "xstate"; -import { ActionArgs, assertEvent } from "xstate5"; +import { ActionArgs, assertEvent } from "xstate"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import ROUTES from "../../../../navigation/routes"; diff --git a/ts/features/itwallet/machine/credential/events.ts b/ts/features/itwallet/machine/credential/events.ts index 3375875eda5..8ad3e12e3ec 100644 --- a/ts/features/itwallet/machine/credential/events.ts +++ b/ts/features/itwallet/machine/credential/events.ts @@ -1,4 +1,4 @@ -import { ErrorActorEvent } from "xstate5"; +import { ErrorActorEvent } from "xstate"; import { type useIONavigation } from "../../../../navigation/params/AppParamsList"; export type Reset = { diff --git a/yarn.lock b/yarn.lock index 66e32dce58b..48b37b18c11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16487,11 +16487,6 @@ xstate@^5: resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.17.4.tgz#334ab2da123973634097f7ca48387ae1589c774e" integrity sha512-KM2FYVOUJ04HlOO4TY3wEXqoYPR/XsDu+ewm+IWw0vilXqND0jVfvv04tEFwp8Mkk7I/oHXM8t1Ex9xJyUS4ZA== -xstate@^5.13.0: - version "5.13.0" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.13.0.tgz#7f7092d813a89d94024b083fe23a86b6cf4a323a" - integrity sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ== - xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"