diff --git a/packages/wallets/docs/examples/FlowProvider/AccountFlow/AccountFlow.tsx b/packages/wallets/docs/examples/FlowProvider/AccountFlow/AccountFlow.tsx new file mode 100644 index 000000000000..71aa476b9ee0 --- /dev/null +++ b/packages/wallets/docs/examples/FlowProvider/AccountFlow/AccountFlow.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { ModalStepWrapper } from '../../../../src/components/Base/ModalStepWrapper'; +import { FlowProvider, TFlowProviderContext, useFlow } from '../../../../src/components/FlowProvider'; +import { useModal } from '../../../../src/components/ModalProvider'; +import { MT5AccountType } from '../../../../src/features/cfd/screens/MT5AccountType'; +import VerificationFlow from './VerificationFlow'; + +const PasswordScreen = () => { + return ( +
+ Password Screen in Account Flow +
+ ); +}; + +const ScreenB = () => { + const { formValues, setFormValues } = useFlow(); + return ( +
+

Screen B

+ setFormValues('testb', e.target.value)} + style={{ border: '1px solid black', padding: '4px', width: '100%' }} + type='text' + value={formValues.testb} + /> + setFormValues('testa', e.target.value)} + style={{ border: '1px solid black', padding: '4px', width: '100%' }} + type='text' + value={formValues.testa} + /> +
+ ); +}; + +const ScreenA = () => { + const { formValues, setFormValues } = useFlow(); + + return ( +
+ setFormValues('testa', e.target.value)} + style={{ border: '1px solid black', padding: '4px', width: '100%' }} + type='text' + value={formValues.testa} + /> +
+ ); +}; + +const JurisdictionScreen = () => { + return ( +
+

Jurisdiction Screen

+
+ ); +}; + +const screens = { + aScreen: , + bScreen: , + JurisdictionScreen: , + passwordScreen: , + selectAccountTypeScreen: marketType} selectedMarketType='all' />, +}; + +const AccountFlow = () => { + const { show } = useModal(); + const nextFlowHandler = ({ + currentScreenId, + switchScreen, + switchNextScreen, + }: TFlowProviderContext) => { + switch (currentScreenId) { + case 'bScreen': + show(); + break; + case 'passwordScreen': + switchScreen('bScreen'); + break; + default: + switchNextScreen(); + } + }; + + return ( + + {context => { + return ( + ( + + )} + title='Account Flow' + > + {context.WalletScreen} + + ); + }} + + ); +}; + +export default AccountFlow; diff --git a/packages/wallets/docs/examples/FlowProvider/AccountFlow/VerificationFlow.tsx b/packages/wallets/docs/examples/FlowProvider/AccountFlow/VerificationFlow.tsx new file mode 100644 index 000000000000..76401f2f4972 --- /dev/null +++ b/packages/wallets/docs/examples/FlowProvider/AccountFlow/VerificationFlow.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { ModalWrapper } from '../../../../src/components/Base'; +import { ModalStepWrapper } from '../../../../src/components/Base/ModalStepWrapper'; +import { FlowProvider, TFlowProviderContext, useFlow } from '../../../../src/components/FlowProvider'; +import { useModal } from '../../../../src/components/ModalProvider'; + +const PasswordScreen = () => { + return ( +
+ Password Screen in Verification Flow +
+ ); +}; + +const ScreenB = () => { + const { formValues, setFormValues } = useFlow(); + return ( +
+

Screen X in Verification Flow

+ setFormValues('testb', e.target.value)} + style={{ border: '1px solid black', padding: '4px', width: '100%' }} + type='text' + value={formValues.testb} + /> + setFormValues('testa', e.target.value)} + style={{ border: '1px solid black', padding: '4px', width: '100%' }} + type='text' + value={formValues.testa} + /> +
+ ); +}; + +const ScreenA = () => { + const { formValues, setFormValues } = useFlow(); + + return ( +
+ setFormValues('testa', e.target.value)} + style={{ border: '1px solid black', padding: '4px', width: '100%' }} + type='text' + value={formValues.testa} + /> +
+ ); +}; + +const SuccessModal = () => { + return ( + +
+

SUCCESS MODAL!

+
+
+ ); +}; + +const screens = { + aScreen: , + bScreen: , + passwordScreen: , +}; + +const VerificationFlow = () => { + const { show } = useModal(); + const nextFlowHandler = ({ currentScreenId, switchNextScreen }: TFlowProviderContext) => { + switch (currentScreenId) { + case 'bScreen': + show(); + break; + default: + switchNextScreen(); + } + }; + + return ( + + {context => { + return ( + ( + + )} + title='Verification Flow' + > + {context.WalletScreen} + + ); + }} + + ); +}; + +export default VerificationFlow; diff --git a/packages/wallets/docs/examples/FlowProvider/AccountFlow/index.ts b/packages/wallets/docs/examples/FlowProvider/AccountFlow/index.ts new file mode 100644 index 000000000000..be5aa3dc896b --- /dev/null +++ b/packages/wallets/docs/examples/FlowProvider/AccountFlow/index.ts @@ -0,0 +1 @@ +export { default as AccountFlow } from './AccountFlow'; diff --git a/packages/wallets/src/components/FlowProvider/FlowProvider.tsx b/packages/wallets/src/components/FlowProvider/FlowProvider.tsx new file mode 100644 index 000000000000..f0661ad22b0c --- /dev/null +++ b/packages/wallets/src/components/FlowProvider/FlowProvider.tsx @@ -0,0 +1,126 @@ +import React, { + createContext, + ReactElement, + ReactFragment, + ReactNode, + ReactPortal, + useContext, + useMemo, + useState, +} from 'react'; +import { Formik, FormikErrors, FormikValues } from 'formik'; + +export type TFlowProviderContext = { + WalletScreen?: ReactNode; + currentScreenId: keyof T; + formValues: FormikValues; + setFormValues: ( + field: string, + value: unknown, + shouldValidate?: boolean | undefined + ) => Promise | void>; + switchNextScreen: () => void; + switchScreen: (screenId: keyof T) => void; +}; + +type FlowChildren = ReactElement | ReactFragment | ReactPortal; + +export type TWalletScreens = { + [id: string]: ReactNode; +}; + +export type TFlowProviderProps = { + children: (context: TFlowProviderContext) => FlowChildren; + initialValues: FormikValues; + screens: T; + screensOrder: (keyof T)[]; +}; + +const FlowProviderContext = createContext | null>(null); + +/** + * Hook to use the flow provider's context. + * + * @returns {TFlowProviderContext} The flow provider's context: + * - `currentScreenId`: The current screen's ID being shown + * - `switchScreen`: Function which switches the current screen to another screen by their ID + * - `switchNextScreen`: Function which switches to the next screen by default. If the current screen is the final screen, it will not do anything. + * - `formValues`: The saved form values stored in Formik. By default it will contain the initial values passed in `initialValues` prop in the provider. + * - `setFormValues`: Function which allows persistence for a form value, which can be used to persist the form values for a previous screen or for the next screen. + * - `WalletScreen`: The rendered screen which is rendered by the FlowProvider. + */ +export const useFlow = () => { + const flowProviderContext = useContext(FlowProviderContext); + + if (!flowProviderContext) throw new Error('useFlow must be used within a FlowProvider component.'); + + return flowProviderContext; +}; + +/** + * The FlowProvider is responsible for: + * - Grouping screens together into a flow + * - Managing screen routing through its context `switchScreen` and `switchNextScreen` + * - Ensuring screen order is maintained through the `screensOrder` prop + * - Persisting form values in screens through `setFormValues` and restoring them through `formValues` + */ +function FlowProvider({ + children, + initialValues, + screens, + screensOrder, +}: TFlowProviderProps) { + const [currentScreenId, setCurrentScreenId] = useState(screensOrder[0]); + const switchScreen = (screenId: keyof T) => { + setCurrentScreenId(screenId); + }; + + const FlowProvider = FlowProviderContext.Provider as React.Provider | null>; + const currentScreenIndex = useMemo(() => screensOrder.indexOf(currentScreenId), [currentScreenId, screensOrder]); + const isFinalScreen = currentScreenIndex >= screensOrder.length - 1; + + const switchNextScreen = () => { + if (!isFinalScreen) { + const nextScreenId = screensOrder[currentScreenIndex + 1]; + switchScreen(nextScreenId); + } + }; + + const currentScreen = useMemo(() => { + return screens[currentScreenId]; + }, [currentScreenId, screens]); + + if (!currentScreenId) return null; + + const context = { + currentScreenId, + switchNextScreen, + switchScreen, + WalletScreen: currentScreen, + }; + + return ( + // We let the logic of the onSubmit be handled by the flow component + undefined}> + {({ setFieldValue, values }) => { + return ( + + {children({ + ...context, + formValues: values, + setFormValues: setFieldValue, + })} + + ); + }} + + ); +} + +export default FlowProvider; diff --git a/packages/wallets/src/components/FlowProvider/index.ts b/packages/wallets/src/components/FlowProvider/index.ts new file mode 100644 index 000000000000..544dc6a0ef1a --- /dev/null +++ b/packages/wallets/src/components/FlowProvider/index.ts @@ -0,0 +1,4 @@ +import FlowProvider, { TFlowProviderContext, useFlow } from './FlowProvider'; + +export { FlowProvider, useFlow }; +export type { TFlowProviderContext };