Skip to content

Commit

Permalink
update componentry for withdraws
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredvu committed Jan 8, 2025
1 parent dd8640f commit 43eacc3
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 51 deletions.
67 changes: 67 additions & 0 deletions src/views/dialogs/WithdrawDialog2/AddressInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { EventHandler } from 'react';

import { SyntheticInputEvent } from 'react-number-format/types/types';
import styled from 'styled-components';

import { STRING_KEYS } from '@/constants/localization';
import { WalletNetworkType } from '@/constants/wallets';

import { useAccounts } from '@/hooks/useAccounts';
import { useStringGetter } from '@/hooks/useStringGetter';

import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';

type AddressInputProps = {
value: string;
onChange: (newValue: string) => void;
_destinationChain: string;
onDestinationClicked: () => void;
};

export const AddressInput = ({
value,
onChange,
_destinationChain,
onDestinationClicked,
}: AddressInputProps) => {
const stringGetter = useStringGetter();
const { sourceAccount } = useAccounts();

const onValueChange: EventHandler<SyntheticInputEvent> = (e) => {
onChange(e.target.value);
};

return (
<div tw="flex items-center justify-between gap-0.5 rounded-0.75 border border-solid border-color-border bg-color-layer-4 px-1.25 py-0.75">
<div tw="flex flex-1 flex-col gap-0.5 text-small">
<div>{stringGetter({ key: STRING_KEYS.ADDRESS })}</div>
<input
placeholder={sourceAccount.address}
tw="flex-1 bg-color-layer-4 text-large font-medium outline-none"
value={value}
onChange={onValueChange}
/>
</div>
<button
tw="flex items-center gap-0.75 rounded-0.75 border border-solid border-color-layer-6 bg-color-layer-5 px-0.5 py-0.375"
type="button"
disabled={sourceAccount.chain === WalletNetworkType.Solana}
onClick={onDestinationClicked}
>
<div tw="flex items-center gap-0.5">
<AssetIcon tw="h-[2rem] w-[2rem]" symbol="ETH" />
<div>Ethereum</div>
</div>
{sourceAccount.chain !== WalletNetworkType.Solana && (
<$CaretIcon size="10px" iconName={IconName.Caret} />
)}
</button>
</div>
);
};

const $CaretIcon = styled(Icon)`
transform: rotate(-90deg);
color: var(--color-text-0);
`;
87 changes: 87 additions & 0 deletions src/views/dialogs/WithdrawDialog2/AmountInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { EventHandler } from 'react';

import { BonsaiCore } from '@/abacus-ts/ontology';
import { SyntheticInputEvent } from 'react-number-format/types/types';

import { STRING_KEYS } from '@/constants/localization';
import { USDC_DECIMALS } from '@/constants/tokens';

import { useStringGetter } from '@/hooks/useStringGetter';

import { Output, OutputType } from '@/components/Output';

import { useAppSelector } from '@/state/appTypes';

import { orEmptyObj } from '@/lib/typeUtils';

type AmountInputProps = {
value: string;
onChange: (newValue: string) => void;
};

export const AmountInput = ({ value, onChange }: AmountInputProps) => {
const stringGetter = useStringGetter();

const onValueChange: EventHandler<SyntheticInputEvent> = (e) => {
onChange(e.target.value);
};

const isLoading =
useAppSelector(BonsaiCore.account.parentSubaccountSummary.loading) === 'pending';

const { freeCollateral } = orEmptyObj(
useAppSelector(BonsaiCore.account.parentSubaccountSummary.data)
);

const onClickMax = () => {
if (!freeCollateral) return;
onChange(freeCollateral.toString());
};

const onMaxDisabled = !freeCollateral || isLoading;

return (
<div tw="flex items-center justify-between gap-0.5 rounded-0.75 border border-solid border-color-border bg-color-layer-4 px-1.25 py-0.75">
<div tw="flex flex-1 flex-col gap-0.5 text-small">
<div>
{stringGetter({ key: STRING_KEYS.AMOUNT })}

{freeCollateral && (
<>
<span></span>
<Output
tw="inline font-medium text-color-text-0"
fractionDigits={USDC_DECIMALS}
slotRight={` ${stringGetter({ key: STRING_KEYS.AVAILABLE })}`}
value={freeCollateral}
type={OutputType.Fiat}
/>
</>
)}

{freeCollateral && (
<>
<span></span>
<button
disabled={onMaxDisabled}
onClick={onClickMax}
type="button"
tw="font-medium"
style={{ color: onMaxDisabled ? 'var(--color-text-0)' : 'var(--color-accent)' }}
>
{stringGetter({ key: STRING_KEYS.MAX })}
</button>
</>
)}
</div>
<input
type="number"
placeholder="0.00"
tw="flex-1 bg-color-layer-4 text-large font-medium outline-none"
value={value}
onChange={onValueChange}
/>
</div>
</div>
);
};
53 changes: 53 additions & 0 deletions src/views/dialogs/WithdrawDialog2/ChainSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Dispatch, Fragment, SetStateAction } from 'react';

