Skip to content

Commit

Permalink
chore(IDPay): [IDPAY-30] Create XState machine for IDPay Onboarding f…
Browse files Browse the repository at this point in the history
…low (#4151)

* Create XState machine for IDPay Onboarding flow

* Add xstate types generation

* State machine refactoring

* Add provider

* Refactor to test the provider

* Add IDPay envs

* Remove API test token
  • Loading branch information
francescopersico authored Nov 4, 2022
1 parent f35e73d commit eea9ad4
Show file tree
Hide file tree
Showing 21 changed files with 656 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ PN_ENABLED=YES
REMINDERS_OPT_IN_ENABLED=YES
# FCI (Firma con IO) feature
FCI_ENABLED=YES

# IDPay
IDPAY_API_UAT_BASEURL='https://api-io.uat.cstar.pagopa.it'
3 changes: 3 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ PN_ENABLED=YES
REMINDERS_OPT_IN_ENABLED=NO
# FCI (Firma con IO) feature
FCI_ENABLED=NO

# IDPay
IDPAY_API_UAT_BASEURL='https://api-io.uat.cstar.pagopa.it'
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
locales/locales.ts
ts/utils/__tests__/xss.test.ts
definitions/*
**/*.typegen.ts
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ shim.js
android/app/google-services.json
# React native config generated file
GeneratedDotEnv.m

# XState Typegen
**/*.typegen.*
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"generate:idpay-onboarding": "rimraf definitions/idpay/onboarding && mkdir -p definitions/idpay/onboarding && gen-api-models --api-spec $npm_package_idpay_onboarding --out-dir ./definitions/idpay/onboarding --no-strict --response-decoders --request-types --client",
"generate": "npm-run-all generate:*",
"locales_unused": "ts-node --skip-project -O '{\"lib\":[\"es2015\"]}' scripts/unused-locales.ts",
"generate-myportal-readme": "jsdoc2md ts/utils/webviewScripts/*.js > MYPORTAL_README.md"
"generate-myportal-readme": "jsdoc2md ts/utils/webviewScripts/*.js > MYPORTAL_README.md",
"generate:xstate-types": "xstate typegen \"ts/**/*.ts?(x)\""
},
"dependencies": {
"@babel/plugin-transform-regenerator": "^7.18.6",
Expand All @@ -101,6 +102,7 @@
"@react-navigation/native": "^5.9.8",
"@react-navigation/stack": "^5.14.9",
"@redux-saga/testing-utils": "^1.1.3",
"@xstate/react": "^3.0.1",
"abort-controller": "^1.0.2",
"async-mutex": "^0.1.3",
"buffer": "^4.9.1",
Expand Down Expand Up @@ -190,7 +192,8 @@
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vision-camera-code-scanner": "^0.2.0",
"xss": "1.0.10"
"xss": "1.0.10",
"xstate": "^4.33.6"
},
"devDependencies": {
"@babel/core": "^7.15.0",
Expand Down Expand Up @@ -229,6 +232,7 @@
"@types/validator": "^9.4.2",
"@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",
Expand Down
9 changes: 9 additions & 0 deletions ts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,12 @@ export const POSTE_DATAMATRIX_SCAN_PREFERRED_PSPS:
),
O.toUndefined
);

/**
* IDPay
*/

export const IDPAY_API_TEST_TOKEN =
Config.IDPAY_API_TEST_TOKEN !== "" ? Config.IDPAY_API_TEST_TOKEN : undefined;

export const IDPAY_API_UAT_BASEURL = Config.IDPAY_API_UAT_BASEURL;
32 changes: 32 additions & 0 deletions ts/features/idpay/onboarding/navigation/navigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import InitiativeDetailsScreen, {
InitiativeDetailsScreenRouteParams
} from "../screens/InitiativeDetailScreen";
import { IDPayOnboardingMachineProvider } from "../xstate/provider";

export const IDPayOnboardingRoutes = {
IDPAY_ONBOARDING_MAIN: "IDPAY_ONBOARDING_MAIN",
IDPAY_ONBOARDING_INITIATIVE_DETAILS: "IDPAY_ONBOARDING_INITIATIVE_DETAILS"
} as const;

export type IDPayOnboardingParamsList = {
[IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS]: InitiativeDetailsScreenRouteParams;
};

