forked from deriv-com/deriv-app
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adrienne/feat: wallets flow provider (deriv-com#10699)
* 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
1 parent
c949695
commit 2ac6589
Showing
5 changed files
with
365 additions
and
0 deletions.
There are no files selected for viewing
118 changes: 118 additions & 0 deletions
118
packages/wallets/docs/examples/FlowProvider/AccountFlow/AccountFlow.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
116 changes: 116 additions & 0 deletions
116
packages/wallets/docs/examples/FlowProvider/AccountFlow/VerificationFlow.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
1 change: 1 addition & 0 deletions
1
packages/wallets/docs/examples/FlowProvider/AccountFlow/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
126
packages/wallets/src/components/FlowProvider/FlowProvider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |