Skip to content

Commit

Permalink
adrienne/feat: wallets flow provider (deriv-com#10699)
Browse files Browse the repository at this point in the history
* backup

* chore: backup working poc flows

* feat: added flow provider to wallets

* chore: reverted different changes

* chore: reverted different changes

* chore: reverted different changes

* chore: reverted icons

* chore: fix eslint issues for docs

* chore: fix eslint issues for docs

* Update AccountFlow.tsx

* Update AccountFlow.tsx

* chore: fixed eslint issues

* feat: added screen ordering

* chore: fix eslint issues

* chore: fix eslint issues

* chore: fix eslint issues

* chore: added more docs and updated context

* chore: fix eslint
  • Loading branch information
adrienne-deriv authored Oct 17, 2023
1 parent c949695 commit 2ac6589
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ display: 'grid', fontSize: 40, height: '100%', placeItems: 'center', width: '100%' }}>
Password Screen in Account Flow
</div>
);
};

const ScreenB = () => {
const { formValues, setFormValues } = useFlow();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', height: '30vh', padding: '4px' }}>
<h1>Screen B</h1>
<input
onChange={e => setFormValues('testb', e.target.value)}
style={{ border: '1px solid black', padding: '4px', width: '100%' }}
type='text'
value={formValues.testb}
/>
<input
onChange={e => setFormValues('testa', e.target.value)}
style={{ border: '1px solid black', padding: '4px', width: '100%' }}
type='text'
value={formValues.testa}
/>
</div>
);
};

const ScreenA = () => {
const { formValues, setFormValues } = useFlow();

return (
<div style={{ height: '30vh', padding: '4px' }}>
<input
onChange={e => setFormValues('testa', e.target.value)}
style={{ border: '1px solid black', padding: '4px', width: '100%' }}
type='text'
value={formValues.testa}
/>
</div>
);
};

const JurisdictionScreen = () => {
return (
<div>
<h1>Jurisdiction Screen</h1>
</div>
);
};

const screens = {
aScreen: <ScreenA />,
bScreen: <ScreenB />,
JurisdictionScreen: <JurisdictionScreen />,
passwordScreen: <PasswordScreen />,
selectAccountTypeScreen: <MT5AccountType onMarketTypeSelect={marketType => marketType} selectedMarketType='all' />,
};

const AccountFlow = () => {
const { show } = useModal();
const nextFlowHandler = ({
currentScreenId,
switchScreen,
switchNextScreen,
}: TFlowProviderContext<typeof screens>) => {
switch (currentScreenId) {
case 'bScreen':
show(<VerificationFlow />);
break;
case 'passwordScreen':
switchScreen('bScreen');
break;
default:
switchNextScreen();
}
};

return (
<FlowProvider
initialValues={{
testa: '',
testb: '',
}}
screens={screens}
screensOrder={['selectAccountTypeScreen', 'JurisdictionScreen', 'passwordScreen', 'aScreen', 'bScreen']}
>
{context => {
return (
<ModalStepWrapper
renderFooter={() => (
<button
onClick={() => {
nextFlowHandler(context);
}}
>
{context.currentScreenId !== 'JurisdictionScreen' ? 'Next' : 'Submit'}
</button>
)}
title='Account Flow'
>
{context.WalletScreen}
</ModalStepWrapper>
);
}}
</FlowProvider>
);
};

export default AccountFlow;
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ display: 'grid', fontSize: 40, height: '100%', placeItems: 'center', width: '100%' }}>
Password Screen in Verification Flow
</div>
);
};

const ScreenB = () => {
const { formValues, setFormValues } = useFlow();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', height: '30vh', padding: '4px' }}>
<h1>Screen X in Verification Flow</h1>
<input
onChange={e => setFormValues('testb', e.target.value)}
style={{ border: '1px solid black', padding: '4px', width: '100%' }}
type='text'
value={formValues.testb}
/>
<input
onChange={e => setFormValues('testa', e.target.value)}
style={{ border: '1px solid black', padding: '4px', width: '100%' }}
type='text'
value={formValues.testa}
/>
</div>
);
};

