Skip to content

Commit

Permalink
Merge pull request #23 from lidofinance/develop
Browse files Browse the repository at this point in the history
Develop to main
  • Loading branch information
itaven authored Aug 21, 2023
2 parents 2e4fa20 + 72ec8d0 commit f4dc8a2
Show file tree
Hide file tree
Showing 52 changed files with 946 additions and 851 deletions.
120 changes: 120 additions & 0 deletions features/withdrawals/claim/claim-form-context/claim-form-context.tsx
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>
);
};
2 changes: 2 additions & 0 deletions features/withdrawals/claim/claim-form-context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './claim-form-context';
export * from './types';
17 changes: 17 additions & 0 deletions features/withdrawals/claim/claim-form-context/types.ts
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[];
};
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 features/withdrawals/claim/claim-form-context/use-helper-state.ts
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;
};
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 features/withdrawals/claim/claim-form-context/validation.ts
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');
}
};
20 changes: 20 additions & 0 deletions features/withdrawals/claim/claim.tsx
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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getScreenSize } from 'utils/getScreenSize';
import {
REQUESTS_LIST_MIN_HEIGHT,
REQUESTS_LIST_ITEM_SIZE,
} from '../requests-list/styles';
} from './requests-list/styles';

// Adding 2/3 of item size to make next item slightly visible
// so user can understand that there is scrollable list
Expand Down
Loading

0 comments on commit f4dc8a2

Please sign in to comment.