-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from lidofinance/develop
Develop to main
- Loading branch information
Showing
52 changed files
with
946 additions
and
851 deletions.
There are no files selected for viewing
120 changes: 120 additions & 0 deletions
120
features/withdrawals/claim/claim-form-context/claim-form-context.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,120 @@ | ||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'; | ||
import { FormProvider, useForm } from 'react-hook-form'; | ||
|
||
import invariant from 'tiny-invariant'; | ||
import { ClaimFormInputType, ClaimFormValidationContext } from './types'; | ||
import { claimFormValidationResolver } from './validation'; | ||
import { useClaim } from 'features/withdrawals/hooks'; | ||
import { useMaxSelectedCount } from './use-max-selected-count'; | ||
import { | ||
generateDefaultValues, | ||
useGetDefaultValues, | ||
} from './use-default-values'; | ||
import { ClaimFormHelperState, useHelperState } from './use-helper-state'; | ||
import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; | ||
import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; | ||
|
||
type ClaimFormDataContextValueType = { | ||
onSubmit: NonNullable<React.ComponentProps<'form'>['onSubmit']>; | ||
} & ClaimFormHelperState; | ||
|
||
const claimFormDataContext = | ||
createContext<ClaimFormDataContextValueType | null>(null); | ||
claimFormDataContext.displayName = 'claimFormDataContext'; | ||
|
||
export const useClaimFormData = () => { | ||
const contextData = useContext(claimFormDataContext); | ||
invariant(contextData); | ||
return contextData; | ||
}; | ||
|
||
export const ClaimFormProvider: React.FC = ({ children }) => { | ||
const { dispatchModalState } = useTransactionModal(); | ||
const { data } = useClaimData(); | ||
|
||
const [shouldReset, setShouldReset] = useState<boolean>(false); | ||
const { maxSelectedRequestCount, defaultSelectedRequestCount } = | ||
useMaxSelectedCount(); | ||
const { getDefaultValues } = useGetDefaultValues(defaultSelectedRequestCount); | ||
|
||
const formObject = useForm<ClaimFormInputType, ClaimFormValidationContext>({ | ||
defaultValues: getDefaultValues, | ||
resolver: claimFormValidationResolver, | ||
context: { maxSelectedRequestCount }, | ||
mode: 'onChange', | ||
reValidateMode: 'onChange', | ||
}); | ||
const { watch, reset, handleSubmit, setValue, getValues, formState } = | ||
formObject; | ||
const helperState = useHelperState(watch, maxSelectedRequestCount); | ||
|
||
const claim = useClaim(); | ||
const onSubmit = useMemo( | ||
() => | ||
handleSubmit(async ({ selectedTokens }) => { | ||
const success = await claim(selectedTokens); | ||
if (success) setShouldReset(true); | ||
}), | ||
[handleSubmit, claim], | ||
); | ||
|
||
useEffect(() => { | ||
dispatchModalState({ type: 'set_on_retry', callback: onSubmit }); | ||
}, [dispatchModalState, onSubmit]); | ||
|
||
const { isSubmitting } = formState; | ||
|
||
// handles reset and data update | ||
useEffect(() => { | ||
// no updates while submitting | ||
if (!data || isSubmitting) return; | ||
|
||
// reset state after submit | ||
if (shouldReset) { | ||
reset(generateDefaultValues(data, defaultSelectedRequestCount)); | ||
setShouldReset(false); | ||
return; | ||
} | ||
|
||
// for regular updates generate new list but keep user input | ||
const oldValues = getValues('requests'); | ||
const checkedIds = new Set( | ||
oldValues.filter((req) => req.checked).map((req) => req.token_id), | ||
); | ||
const newRequests = generateDefaultValues( | ||
data, | ||
defaultSelectedRequestCount, | ||
).requests.map((request) => ({ | ||
...request, | ||
checked: checkedIds.has(request.token_id), | ||
})); | ||
|
||
setValue('requests', newRequests, { | ||
shouldValidate: true, | ||
shouldDirty: false, | ||
}); | ||
}, [ | ||
data, | ||
getValues, | ||
setValue, | ||
shouldReset, | ||
reset, | ||
isSubmitting, | ||
defaultSelectedRequestCount, | ||
]); | ||
|
||
const claimFormDataContextValue = useMemo(() => { | ||
return { | ||
onSubmit, | ||
...helperState, | ||
}; | ||
}, [helperState, onSubmit]); | ||
|
||
return ( | ||
<FormProvider {...formObject}> | ||
<claimFormDataContext.Provider value={claimFormDataContextValue}> | ||
{useMemo(() => children, [children])} | ||
</claimFormDataContext.Provider> | ||
</FormProvider> | ||
); | ||
}; |
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,2 @@ | ||
export * from './claim-form-context'; | ||
export * from './types'; |
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,17 @@ | ||
import { | ||
RequestStatusClaimable, | ||
RequestStatusesUnion, | ||
} from 'features/withdrawals/types/request-status'; | ||
|
||
export type ClaimFormValidationContext = { | ||
maxSelectedRequestCount: number; | ||
}; | ||
|
||
export type ClaimFormInputType = { | ||
requests: { | ||
checked: boolean; | ||
token_id: string; | ||
status: RequestStatusesUnion; | ||
}[]; | ||
selectedTokens: RequestStatusClaimable[]; | ||
}; |
50 changes: 50 additions & 0 deletions
50
features/withdrawals/claim/claim-form-context/use-default-values.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,50 @@ | ||
import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; | ||
import { useCallback, useEffect, useMemo } from 'react'; | ||
import { useAwaiter } from 'shared/hooks/use-awaiter'; | ||
import { ClaimFormInputType } from './types'; | ||
import { WithdrawalRequests } from 'features/withdrawals/hooks'; | ||
|
||
export const generateDefaultValues = ( | ||
data: WithdrawalRequests, | ||
defaultSelectedRequestCount: number, | ||
): ClaimFormInputType => { | ||
const requests = [ | ||
...data.sortedClaimableRequests.map((request, index) => ({ | ||
token_id: request.stringId, | ||
checked: index < defaultSelectedRequestCount, | ||
status: request, | ||
})), | ||
...data.pendingRequests.map((request) => ({ | ||
token_id: request.stringId, | ||
checked: false, | ||
status: request, | ||
})), | ||
]; | ||
|
||
return { requests, selectedTokens: [] }; | ||
}; | ||
|
||
/// provides values & defaultValues props to useForm | ||
/// values keep requests list updated if added/removed and smartly merged with existing state | ||
/// defaultValues provides async function, used by form to wait for data to load | ||
export const useGetDefaultValues = (defaultSelectedRequestCount: number) => { | ||
const { data, error } = useClaimData(); | ||
const values: ClaimFormInputType | undefined = useMemo(() => { | ||
if (!data) return undefined; | ||
return generateDefaultValues(data, defaultSelectedRequestCount); | ||
}, [defaultSelectedRequestCount, data]); | ||
|
||
const { awaiter, resolver } = useAwaiter(values); | ||
useEffect(() => { | ||
if (error && !resolver.isResolved) { | ||
resolver.resolve({ requests: [], selectedTokens: [] }); | ||
} | ||
}, [resolver, error]); | ||
|
||
const getDefaultValues = useCallback( | ||
() => (values ? Promise.resolve(values) : awaiter), | ||
[awaiter, values], | ||
); | ||
|
||
return { getDefaultValues }; | ||
}; |
54 changes: 54 additions & 0 deletions
54
features/withdrawals/claim/claim-form-context/use-helper-state.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,54 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { type UseFormWatch } from 'react-hook-form'; | ||
import { Zero } from '@ethersproject/constants'; | ||
import { type BigNumber } from 'ethers'; | ||
|
||
import { RequestStatusClaimable } from 'features/withdrawals/types/request-status'; | ||
|
||
import { ClaimFormInputType } from './types'; | ||
|
||
export type ClaimFormHelperState = { | ||
selectedRequests: RequestStatusClaimable[]; | ||
ethToClaim: BigNumber; | ||
canSelectMore: boolean; | ||
requestsCount: number; | ||
}; | ||
|
||
const initState = (): ClaimFormHelperState => ({ | ||
selectedRequests: [], | ||
canSelectMore: true, | ||
ethToClaim: Zero, | ||
requestsCount: 0, | ||
}); | ||
|
||
/// subscribes to form updates and provides derived helper state for UI | ||
export const useHelperState = ( | ||
watch: UseFormWatch<ClaimFormInputType>, | ||
maxSelectedRequestCount: number, | ||
) => { | ||
const [helperState, setHelperState] = | ||
useState<ClaimFormHelperState>(initState); | ||
|
||
useEffect(() => { | ||
const subscription = watch(({ requests }) => { | ||
const selectedRequests = | ||
requests | ||
?.filter((r) => r?.checked) | ||
.map((r) => r?.status as RequestStatusClaimable) ?? []; | ||
|
||
const ethToClaim = selectedRequests.reduce((ethSum, request) => { | ||
return ethSum.add(request?.claimableEth ?? Zero); | ||
}, Zero); | ||
|
||
setHelperState({ | ||
selectedRequests, | ||
canSelectMore: selectedRequests.length < maxSelectedRequestCount, | ||
requestsCount: requests?.length ?? 0, | ||
ethToClaim, | ||
}); | ||
}); | ||
return () => subscription.unsubscribe(); | ||
}, [watch, maxSelectedRequestCount]); | ||
|
||
return helperState; | ||
}; |
22 changes: 22 additions & 0 deletions
22
features/withdrawals/claim/claim-form-context/use-max-selected-count.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,22 @@ | ||
import { useIsLedgerLive } from 'shared/hooks/useIsLedgerLive'; | ||
|
||
import { | ||
DEFAULT_CLAIM_REQUEST_SELECTED, | ||
MAX_REQUESTS_COUNT, | ||
MAX_REQUESTS_COUNT_LEDGER_LIMIT, | ||
} from 'features/withdrawals/withdrawals-constants'; | ||
|
||
export const useMaxSelectedCount = () => { | ||
const isLedgerLive = useIsLedgerLive(); | ||
const maxSelectedRequestCount = isLedgerLive | ||
? MAX_REQUESTS_COUNT_LEDGER_LIMIT | ||
: MAX_REQUESTS_COUNT; | ||
const defaultSelectedRequestCount = Math.min( | ||
DEFAULT_CLAIM_REQUEST_SELECTED, | ||
maxSelectedRequestCount, | ||
); | ||
return { | ||
maxSelectedRequestCount, | ||
defaultSelectedRequestCount, | ||
}; | ||
}; |
40 changes: 40 additions & 0 deletions
40
features/withdrawals/claim/claim-form-context/validation.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,40 @@ | ||
import { Resolver } from 'react-hook-form'; | ||
import { | ||
ValidationError, | ||
handleResolverValidationError, | ||
} from 'shared/hook-form/validation-error'; | ||
import invariant from 'tiny-invariant'; | ||
import { ClaimFormValidationContext, ClaimFormInputType } from './types'; | ||
import { RequestStatusClaimable } from 'features/withdrawals/types/request-status'; | ||
|
||
export const claimFormValidationResolver: Resolver< | ||
ClaimFormInputType, | ||
ClaimFormValidationContext | ||
> = async ({ requests }, context) => { | ||
invariant(context); | ||
try { | ||
const { maxSelectedRequestCount } = context; | ||
const selectedTokens = requests | ||
.filter((r) => r.checked) | ||
.map((r) => r.status as RequestStatusClaimable); | ||
|
||
if (selectedTokens.length === 0) | ||
throw new ValidationError('requests', 'No requests selected for claim'); | ||
|
||
if (selectedTokens.length > maxSelectedRequestCount) | ||
throw new ValidationError( | ||
'requests', | ||
`Cannot claim more than ${maxSelectedRequestCount} requests at once`, | ||
); | ||
|
||
return { | ||
values: { | ||
requests, | ||
selectedTokens, | ||
}, | ||
errors: {}, | ||
}; | ||
} catch (error) { | ||
return handleResolverValidationError(error, 'ClaimForm', 'selectedTokens'); | ||
} | ||
}; |
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,20 @@ | ||
import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; | ||
import { ClaimFaq } from 'features/withdrawals/withdrawals-faq/claim-faq'; | ||
|
||
import { ClaimForm } from './form'; | ||
import { TxClaimModal } from './tx-modal'; | ||
import { ClaimWallet } from './wallet'; | ||
import { ClaimFormProvider } from './claim-form-context'; | ||
|
||
export const Claim = () => { | ||
return ( | ||
<TransactionModalProvider> | ||
<ClaimFormProvider> | ||
<ClaimWallet /> | ||
<ClaimForm /> | ||
<ClaimFaq /> | ||
<TxClaimModal /> | ||
</ClaimFormProvider> | ||
</TransactionModalProvider> | ||
); | ||
}; |
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
Oops, something went wrong.