import { mainnet } from 'viem/chains';

import { CHAIN_INFO } from '@/constants/chains';

import { AssetIcon } from '@/components/AssetIcon';

export const ChainSelect = ({
onBack,
selectedChain,
setSelectedChain,
}: {
onBack: () => void;
selectedChain: string;
setSelectedChain: Dispatch<SetStateAction<string>>;
}) => {
const onChainClick = (newChainId: string) => () => {
setSelectedChain(newChainId);
onBack();
};

return (
<div tw="flex flex-col gap-0.5 py-1">
<div tw="flex flex-col">
{[mainnet.id].map((chain) => {
const chainId = chain.toString();

return (
<Fragment key={chainId}>
<button
onClick={onChainClick(chainId)}
type="button"
style={{
backgroundColor: chainId === selectedChain ? 'var(--color-layer-4)' : undefined,
}}
tw="flex w-full justify-between px-1.25 py-1 hover:bg-color-layer-4"
key={chainId}
>
<div tw="flex items-center gap-0.75">
<AssetIcon tw="h-[2rem] w-[2rem]" symbol="ETH" />
<div tw="flex flex-col items-start gap-0.125">
<div tw="text-medium font-medium">{CHAIN_INFO[chainId]?.name}</div>
</div>
</div>
</button>
</Fragment>
);
})}
</div>
</div>
);
};
72 changes: 21 additions & 51 deletions src/views/dialogs/WithdrawDialog2/WithdrawDialog2.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,37 @@
import { useRef, useState } from 'react';

import styled from 'styled-components';
import { mainnet } from 'viem/chains';

import { DepositDialog2Props, DialogProps } from '@/constants/dialogs';
import { CosmosChainId } from '@/constants/graz';
import { STRING_KEYS } from '@/constants/localization';
import { SOLANA_MAINNET_ID } from '@/constants/solana';
import { USDC_ADDRESSES, USDC_DECIMALS } from '@/constants/tokens';
import { WalletNetworkType } from '@/constants/wallets';

import { useAccounts } from '@/hooks/useAccounts';
import { useBreakpoints } from '@/hooks/useBreakpoints';
import { useStringGetter } from '@/hooks/useStringGetter';

import { Dialog, DialogPlacement } from '@/components/Dialog';

import { SourceAccount } from '@/state/wallet';

import { DepositForm } from '../DepositDialog2/DepositForm';
import { TokenSelect } from '../DepositDialog2/TokenSelect';
import { DepositToken } from '../DepositDialog2/types';

function getDefaultToken(sourceAccount: SourceAccount): DepositToken {
if (!sourceAccount.chain) throw new Error('No user chain detected');

// TODO(deposit2.0): Use user's biggest balance as the default token
if (sourceAccount.chain === WalletNetworkType.Evm) {
return {
chainId: mainnet.id.toString(),
denom: USDC_ADDRESSES[mainnet.id],
decimals: USDC_DECIMALS,
};
}

if (sourceAccount.chain === WalletNetworkType.Solana) {
return {
chainId: SOLANA_MAINNET_ID,
denom: USDC_ADDRESSES[SOLANA_MAINNET_ID],
decimals: USDC_DECIMALS,
};
}

return {
chainId: CosmosChainId.Osmosis,
denom: USDC_ADDRESSES[CosmosChainId.Osmosis],
decimals: USDC_DECIMALS,
};
}
import { ChainSelect } from './ChainSelect';
import { WithdrawForm } from './WithdrawForm';

