Skip to content

Commit c14945e

Browse files
committed
[squash] transfer ui
1 parent 3415ebf commit c14945e

3 files changed

Lines changed: 209 additions & 95 deletions

File tree

turnkey/src/features/swap/swap-ui.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const SwapForm = ({
4343
balances.assets[1].aggregatedAssetId
4444
);
4545
const [btcAccount] = useBTCAccount();
46-
const [amount, setAmount] = useState<string>("0.00");
46+
const [amount, setAmount] = useState<string>("");
4747
const fromAsset =
4848
// @ts-expect-error
4949
fromAssetId === "BTC"
@@ -69,17 +69,21 @@ const SwapForm = ({
6969
(balance) => balance.aggregatedAssetId === toAssetId
7070
)!;
7171

72-
const amountAsBigInt = useMemo(
73-
() =>
74-
BigInt(
72+
const amountAsBigInt = useMemo(() => {
73+
if (!amount) return BigInt(0);
74+
75+
try {
76+
return BigInt(
7577
Math.floor(
7678
parseFloat(amount) *
7779
// @ts-expect-error
7880
10 ** (fromAssetId === "BTC" ? 8 : fromAssetBalance.decimals)
7981
)
80-
),
81-
[amount, fromAssetId, fromAssetBalance.decimals]
82-
);
82+
);
83+
} catch {
84+
return BigInt(0);
85+
}
86+
}, [amount, fromAssetId, fromAssetBalance.decimals]);
8387

8488
const swapQuoteQuery = useSwapQuote({
8589
amount: amountAsBigInt,
@@ -160,6 +164,7 @@ const SwapForm = ({
160164
}}
161165
errorMessage={swapQuoteQuery.error?.message}
162166
inputProps={{
167+
placeholder: "0.00",
163168
type: "text",
164169
id: "amount",
165170
name: "amount",
Lines changed: 194 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,52 @@
11
"use client";
2-
import { formatUnits } from "viem";
2+
import { formatUnits, parseUnits, isAddress } from "viem";
33
import { useBalances } from "../balances/use-balances";
4-
import { useState } from "react";
4+
import { forwardRef, useMemo, useState } from "react";
55
import { AssetId } from "../assets/assets";
66
import { useTransfer } from "./use-transfer";
77
import { useSupportedChains } from "../chain/use-supported-chains";
88
import { TransactionStatusUI } from "../transaction-status/transaction-status-ui";
9+
import { TokenInput } from "../input/input";
10+
import type {
11+
InputHTMLAttributes,
12+
LabelHTMLAttributes,
13+
PropsWithChildren,
14+
} from "react";
15+
import { useId } from "react";
16+
import {
17+
arbitrum,
18+
avalanche,
19+
base,
20+
linea,
21+
mainnet,
22+
optimism,
23+
polygon,
24+
} from "viem/chains";
25+
26+
const chainObjects = [
27+
base,
28+
arbitrum,
29+
optimism,
30+
polygon,
31+
linea,
32+
avalanche,
33+
mainnet,
34+
];
935

1036
export const Transfer = () => {
1137
const balancesQuery = useBalances();
1238
const chainsQuery = useSupportedChains();
1339

1440
return (
1541
<div>
16-
<h2 className="text-2xl font-medium">Transfer</h2>
42+
<h2 className="text-5xl flex flex-col">
43+
<span>Simple transfer,</span>
44+
<span className="text-gray">as it should be</span>
45+
</h2>
1746

1847
{balancesQuery.status === "pending" ||
1948
chainsQuery.status === "pending" ? (
20-
<p className="animate-pulse text-white/50">Loading data...</p>
49+
<p className="animate-pulse text-white/50">Loading your account...</p>
2150
) : null}
2251
{balancesQuery.status === "success" &&
2352
chainsQuery.status === "success" ? (
@@ -42,12 +71,22 @@ const TransferForm = ({
4271
const asset = balances.assets.find(
4372
(asset) => asset.aggregatedAssetId === assetId
4473
);
45-
let amountAsBigInt: bigint;
46-
try {
47-
amountAsBigInt = BigInt(amount);
48-
} catch {
49-
amountAsBigInt = BigInt(0);
50-
}
74+
const fromAssetBalance = balances.balances.balanceByAsset.find(
75+
(balance) => balance.aggregatedAssetId === assetId
76+
)!;
77+
78+
const amountAsBigInt = useMemo(() => {
79+
try {
80+
return parseUnits(amount, asset!.decimals);
81+
} catch {
82+
return BigInt(0);
83+
}
84+
}, [amount, asset]);
85+
const [address, setAddress] = useState("");
86+
const addressError =
87+
address && !isAddress(address)
88+
? "Please enter a valid Ethereum address"
89+
: undefined;
5190

5291
if (mutation.status === "success") {
5392
return (
@@ -59,101 +98,171 @@ const TransferForm = ({
5998
}
6099

61100
return (
62-
<form className="flex flex-col gap-4 mt-4 max-w-[500px]" onSubmit={submit}>
63-
<div className="flex flex-col gap-1">
64-
<label htmlFor="asset" className="text-sm text-white/70">
65-
Asset
66-
</label>
67-
68-
<select
69-
id="asset"
70-
name="asset"
71-
value={assetId}
72-
className="px-4 py-3 rounded-xl bg-surface-level-3 h-14"
73-
onChange={(event) => setAssetId(event.target.value as AssetId)}
74-
>
75-
{balances.assets.map((asset) => {
76-
return (
77-
<option
78-
value={asset.aggregatedAssetId}
79-
key={asset.aggregatedAssetId}
80-
>
81-
{asset.symbol}
82-
</option>
83-
);
84-
})}
85-
</select>
86-
</div>
87-
88-
<div className="flex flex-col gap-1">
89-
<label htmlFor="amount" className="text-sm text-white/70">
90-
Amount
91-
</label>
92-
93-
<input
94-
type="text"
95-
id="amount"
96-
name="amount"
97-
value={amount}
98-
onChange={(event) => setAmount(event.target.value)}
99-
required
100-
className="px-4 py-3 rounded-xl bg-surface-level-3 h-14"
101-
/>
102-
103-
{asset ? (
104-
<p className="text-white/60 text-sm">
105-
{formatUnits(amountAsBigInt, asset.decimals)} {asset.symbol}
106-
</p>
107-
) : null}
108-
</div>
101+
<form
102+
className="flex flex-col gap-5 mt-14 max-w-2xl mx-auto"
103+
onSubmit={(event) => submit(event, amountAsBigInt)}
104+
>
105+
<TokenInput
106+
balance={{
107+
amount: BigInt(fromAssetBalance.balance),
108+
decimals: fromAssetBalance.decimals,
109+
}}
110+
asset={asset!}
111+
setAssetId={setAssetId}
112+
assets={balances.assets}
113+
selectProps={{
114+
id: "asset",
115+
name: "asset",
116+
}}
117+
inputProps={{
118+
type: "text",
119+
id: "amount",
120+
name: "amount",
121+
required: true,
122+
value: amount,
123+
onChange: (event) =>
124+
setAmount((event.target as HTMLInputElement).value),
125+
}}
126+
/>
109127

110-
<div className="flex flex-col gap-1">
111-
<label htmlFor="address" className="text-sm text-white/70">
112-
Recipient address
113-
</label>
114-
115-
<input
116-
type="text"
117-
id="address"
118-
name="address"
119-
required
120-
className="px-4 py-3 rounded-xl bg-surface-level-3 h-14"
121-
/>
122-
</div>
128+
<InputField
129+
inputProps={{
130+
type: "text",
131+
id: "address",
132+
name: "address",
133+
required: true,
134+
value: address,
135+
onChange: (e) => setAddress(e.target.value),
136+
}}
137+
labelProps={{
138+
children: (
139+
<span>
140+
Recipient address <RequiredSup />
141+
</span>
142+
),
143+
}}
144+
error={addressError}
145+
/>
123146

124-
<div className="flex flex-col gap-1">
125-
<label htmlFor="chain" className="text-sm text-white/70">
126-
Destination chain
127-
</label>
147+
<select
148+
id="chain"
149+
name="chain"
150+
className="px-4 py-3 rounded-xl bg-surface-level-2 h-20 border border-surface-level-4"
151+
>
152+
{chains
153+
.flatMap((chain) => {
154+
const found = chainObjects.find(
155+
(chainObj) => Number(chain.chain.reference) === chainObj.id
156+
);
157+
if (!found) return [];
128158

129-
<select
130-
id="chain"
131-
name="chain"
132-
className="px-4 py-3 rounded-xl bg-surface-level-3 h-14"
133-
>
134-
{chains.map((chain) => {
159+
return [[found.name, chain]] as const;
160+
})
161+
.map(([name, chain]) => {
135162
return (
136163
<option value={chain.chain.chain} key={chain.chain.chain}>
137-
{chain.chain.reference}
164+
{name}
138165
</option>
139166
);
140167
})}
141-
</select>
142-
</div>
168+
</select>
143169

144170
<div>
145171
<button
146172
type="submit"
147-
className="bg-brand-orange rounded-full text-black py-4 px-10 font-medium"
173+
className="h-20 rounded-[100px] py-[30px] px-10 text-base bg-brand-orange text-brand-orange-foreground hover:bg-brand-orange-lighten-20 mt-20 w-full disabled:opacity-50 disabled:cursor-not-allowed justify-center flex"
174+
disabled={
175+
mutation.status === "pending" ||
176+
!amountAsBigInt ||
177+
!!addressError ||
178+
!address
179+
}
148180
>
149181
{mutation.status === "pending" ? (
150182
<span className="animate-pulse">Transferring...</span>
151183
) : (
152-
"Initiate transfer"
184+
"Transfer"
153185
)}
154186
</button>
155187
<p className="text-red-400">{mutation.error?.message}</p>
156188
</div>
157189
</form>
158190
);
159191
};
192+
193+
export const InputField = ({
194+
labelProps,
195+
inputProps,
196+
error,
197+
className,
198+
}: {
199+
inputProps: InputHTMLAttributes<HTMLInputElement>;
200+
labelProps?: LabelHTMLAttributes<HTMLLabelElement>;
201+
className?: string;
202+
error?: string;
203+
}) => {
204+
const fallbackId = useId();
205+
const rootErrorId = useId();
206+
const errorId = error ? `${rootErrorId}-error` : undefined;
207+
const id = inputProps.id ?? fallbackId;
208+
209+
return (
210+
<div className={className}>
211+
<div
212+
className={`flex flex-col h-20 bg-surface-level-2 ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 rounded-xl overflow-hidden text-base border border-surface-level-4 relative ${
213+
error ? "border-destructive border" : ""
214+
}`}
215+
>
216+
{labelProps ? (
217+
<label
218+
htmlFor={id}
219+
{...labelProps}
220+
className={`pl-6 text-sm text-left pt-[14px] text-gray ${
221+
className ?? ""
222+
}`}
223+
/>
224+
) : null}
225+
<Input
226+
id={id}
227+
aria-invalid={errorId ? true : undefined}
228+
aria-describedby={errorId}
229+
{...inputProps}
230+
className={`bg-transparent h-full pl-6 text-white ${
231+
inputProps.className ?? ""
232+
}`}
233+
/>
234+
</div>
235+
{errorId ? (
236+
<div className="min-h-6 pt-2">
237+
<p id={errorId} className="text-destructive text-xs">
238+
{error}
239+
</p>
240+
</div>
241+
) : null}
242+
</div>
243+
);
244+
};
245+
246+
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
247+
248+
const Input = forwardRef<HTMLInputElement, InputProps>(
249+
({ className, type, ...props }, ref) => {
250+
return (
251+
<input
252+
type={type}
253+
className={`flex h-10 w-full bg-transparent px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
254+
className ?? ""
255+
}`}
256+
ref={ref}
257+
{...props}
258+
/>
259+
);
260+
}
261+
);
262+
Input.displayName = "Input";
263+
264+
const RequiredSup = (props: PropsWithChildren<{ className?: string }>) => (
265+
<sup {...props} className={`text-brand-orange ${props.className ?? ""}`}>
266+
*
267+
</sup>
268+
);

0 commit comments

Comments
 (0)