diff --git a/package.json b/package.json index 4feaf0b..73f7392 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "react-dom": "^17.0.2", "react-scripts": "5.0.0", "react-select": "3.1.0", - "rxjs": "^7.5.4", + "rxjs": "^7.5.5", "theme-ui": "^0.13.1", "typescript": "^4.5.4", "web-vitals": "^2.1.2" diff --git a/src/flow/Flow.tsx b/src/flow/Flow.tsx index a468529..330264e 100644 --- a/src/flow/Flow.tsx +++ b/src/flow/Flow.tsx @@ -1,27 +1,33 @@ import { useState, FC } from 'react' +import { Observable } from 'rxjs' +import { useObservable } from '../stateMachineExperiment/helpers/useObservable' export type GenericStepProps = { next?: () => void previous?: () => void updateState: (s: Partial) => void skip?: () => void + state$: Observable } & S type Direction = 'forwards' | 'backwards' export function Flow( - props: { steps: Array>> } & { + props: { steps: Array> } & { updateState: (state: Partial) => void next?: () => void previous?: () => void name: string + state$: Observable } & S, ) { const parentPrevious = props.previous const parentNext = props.next - let nextPreviousCalledThisRender = false const [currentStepIndex, setCurrentStepIndex] = useState(0) + const currentStep = props.steps[currentStepIndex] + const canSkip = useObservable(currentStep.canSkip$(props.state$)) + const [direction, setDirection] = useState('forwards') const isNestedFlow = parentPrevious || parentNext @@ -30,7 +36,6 @@ export function Flow( const next = canProgress ? () => { - nextPreviousCalledThisRender = true setDirection('forwards') setCurrentStepIndex((currentStep) => currentStep + 1) } @@ -40,7 +45,6 @@ export function Flow( const previous = canRegress ? () => { - nextPreviousCalledThisRender = true setDirection('backwards') setCurrentStepIndex((currentStep) => currentStep - 1) } @@ -50,14 +54,7 @@ export function Flow( const skip = direction === 'forwards' ? next : previous - const currentStep = props.steps[currentStepIndex] - - if ( - nextPreviousCalledThisRender && - isSkippable(currentStep) && - skip && - currentStep.canSkip(props) - ) { + if (skip && canSkip) { console.log(`skipping step with index ${currentStepIndex} in flow ${props.name}`) skip() } @@ -65,14 +62,7 @@ export function Flow( return } -function isSkippable(step: IStep | ISkippableStep): step is ISkippableStep { - return (step as ISkippableStep).canSkip !== undefined -} - export interface IStep { Component: FC> -} - -export interface ISkippableStep extends IStep { - canSkip: (s: StepSpecificProps) => boolean + canSkip$: (state$: Observable) => Observable } diff --git a/src/flow/OpenBorrowVault.tsx b/src/flow/OpenBorrowVault.tsx index f000f98..fd86e77 100644 --- a/src/flow/OpenBorrowVault.tsx +++ b/src/flow/OpenBorrowVault.tsx @@ -5,6 +5,7 @@ import { CreateProxy, CreateProxyProps } from './create-proxy/CreateProxy' import { Confirmation, ConfirmationProps } from './steps/Confirmation' import { Complete, CompleteProps } from './steps/Complete' import { Allowance } from './allowance/Allowance' +import { Subject } from 'rxjs' type OpenBorrowVaultType = SimulateStepProps & CreateProxyProps & ConfirmationProps & CompleteProps @@ -20,11 +21,17 @@ export function OpenBorrowVault() { ethPrice: 2000, walletAddress: '0xWalletAddress', }) + const state$ = new Subject() useEffect(() => { const i = setInterval(() => { setViewState((oldState) => { - return calculateViewModal({ ...oldState, ethPrice: Math.floor(Math.random() * 10000) }) + const nextState = calculateViewModal({ + ...oldState, + ethPrice: Math.floor(Math.random() * 10000), + }) + state$.next(nextState) + return nextState }) }, 1000) return () => clearInterval(i) @@ -38,6 +45,7 @@ export function OpenBorrowVault() { updateState={(newState) => setViewState((oldState) => calculateViewModal({ ...oldState, ...newState })) } + state$={state$.asObservable()} /> ) } diff --git a/src/flow/allowance/Allowance.tsx b/src/flow/allowance/Allowance.tsx index d11d54c..595ba97 100644 --- a/src/flow/allowance/Allowance.tsx +++ b/src/flow/allowance/Allowance.tsx @@ -1,16 +1,18 @@ -import { Flow, GenericStepProps, ISkippableStep } from '../Flow' +import { Flow, GenericStepProps, IStep } from '../Flow' import { ConfigureAllowanceAmount, ConfigureAllowanceAmountProps, } from './steps/ConfigureAllowanceAmount' import { Done, DoneProps } from './steps/Done' import { useEffect, useState } from 'react' +import { Observable, of, Subject, switchMap } from 'rxjs' export type AllowanceProps = ConfigureAllowanceAmountProps & DoneProps -export const Allowance: ISkippableStep = { +export const Allowance: IStep = { Component: (props: GenericStepProps) => { const [viewState, setViewState] = useState(props) + const state$ = new Subject() useEffect(() => { if (props.configuredAllowance) { @@ -24,11 +26,18 @@ export const Allowance: ISkippableStep = { name="allowance" steps={[ConfigureAllowanceAmount, Done]} updateState={(newState) => { - setViewState((oldState) => ({ ...oldState, ...newState })) + setViewState((oldState) => { + const nextState = { ...oldState, ...newState } + state$.next(nextState) + return nextState + }) props.updateState(newState) }} + state$={state$.asObservable()} /> ) }, - canSkip: (props) => !!props.configuredAllowance, + canSkip$: (state$: Observable): Observable => { + return state$.pipe(switchMap((state) => of(!!state.configuredAllowance))) + }, } diff --git a/src/flow/allowance/steps/ConfigureAllowanceAmount.tsx b/src/flow/allowance/steps/ConfigureAllowanceAmount.tsx index 34b6a4b..bfeca99 100644 --- a/src/flow/allowance/steps/ConfigureAllowanceAmount.tsx +++ b/src/flow/allowance/steps/ConfigureAllowanceAmount.tsx @@ -1,6 +1,7 @@ import { GenericStepProps, IStep } from '../../Flow' import { SyntheticEvent, useState } from 'react' import { useLoadingDots } from '../../hooks/useLoadingDots' +import { Observable, of } from 'rxjs' export type ConfigureAllowanceAmountProps = { depositAmount?: number @@ -92,4 +93,7 @@ export const ConfigureAllowanceAmount: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/allowance/steps/Done.tsx b/src/flow/allowance/steps/Done.tsx index d5e9cd7..4d3cbc4 100644 --- a/src/flow/allowance/steps/Done.tsx +++ b/src/flow/allowance/steps/Done.tsx @@ -1,3 +1,4 @@ +import { Observable, of } from 'rxjs' import { GenericStepProps, IStep } from '../../Flow' export type DoneProps = {} @@ -12,4 +13,7 @@ export const Done: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/create-proxy/CreateProxy.pipe.ts b/src/flow/create-proxy/CreateProxy.pipe.ts new file mode 100644 index 0000000..91c666e --- /dev/null +++ b/src/flow/create-proxy/CreateProxy.pipe.ts @@ -0,0 +1,60 @@ +import { + Subject, + from, + combineLatest, + map, + switchMap, + share, + startWith, + Observable, + merge, + of, +} from 'rxjs' + +type ConnectedContext = { + account: string +} + +type ProxyAddress = (account: string) => Observable + +function createDsProxy(account: string) { + return '0xProxyAdress' +} + +// TODO: Think about how AppContext could be tidied up +function createProxyPipe$( + context$: Observable, + proxyAddress$: Observable, +): Observable<{ createProxy: any; proxy: any }> { + const createProxyClick$ = new Subject() + + const newProxy$ = combineLatest([createProxyClick$, context$]).pipe( + switchMap(([_, context]) => from(createDsProxy(context.account))), + share(), + startWith(undefined), + ) + + const existingProxy$ = combineLatest([context$, proxyAddress$]).pipe( + switchMap(([context, proxyAddress]) => proxyAddress(context.account)), + share(), + ) + + const proxy$ = merge(newProxy$, existingProxy$) + + function createProxy() { + createProxyClick$.next() + } + + // Return viewModel + return combineLatest([of(createProxy), proxy$]).pipe( + map(([createProxy, proxy]) => ({ + createProxy, + proxy, + })), + ) +} + +export const proxy$ = createProxyPipe$( + of({ account: '0xUserAdress' }), + of((account: string) => of('OxProxyAddress')), +) diff --git a/src/flow/create-proxy/CreateProxy.tsx b/src/flow/create-proxy/CreateProxy.tsx index f7266a8..d71f178 100644 --- a/src/flow/create-proxy/CreateProxy.tsx +++ b/src/flow/create-proxy/CreateProxy.tsx @@ -1,14 +1,28 @@ -import { Flow, GenericStepProps, ISkippableStep } from '../Flow' -import { FC, useState } from 'react' +import { Flow, IStep } from '../Flow' +import { useEffect, useState } from 'react' import { Explanation, ExplanationProps } from './steps/Explanation' import { Creation, CreationProps } from './steps/Creation' import { Done, DoneProps } from './steps/Done' +import { useObservable } from '../../stateMachineExperiment/helpers/useObservable' +import { proxy$ } from './CreateProxy.pipe' +import { Observable, Subject, switchMap, of } from 'rxjs' export type CreateProxyProps = ExplanationProps & CreationProps & DoneProps -export const CreateProxy: ISkippableStep = { +export const CreateProxy: IStep = { Component: (props) => { const [viewState, setViewState] = useState(props) + const proxyViewModel = useObservable(proxy$) + const state$ = new Subject() + + useEffect(() => { + setViewState((oldState) => { + return { + ...oldState, + proxyAddress: proxyViewModel?.proxy, + } + }) + }, [proxyViewModel]) return ( @@ -16,14 +30,18 @@ export const CreateProxy: ISkippableStep = { name="proxy" steps={[Explanation, Creation, Done]} updateState={(newState) => { - setViewState((oldState) => ({ ...oldState, ...newState })) + setViewState((oldState) => { + const nextState = { ...oldState, ...newState } + state$.next(nextState) + return nextState + }) props.updateState(newState) }} + state$={state$.asObservable()} /> ) }, - canSkip: (props: CreateProxyProps) => { - console.log('here') - return !!props.proxyAddress + canSkip$: (state$: Observable): Observable => { + return state$.pipe(switchMap((state) => of(!!state.proxyAddress))) }, } diff --git a/src/flow/create-proxy/steps/Creation.tsx b/src/flow/create-proxy/steps/Creation.tsx index 5b125b5..c2f73d1 100644 --- a/src/flow/create-proxy/steps/Creation.tsx +++ b/src/flow/create-proxy/steps/Creation.tsx @@ -1,7 +1,7 @@ import { GenericStepProps, IStep } from '../../Flow' import { useEffect } from 'react' import { useLoadingDots } from '../../hooks/useLoadingDots' -import { clear } from '@testing-library/user-event/dist/clear' +import { Observable, of } from 'rxjs' export type CreationProps = { walletAddress: string @@ -30,4 +30,7 @@ export const Creation: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/create-proxy/steps/Done.tsx b/src/flow/create-proxy/steps/Done.tsx index be4f9ab..a92a8ab 100644 --- a/src/flow/create-proxy/steps/Done.tsx +++ b/src/flow/create-proxy/steps/Done.tsx @@ -1,6 +1,8 @@ +import { Observable, of } from 'rxjs' import { GenericStepProps, IStep } from '../../Flow' export type DoneProps = { + walletAddress: string proxyAddress?: string } @@ -15,4 +17,7 @@ export const Done: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/create-proxy/steps/Explanation.tsx b/src/flow/create-proxy/steps/Explanation.tsx index 6285691..d66ab9e 100644 --- a/src/flow/create-proxy/steps/Explanation.tsx +++ b/src/flow/create-proxy/steps/Explanation.tsx @@ -1,3 +1,4 @@ +import { Observable, of } from 'rxjs' import { GenericStepProps, IStep } from '../../Flow' export type ExplanationProps = { @@ -24,4 +25,7 @@ export const Explanation: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/steps/Complete.tsx b/src/flow/steps/Complete.tsx index 95beaf0..70d47fa 100644 --- a/src/flow/steps/Complete.tsx +++ b/src/flow/steps/Complete.tsx @@ -1,3 +1,4 @@ +import { Observable, of } from 'rxjs' import { GenericStepProps, IStep } from '../Flow' export type CompleteProps = { @@ -17,4 +18,7 @@ export const Complete: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/steps/Confirmation.tsx b/src/flow/steps/Confirmation.tsx index f37144e..387e54d 100644 --- a/src/flow/steps/Confirmation.tsx +++ b/src/flow/steps/Confirmation.tsx @@ -1,6 +1,7 @@ import { GenericStepProps, IStep } from '../Flow' import { useState } from 'react' import { useLoadingDots } from '../hooks/useLoadingDots' +import { Observable, of } from 'rxjs' export type ConfirmationProps = { depositAmount?: number @@ -43,4 +44,7 @@ export const Confirmation: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/src/flow/steps/SimulateStep.tsx b/src/flow/steps/SimulateStep.tsx index 86a9d0c..7d275a8 100644 --- a/src/flow/steps/SimulateStep.tsx +++ b/src/flow/steps/SimulateStep.tsx @@ -1,3 +1,4 @@ +import { Observable, of } from 'rxjs' import { GenericStepProps, IStep } from '../Flow' export type SimulateStepProps = { @@ -40,4 +41,7 @@ export const SimulateStep: IStep = { ) }, + canSkip$: (): Observable => { + return of(false) + }, } diff --git a/yarn.lock b/yarn.lock index a7e56b0..98bed6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8566,10 +8566,10 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" -rxjs@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d" - integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ== +rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== dependencies: tslib "^2.1.0"