const Stack = createStackNavigator<IDPayOnboardingParamsList>();

export const IDPayOnboardingNavigator = () => (
<IDPayOnboardingMachineProvider>
<Stack.Navigator
initialRouteName={
IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS
}
>
<Stack.Screen
name={IDPayOnboardingRoutes.IDPAY_ONBOARDING_INITIATIVE_DETAILS}
component={InitiativeDetailsScreen}
/>
</Stack.Navigator>
</IDPayOnboardingMachineProvider>
);
64 changes: 64 additions & 0 deletions ts/features/idpay/onboarding/screens/InitiativeDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import { RouteProp, useRoute } from "@react-navigation/native";
import { Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useActor } from "@xstate/react";
import { IDPayOnboardingParamsList } from "../navigation/navigator";
import { useOnboardingMachineService } from "../xstate/provider";

type InitiativeDetailsScreenRouteParams = {
serviceId: string;
};

const InitiativeDetailsScreen = () => {
const onboardingMachineService = useOnboardingMachineService();

const [state, send] = useActor(onboardingMachineService);

const route =
useRoute<
RouteProp<
IDPayOnboardingParamsList,
"IDPAY_ONBOARDING_INITIATIVE_DETAILS"
>
>();

const { serviceId } = route.params;

React.useEffect(() => {
send({
type: "SELECT_INITIATIVE",
serviceId
});
}, [send, serviceId]);

const content = React.useMemo(() => {
if (
state.matches("WAITING_INITIATIVE_SELECTION") ||
state.matches("LOADING_INITIATIVE")
) {
return <Text>Loading...</Text>;
}

if (
state.matches("DISPLAYING_INITIATIVE") &&
state.context.initative !== undefined
) {
return <Text>Initiative ID: {state.context.initative.initiativeId}</Text>;
}

return null;
}, [state]);

return (
<SafeAreaView>
<Text>ServiceID: {serviceId}</Text>
<Text>State: {state.value}</Text>
{content}
</SafeAreaView>
);
};

export type { InitiativeDetailsScreenRouteParams };

export default InitiativeDetailsScreen;
85 changes: 85 additions & 0 deletions ts/features/idpay/onboarding/xstate/machine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { assign, createMachine } from "xstate";
import { InitiativeDto } from "../../../../../definitions/idpay/onboarding/InitiativeDto";
import { LOADING_TAG } from "../../../../utils/xstate";

// Context types
export type Context = {
serviceId?: string;
initative?: InitiativeDto;
};

// Events types
type E_SELECT_INITIATIVE = {
type: "SELECT_INITIATIVE";
serviceId: string;
};

type Events = E_SELECT_INITIATIVE;

// Services types
type Services = {
loadInitiative: {
data: InitiativeDto;
};
};

const createIDPayOnboardingMachine = () =>
/** @xstate-layout N4IgpgJg5mDOIC5QEkAiAFAggTQPoHkA5AIX0wCVVlCBxAOgHVNkAVam3a15TNgNQCiuAMoCAMgIDCbIgGJRE6Z0LdeyQQG0ADAF1EoAA4B7WAEsALqaMA7fSACeiAEwAWLQBoQAD2cB2AMx0vgCs-v6+TgCMAJxO-gAcTvEAvsmeaFh4RKQUVLR0YmR5HFxsaoKyEDZgdKbWAG5GANY1ADZGAIYQyNYWph2W9WDaekggxmaWNnaOCP7B8UGR8aFaAGzBLtFaEZ4+CE6+i+Errkdah-4uLqlpINZGEHB2GTgEJGSU7IzMbLTKqn4QgUUhkhDsEz60zG+w8DmcblS6Qwb2yn2KBSK7ABZSBEJMUNsMMQcNm80WvmWqw2Wx2TiRIFeWQ+uW+VGE6DEOGxpR4eLGkKmRNAsM8s2CWhcdHivjW0Uia3iK3CawZTPeOS+tHxkyswu8iAAtC54mLEJFgpE6Fp-GsXKEzjsbbdkkA */
createMachine(
{
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",
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: "DISPLAYING_INITIATIVE",
actions: "loadInitiativeSuccess"
}
]
}
},
DISPLAYING_INITIATIVE: {
type: "final"
}
}
},
{
actions: {
selectInitiative: assign((_, event) => ({
serviceId: event.serviceId
})),
loadInitiativeSuccess: assign((_, event) => ({
initative: event.data
}))
}
}
);

type IDPayOnboardingMachineType = ReturnType<
typeof createIDPayOnboardingMachine
>;

export type { IDPayOnboardingMachineType };
export { createIDPayOnboardingMachine };
62 changes: 62 additions & 0 deletions ts/features/idpay/onboarding/xstate/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as O from "fp-ts/lib/Option";
import React from "react";
import { InterpreterFrom } from "xstate";
import { useInterpret } from "@xstate/react";
import { useIOSelector } from "../../../../store/hooks";
import { sessionInfoSelector } from "../../../../store/reducers/authentication";
import { createOnboardingClient } from "../api/client";
import {
IDPAY_API_TEST_TOKEN,
IDPAY_API_UAT_BASEURL
} from "../../../../config";
import { createServicesImplementation } from "./services";
import {
createIDPayOnboardingMachine,
IDPayOnboardingMachineType
} from "./machine";

type OnboardingMachineContext = InterpreterFrom<IDPayOnboardingMachineType>;

const OnboardingMachineContext = React.createContext<OnboardingMachineContext>(
{} as OnboardingMachineContext
);

type Props = {
children: React.ReactNode;
};

const IDPayOnboardingMachineProvider = (props: Props) => {
const { children } = props;

const sessionInfo = useIOSelector(sessionInfoSelector);

if (O.isNone(sessionInfo)) {
throw new Error("Session info is undefined");
}

const token =
IDPAY_API_TEST_TOKEN !== undefined
? IDPAY_API_TEST_TOKEN
: sessionInfo.value.bpdToken;

const onboardingClient = createOnboardingClient(IDPAY_API_UAT_BASEURL, token);

const machine = createIDPayOnboardingMachine();

const services = createServicesImplementation(onboardingClient);

const machineService = useInterpret(machine, {
services
});

return (
<OnboardingMachineContext.Provider value={machineService}>
{children}
</OnboardingMachineContext.Provider>
);
};

const useOnboardingMachineService = () =>
React.useContext(OnboardingMachineContext);

export { IDPayOnboardingMachineProvider, useOnboardingMachineService };
38 changes: 38 additions & 0 deletions ts/features/idpay/onboarding/xstate/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { pipe } from "fp-ts/lib/function";
import * as E from "fp-ts/lib/Either";
import { InitiativeDto } from "../../../../../definitions/idpay/onboarding/InitiativeDto";
import { OnboardingClient } from "../api/client";
import { Context } from "./machine";

const createServicesImplementation = (onboardingClient: OnboardingClient) => {
const loadInitiative = async (context: Context) => {
if (context.serviceId === undefined) {
return Promise.reject("serviceId is undefined");
}

const response = await onboardingClient.getInitiativeId({
serviceId: context.serviceId
});

const data: Promise<InitiativeDto> = pipe(
response,
E.fold(
_ => Promise.reject("error loading initiative"),
_ => {
if (_.status !== 200) {
return Promise.reject("error loading initiative");
}
return Promise.resolve(_.value);
}
)
);

return data;
};

return {
loadInitiative
};
};

export { createServicesImplementation };
9 changes: 9 additions & 0 deletions ts/navigation/AppStackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import {
FimsNavigator
} from "../features/fims/navigation/navigator";
import FIMS_ROUTES from "../features/fims/navigation/routes";
import {
IDPayOnboardingNavigator,
IDPayOnboardingRoutes
} from "../features/idpay/onboarding/navigation/navigator";
import UADONATION_ROUTES from "../features/uaDonations/navigation/routes";
import { UAWebViewScreen } from "../features/uaDonations/screens/UAWebViewScreen";
import { ZendeskStackNavigator } from "../features/zendesk/navigation/navigator";
Expand Down Expand Up @@ -141,6 +145,11 @@ export const AppStackNavigator = () => {
component={CdcStackNavigator}
/>
)}

<Stack.Screen
name={IDPayOnboardingRoutes.IDPAY_ONBOARDING_MAIN}
component={IDPayOnboardingNavigator}
/>
</Stack.Navigator>
);
};
Expand Down
Loading

0 comments on commit eea9ad4

Please sign in to comment.