const ScreenA = () => {
const { formValues, setFormValues } = useFlow();

return (
<div style={{ height: '30vh', padding: '4px' }}>
<input
onChange={e => setFormValues('testa', e.target.value)}
style={{ border: '1px solid black', padding: '4px', width: '100%' }}
type='text'
value={formValues.testa}
/>
</div>
);
};

const SuccessModal = () => {
return (
<ModalWrapper>
<div
style={{
background: 'white',
height: '50vh',
width: '50vw',
}}
>
<h1>SUCCESS MODAL!</h1>
</div>
</ModalWrapper>
);
};

const screens = {
aScreen: <ScreenA />,
bScreen: <ScreenB />,
passwordScreen: <PasswordScreen />,
};

const VerificationFlow = () => {
const { show } = useModal();
const nextFlowHandler = ({ currentScreenId, switchNextScreen }: TFlowProviderContext<typeof screens>) => {
switch (currentScreenId) {
case 'bScreen':
show(<SuccessModal />);
break;
default:
switchNextScreen();
}
};

return (
<FlowProvider
initialValues={{
testa: '',
testb: '',
}}
screens={screens}
screensOrder={['passwordScreen', 'aScreen', 'bScreen']}
>
{context => {
return (
<ModalStepWrapper
renderFooter={() => (
<button
onClick={() => {
nextFlowHandler(context);
}}
>
{context.currentScreenId !== 'bScreen' ? 'Next' : 'Submit'}
</button>
)}
title='Verification Flow'
>
{context.WalletScreen}
</ModalStepWrapper>
);
}}
</FlowProvider>
);
};

export default VerificationFlow;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AccountFlow } from './AccountFlow';
126 changes: 126 additions & 0 deletions packages/wallets/src/components/FlowProvider/FlowProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<T> = {
WalletScreen?: ReactNode;
currentScreenId: keyof T;
formValues: FormikValues;
setFormValues: (
field: string,
value: unknown,
shouldValidate?: boolean | undefined
) => Promise<FormikErrors<unknown> | void>;
switchNextScreen: () => void;
switchScreen: (screenId: keyof T) => void;
};

type FlowChildren = ReactElement | ReactFragment | ReactPortal;

export type TWalletScreens = {
[id: string]: ReactNode;
};

export type TFlowProviderProps<T> = {
children: (context: TFlowProviderContext<T>) => FlowChildren;
initialValues: FormikValues;
screens: T;
screensOrder: (keyof T)[];
};

const FlowProviderContext = createContext<TFlowProviderContext<TWalletScreens> | 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<T extends TWalletScreens>({
children,
initialValues,
screens,
screensOrder,
}: TFlowProviderProps<T>) {
const [currentScreenId, setCurrentScreenId] = useState<keyof T>(screensOrder[0]);
const switchScreen = (screenId: keyof T) => {
setCurrentScreenId(screenId);
};

const FlowProvider = FlowProviderContext.Provider as React.Provider<TFlowProviderContext<T> | 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
<Formik initialValues={initialValues} onSubmit={() => undefined}>
{({ setFieldValue, values }) => {
return (
<FlowProvider
value={{
...context,
formValues: values,
setFormValues: setFieldValue,
}}
>
{children({
...context,
formValues: values,
setFormValues: setFieldValue,
})}
</FlowProvider>
);
}}
</Formik>
);
}

export default FlowProvider;
4 changes: 4 additions & 0 deletions packages/wallets/src/components/FlowProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import FlowProvider, { TFlowProviderContext, useFlow } from './FlowProvider';

export { FlowProvider, useFlow };
export type { TFlowProviderContext };

0 comments on commit 2ac6589

Please sign in to comment.