11"use client" ;
2- import { formatUnits } from "viem" ;
2+ import { formatUnits , parseUnits , isAddress } from "viem" ;
33import { useBalances } from "../balances/use-balances" ;
4- import { useState } from "react" ;
4+ import { forwardRef , useMemo , useState } from "react" ;
55import { AssetId } from "../assets/assets" ;
66import { useTransfer } from "./use-transfer" ;
77import { useSupportedChains } from "../chain/use-supported-chains" ;
88import { 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
1036export 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