Skip to content

Commit

Permalink
feat: deposit modal form invalid states (#1415)
Browse files Browse the repository at this point in the history
  • Loading branch information
tinaszheng authored Jan 16, 2025
1 parent 0eeaa2d commit 5e17193
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 84 deletions.
17 changes: 13 additions & 4 deletions src/views/dialogs/DepositDialog2/AmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';

import { useBalance } from './queries';
import { getTokenSymbol, isNativeTokenDenom } from './utils';

export type AmountInputProps = {
value: string;
onChange: (newValue: string) => void;
token: TokenForTransfer;
onTokenClick: () => void;
tokenBalance: { raw?: string; formatted?: string };
error?: Error | null;
};

const numericValueRegex = /^\d*(?:\\[.])?\d*$/;
Expand All @@ -33,7 +34,14 @@ function escapeRegExp(string: string): string {

const GAS_RESERVE_AMOUNT = parseUnits('0.01', ETH_DECIMALS);

export const AmountInput = ({ value, onChange, token, onTokenClick }: AmountInputProps) => {
export const AmountInput = ({
value,
onChange,
token,
onTokenClick,
tokenBalance,
error,
}: AmountInputProps) => {
const stringGetter = useStringGetter();
const { sourceAccount } = useAccounts();

Expand All @@ -45,8 +53,6 @@ export const AmountInput = ({ value, onChange, token, onTokenClick }: AmountInpu
onChange(e.target.value);
};

const tokenBalance = useBalance(token.chainId, token.denom);

const onClickMax = () => {
if (!tokenBalance.raw) return;

Expand Down Expand Up @@ -98,9 +104,12 @@ export const AmountInput = ({ value, onChange, token, onTokenClick }: AmountInpu
)}
</div>
<input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
type="number"
placeholder="0.00"
tw="flex-1 bg-color-layer-4 text-large font-medium outline-none"
style={{ color: error ? 'var(--color-error)' : undefined }}
value={value}
onChange={onValueChange}
/>
Expand Down
90 changes: 59 additions & 31 deletions src/views/dialogs/DepositDialog2/DepositForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';

import { formatUnits, parseUnits } from 'viem';

Expand All @@ -11,7 +11,7 @@ import { SkipRouteSpeed } from '@/hooks/transfers/skipClient';
import { useDebounce } from '@/hooks/useDebounce';
import { useStringGetter } from '@/hooks/useStringGetter';

import { CoinbaseBrandIcon } from '@/icons';
import { CoinbaseBrandIcon, WarningIcon } from '@/icons';

import { Button } from '@/components/Button';
import { Output, OutputType } from '@/components/Output';
Expand All @@ -21,7 +21,8 @@ import { openDialog } from '@/state/dialogs';

import { AmountInput } from './AmountInput';
import { RouteOptions } from './RouteOptions';
import { useDepositRoutes } from './queries';
import { useBalance, useDepositRoutes } from './queries';
import { getTokenSymbol } from './utils';

export const DepositForm = ({
onTokenSelect,
Expand All @@ -38,35 +39,64 @@ export const DepositForm = ({
}) => {
const dispatch = useAppDispatch();
const stringGetter = useStringGetter();
const [selectedSpeed, setSelectedSpeed] = useState<SkipRouteSpeed>('fast');
const tokenBalance = useBalance(token.chainId, token.denom);

const [selectedSpeed, setSelectedSpeed] = useState<SkipRouteSpeed>('fast');
const debouncedAmount = useDebounce(amount);
const { data: routes, isFetching } = useDepositRoutes(token, debouncedAmount);
const {
data: routes,
isFetching,
isPlaceholderData,
error,
} = useDepositRoutes(token, debouncedAmount);

useEffect(() => {
if (debouncedAmount && !isFetching && !routes?.fast) setSelectedSpeed('slow');
if (debouncedAmount && !isFetching && routes && !routes.fast) setSelectedSpeed('slow');
}, [isFetching, routes, debouncedAmount]);

const selectedRoute = selectedSpeed === 'fast' ? routes?.fast : routes?.slow;
const depositRoute = !isPlaceholderData ? selectedRoute : undefined;

const hasSufficientBalance = depositRoute
? tokenBalance.raw && BigInt(depositRoute.amountIn) <= BigInt(tokenBalance.raw)
: true;

const depositDisabled = isFetching || !hasSufficientBalance || !depositRoute;

const depositButtonInner = useMemo(() => {
if (!hasSufficientBalance) return `Insufficient ${getTokenSymbol(token.denom)}`;
if (error)
return (
<div tw="flex items-center gap-0.5">
<div tw="flex items-center text-color-error">
<WarningIcon />
</div>
{/* TODO(deposit2.0): localization */}
<div>Min deposit is $10</div>
</div>
);

return stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS });
}, [error, hasSufficientBalance, stringGetter, token.denom]);

return (
<div tw="flex min-h-10 flex-col gap-2 p-1.25">
<div tw="flex flex-col gap-0.5">
<AmountInput
tokenBalance={tokenBalance}
value={amount}
onChange={setAmount}
token={token}
onTokenClick={onTokenSelect}
error={error}
/>
<RouteOptions
routes={routes}
isLoading={isFetching}
disabled={!amount || parseUnits(amount, token.decimals) === BigInt(0)}
selectedSpeed={selectedSpeed}
onSelectSpeed={setSelectedSpeed}
/>
{routes && (
<RouteOptions
routes={routes}
isLoading={isFetching}
disabled={!amount || parseUnits(amount, token.decimals) === BigInt(0)}
selectedSpeed={selectedSpeed}
onSelectSpeed={setSelectedSpeed}
/>
)}
<div tw="flex flex-col gap-0.5">
<div tw="flex items-center gap-1">
<hr tw="flex-1 border-[0.5px] border-solid border-color-border" />
Expand Down Expand Up @@ -95,28 +125,26 @@ export const DepositForm = ({
<div tw="flex flex-col gap-0.5">
<Button
tw="w-full"
state={{ isDisabled: true, isLoading: isFetching }}
disabled
state={{ isDisabled: depositDisabled, isLoading: isFetching }}
disabled={depositDisabled}
action={ButtonAction.Primary}
type={ButtonType.Submit}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
{depositButtonInner}
</Button>
{/* TODO(deposit2.0): Show difference between current and new balance here */}
{selectedRoute && (
<div tw="flex justify-between text-small">
{/* TODO(deposit2.0): localization */}
<div tw="text-color-text-0">Available balance</div>
<div>
+
<Output
tw="inline"
type={OutputType.Fiat}
value={formatUnits(BigInt(selectedRoute.amountOut), USDC_DECIMALS)}
/>
</div>
<div tw="flex justify-between text-small">
{/* TODO(deposit2.0): localization */}
<div tw="text-color-text-0">Available balance</div>
<div style={{ color: isFetching ? 'var(--color-text-0)' : undefined }}>
+
<Output
tw="inline"
type={OutputType.Fiat}
value={formatUnits(BigInt(depositRoute?.amountOut ?? 0), USDC_DECIMALS)}
/>
</div>
)}
</div>
</div>
</div>
);
Expand Down
101 changes: 52 additions & 49 deletions src/views/dialogs/DepositDialog2/RouteOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import { ReactNode, useMemo } from 'react';

import { RouteResponse } from '@skip-go/client';
import { formatUnits } from 'viem';
Expand All @@ -12,7 +12,7 @@ import { LightningIcon, ShieldIcon } from '@/icons';
import { Output, OutputType } from '@/components/Output';

type Props = {
routes: { slow?: RouteResponse; fast?: RouteResponse };
routes?: { slow?: RouteResponse; fast?: RouteResponse };
isLoading: boolean;
disabled: boolean;
selectedSpeed: SkipRouteSpeed;
Expand All @@ -26,24 +26,55 @@ export const RouteOptions = ({
onSelectSpeed,
disabled,
}: Props) => {
// TODO(deposit2.0): finalize error handling here
if (!routes.slow && !routes.fast) {
return <div>There was an error loading deposit quotes.</div>;
}
const fastRouteDescription = useMemo(() => {
const fastOperationFee = // @ts-ignore
routes?.fast?.operations.find((op) => Boolean(op.goFastTransfer))?.goFastTransfer?.fee;
const totalFastFee = fastOperationFee
? formatUnits(
BigInt(fastOperationFee.bpsFeeAmount ?? 0) +
BigInt(fastOperationFee.destinationChainFeeAmount ?? 0) +
BigInt(fastOperationFee.sourceChainFeeAmount ?? 0),
6
)
: '-';

const fastOperationFee = // @ts-ignore
routes.fast?.operations.find((op) => Boolean(op.goFastTransfer))?.goFastTransfer?.fee;
const totalFastFee = fastOperationFee
? formatUnits(
BigInt(fastOperationFee.bpsFeeAmount ?? 0) +
BigInt(fastOperationFee.destinationChainFeeAmount ?? 0) +
BigInt(fastOperationFee.sourceChainFeeAmount ?? 0),
6
)
: '-';
// TODO(deposit2.0): localization
if (!routes || disabled) return '$$ fee, $10k limit';
if (!routes.fast) return 'Unavailable';

return (
<span>
<Output
tw="inline"
type={OutputType.Fiat}
fractionDigits={USD_DECIMALS}
value={totalFastFee}
/>{' '}
fee, $10k limit
</span>
);
}, [routes, disabled]);

const slowRouteDescription = useMemo(() => {
// TODO(deposit2.0): localization
if (!routes || disabled) return '$ fee, no limit';
if (!routes.slow) return 'Unavailable';

return (
<span>
<Output
tw="inline"
type={OutputType.Fiat}
fractionDigits={USD_DECIMALS}
value={routes.slow.estimatedFees[0]?.usdAmount}
/>{' '}
fee, no limit
</span>
);
}, [routes, disabled]);

return (
<div tw="flex gap-1">
<div tw="flex gap-0.5">
<RouteOption
icon={
<span
Expand All @@ -58,25 +89,11 @@ export const RouteOptions = ({
</span>
}
selected={selectedSpeed === 'fast'}
disabled={disabled || !routes.fast || isLoading}
disabled={disabled || !routes?.fast || isLoading}
onClick={() => onSelectSpeed('fast')}
// TODO(deposit2.0): localization
title="Instant"
description={
routes.fast ? (
<span>
<Output
tw="inline"
type={OutputType.Fiat}
fractionDigits={USD_DECIMALS}
value={totalFastFee}
/>{' '}
fee, $10k limit
</span>
) : (
'Unavailable'
)
}
description={fastRouteDescription}
/>
<RouteOption
icon={
Expand All @@ -92,25 +109,11 @@ export const RouteOptions = ({
</span>
}
selected={selectedSpeed === 'slow'}
disabled={disabled || isLoading || !routes.slow}
disabled={disabled || isLoading || !routes?.slow}
onClick={() => onSelectSpeed('slow')}
// TODO(deposit2.0): localization
title="~20 mins"
description={
routes.slow ? (
<span>
<Output
tw="inline"
type={OutputType.Fiat}
fractionDigits={USD_DECIMALS}
value={routes.slow.estimatedFees[0]?.usdAmount}
/>{' '}
fee, no limit
</span>
) : (
'Unavailable'
)
}
description={slowRouteDescription}
/>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions src/views/dialogs/DepositDialog2/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ async function getSkipDepositRoutes(
destAssetChainID: DYDX_DEPOSIT_CHAIN,
amountIn: parseUnits(amount, token.decimals).toString(),
smartRelay: true,
// allow quotes even if they have large price impact, as the user would see the difference in fees anyway
allowUnsafe: true,
smartSwapOptions: { evmSwaps: true },
};

Expand All @@ -147,6 +149,7 @@ export function useDepositRoutes(token: TokenForTransfer, amount: string) {
staleTime: 1 * timeUnits.minute,
refetchOnMount: 'always',
placeholderData: (prev) => prev,
retry: false,
});
}

Expand Down

0 comments on commit 5e17193

Please sign in to comment.