export const WithdrawDialog2 = ({ setIsOpen }: DialogProps<DepositDialog2Props>) => {
const { sourceAccount } = useAccounts();

const [destinationAddress, setDestinationAddress] = useState('');
const [destinationChain, setDestinationChain] = useState('');
const [amount, setAmount] = useState('');
const [token, setToken] = useState<DepositToken>(getDefaultToken(sourceAccount));

const { isMobile } = useBreakpoints();
const stringGetter = useStringGetter();

const [formState, setFormState] = useState<'form' | 'token-select'>('form');
const tokenSelectRef = useRef<HTMLDivElement | null>(null);
const [formState, setFormState] = useState<'form' | 'chain-select'>('form');
const chainSelectRef = useRef<HTMLDivElement | null>(null);

// TODO(deposit2): localization
const dialogTitle =
formState === 'form' ? stringGetter({ key: STRING_KEYS.DEPOSIT }) : 'Select Token';
formState === 'form'
? stringGetter({ key: STRING_KEYS.WITHDRAW })
: stringGetter({ key: STRING_KEYS.SELECT_CHAIN });

const onShowForm = () => {
setFormState('form');
tokenSelectRef.current?.scroll({ top: 0 });
chainSelectRef.current?.scroll({ top: 0 });
};

return (
Expand All @@ -86,22 +50,28 @@ export const WithdrawDialog2 = ({ setIsOpen }: DialogProps<DepositDialog2Props>)
tw="w-[50%]"
style={{ marginLeft: formState === 'form' ? 0 : '-50%', transition: 'margin 500ms' }}
>
<DepositForm
<WithdrawForm
amount={amount}
setAmount={setAmount}
token={token}
onTokenSelect={() => setFormState('token-select')}
destinationAddress={destinationAddress}
setDestinationAddress={setDestinationAddress}
destinationChain={destinationChain}
onChainSelect={() => setFormState('chain-select')}
/>
</div>
<div
ref={tokenSelectRef}
ref={chainSelectRef}
tw="w-[50%] overflow-scroll"
style={{
height: formState === 'form' ? 0 : '100%',
maxHeight: isMobile ? '50vh' : '25rem',
}}
>
<TokenSelect token={token} setToken={setToken} onBack={onShowForm} />
<ChainSelect
selectedChain={destinationChain}
setSelectedChain={setDestinationChain}
onBack={onShowForm}
/>
</div>
</div>
</$Dialog>
Expand Down
50 changes: 50 additions & 0 deletions src/views/dialogs/WithdrawDialog2/WithdrawForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Dispatch, SetStateAction } from 'react';

import { ButtonAction, ButtonState, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';

import { useStringGetter } from '@/hooks/useStringGetter';

import { Button } from '@/components/Button';

import { AddressInput } from './AddressInput';
import { AmountInput } from './AmountInput';

export const WithdrawForm = ({
amount,
setAmount,
destinationAddress,
setDestinationAddress,
destinationChain,
onChainSelect,
}: {
amount: string;
setAmount: Dispatch<SetStateAction<string>>;
destinationAddress: string;
setDestinationAddress: Dispatch<SetStateAction<string>>;
destinationChain: string;
onChainSelect: () => void;
}) => {
const stringGetter = useStringGetter();

return (
<div tw="flex min-h-10 flex-col gap-1 p-1.25">
<AddressInput
value={destinationAddress}
onChange={setDestinationAddress}
_destinationChain={destinationChain}
onDestinationClicked={onChainSelect}
/>
<AmountInput value={amount} onChange={setAmount} />
<Button
tw="w-full"
state={ButtonState.Disabled}
disabled
action={ButtonAction.Primary}
type={ButtonType.Submit}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
</div>
);
};

0 comments on commit 43eacc3

Please sign in to comment.