@@ -7,11 +7,18 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77import { Input } from "@/components/ui/input" ;
88import { Label } from "@/components/ui/label" ;
99import { useThirdwebClient } from "@/constants/thirdweb.client" ;
10- import { CreditCardIcon } from "lucide-react" ;
11- import { useMemo , useState } from "react" ;
10+ import { ChevronDownIcon , CreditCardIcon } from "lucide-react" ;
11+ import { useCallback , useMemo , useState } from "react" ;
1212import { toast } from "sonner" ;
13- import { type ThirdwebClient , defineChain , getContract } from "thirdweb" ;
13+ import {
14+ type ThirdwebClient ,
15+ defineChain ,
16+ getContract ,
17+ toUnits ,
18+ } from "thirdweb" ;
1419import { getCurrencyMetadata } from "thirdweb/extensions/erc20" ;
20+ import { resolveScheme , upload } from "thirdweb/storage" ;
21+ import { FileInput } from "../../../../components/shared/FileInput" ;
1522import { resolveEns } from "../../../../lib/ens" ;
1623
1724export function CheckoutLinkForm ( ) {
@@ -20,23 +27,121 @@ export function CheckoutLinkForm() {
2027 const [ recipientAddress , setRecipientAddress ] = useState ( "" ) ;
2128 const [ tokenAddressWithChain , setTokenAddressWithChain ] = useState ( "" ) ;
2229 const [ amount , setAmount ] = useState ( "" ) ;
30+ const [ title , setTitle ] = useState ( "" ) ;
31+ const [ image , setImage ] = useState < File | null > ( null ) ;
32+ const [ imageUri , setImageUri ] = useState < string > ( "" ) ;
33+ const [ uploadingImage , setUploadingImage ] = useState ( false ) ;
2334 const [ isLoading , setIsLoading ] = useState ( false ) ;
2435 const [ error , setError ] = useState < string > ( ) ;
36+ const [ showAdvanced , setShowAdvanced ] = useState ( false ) ;
2537
2638 const isFormComplete = useMemo ( ( ) => {
2739 return chainId && recipientAddress && tokenAddressWithChain && amount ;
2840 } , [ chainId , recipientAddress , tokenAddressWithChain , amount ] ) ;
2941
30- const handleSubmit = async ( e : React . FormEvent ) => {
31- e . preventDefault ( ) ;
32- setError ( undefined ) ;
33- setIsLoading ( true ) ;
42+ const handleImageUpload = useCallback (
43+ async ( file : File ) => {
44+ try {
45+ setImage ( file ) ;
46+ setUploadingImage ( true ) ;
3447
35- try {
36- if ( ! chainId || ! recipientAddress || ! tokenAddressWithChain || ! amount ) {
37- throw new Error ( "All fields are required" ) ;
48+ const uri = await upload ( {
49+ client,
50+ files : [ file ] ,
51+ } ) ;
52+
53+ // eslint-disable-next-line no-restricted-syntax
54+ const resolvedUrl = resolveScheme ( {
55+ uri,
56+ client,
57+ } ) ;
58+
59+ setImageUri ( resolvedUrl ) ;
60+ toast . success ( "Image uploaded successfully" ) ;
61+ } catch ( error ) {
62+ console . error ( "Error uploading image:" , error ) ;
63+ toast . error ( "Failed to upload image" ) ;
64+ setImage ( null ) ;
65+ } finally {
66+ setUploadingImage ( false ) ;
3867 }
68+ } ,
69+ [ client ] ,
70+ ) ;
71+
72+ const handleSubmit = useCallback (
73+ async ( e : React . FormEvent ) => {
74+ e . preventDefault ( ) ;
75+ setError ( undefined ) ;
76+ setIsLoading ( true ) ;
77+
78+ try {
79+ if (
80+ ! chainId ||
81+ ! recipientAddress ||
82+ ! tokenAddressWithChain ||
83+ ! amount
84+ ) {
85+ throw new Error ( "All fields are required" ) ;
86+ }
87+
88+ const inputs = await parseInputs (
89+ client ,
90+ chainId ,
91+ tokenAddressWithChain ,
92+ recipientAddress ,
93+ amount ,
94+ ) ;
95+
96+ // Build checkout URL
97+ const params = new URLSearchParams ( {
98+ chainId : inputs . chainId . toString ( ) ,
99+ recipientAddress : inputs . recipientAddress ,
100+ tokenAddress : inputs . tokenAddress ,
101+ amount : inputs . amount . toString ( ) ,
102+ } ) ;
103+
104+ // Add title as name parameter if provided
105+ if ( title ) {
106+ params . set ( "name" , title ) ;
107+ }
108+
109+ // Add image URI if available
110+ if ( imageUri ) {
111+ params . set ( "image" , imageUri ) ;
112+ }
113+
114+ const checkoutUrl = `${ window . location . origin } /checkout?${ params . toString ( ) } ` ;
115+
116+ // Copy to clipboard
117+ await navigator . clipboard . writeText ( checkoutUrl ) ;
118+
119+ // Show success toast
120+ toast . success ( "Checkout link copied to clipboard." ) ;
121+ } catch ( err ) {
122+ setError ( err instanceof Error ? err . message : "An error occurred" ) ;
123+ } finally {
124+ setIsLoading ( false ) ;
125+ }
126+ } ,
127+ [
128+ amount ,
129+ chainId ,
130+ client ,
131+ imageUri ,
132+ recipientAddress ,
133+ title ,
134+ tokenAddressWithChain ,
135+ ] ,
136+ ) ;
137+
138+ const handlePreview = useCallback ( async ( ) => {
139+ if ( ! chainId || ! recipientAddress || ! tokenAddressWithChain || ! amount ) {
140+ toast . error ( "Please fill in all fields first" ) ;
141+ return ;
142+ }
39143
144+ try {
40145 const inputs = await parseInputs (
41146 client ,
42147 chainId ,
@@ -45,27 +150,36 @@ export function CheckoutLinkForm() {
45150 amount ,
46151 ) ;
47152
48- // Build checkout URL
49153 const params = new URLSearchParams ( {
50154 chainId : inputs . chainId . toString ( ) ,
51155 recipientAddress : inputs . recipientAddress ,
52156 tokenAddress : inputs . tokenAddress ,
53157 amount : inputs . amount . toString ( ) ,
54158 } ) ;
55159
56- const checkoutUrl = `${ window . location . origin } /checkout?${ params . toString ( ) } ` ;
160+ // Add title as name parameter if provided
161+ if ( title ) {
162+ params . set ( "name" , title ) ;
163+ }
57164
58- // Copy to clipboard
59- await navigator . clipboard . writeText ( checkoutUrl ) ;
165+ // Add image URI if available
166+ if ( imageUri ) {
167+ params . set ( "image" , imageUri ) ;
168+ }
60169
61- // Show success toast
62- toast . success ( "Checkout link copied to clipboard." ) ;
170+ window . open ( `/checkout?${ params . toString ( ) } ` , "_blank" ) ;
63171 } catch ( err ) {
64- setError ( err instanceof Error ? err . message : "An error occurred" ) ;
65- } finally {
66- setIsLoading ( false ) ;
172+ toast . error ( err instanceof Error ? err . message : "An error occurred" ) ;
67173 }
68- } ;
174+ } , [
175+ amount ,
176+ chainId ,
177+ client ,
178+ imageUri ,
179+ recipientAddress ,
180+ title ,
181+ tokenAddressWithChain ,
182+ ] ) ;
69183
70184 return (
71185 < Card className = "mx-auto w-full max-w-[500px]" >
@@ -138,6 +252,65 @@ export function CheckoutLinkForm() {
138252 />
139253 </ div >
140254
255+ < div className = "space-y-4" >
256+ < Button
257+ type = "button"
258+ variant = "ghost"
259+ className = "flex w-full items-center justify-between px-0 text-muted-foreground hover:bg-transparent"
260+ onClick = { ( ) => setShowAdvanced ( ! showAdvanced ) }
261+ >
262+ < span > Advanced Options</ span >
263+ < ChevronDownIcon
264+ className = { `size-4 transition-transform duration-200 ease-in-out ${
265+ showAdvanced ? "rotate-180" : ""
266+ } `}
267+ />
268+ </ Button >
269+
270+ < div
271+ className = { `grid transition-all duration-200 ease-in-out ${
272+ showAdvanced
273+ ? "grid-rows-[1fr] opacity-100"
274+ : "grid-rows-[0fr] opacity-0"
275+ } `}
276+ >
277+ < div className = "overflow-hidden" >
278+ < div className = "space-y-6 pt-2" >
279+ < div className = "space-y-2" >
280+ < Label htmlFor = "title" className = "font-medium text-sm" >
281+ Title
282+ </ Label >
283+ < Input
284+ id = "title"
285+ value = { title }
286+ onChange = { ( e ) => setTitle ( e . target . value ) }
287+ placeholder = "Checkout for..."
288+ className = "w-full"
289+ />
290+ </ div >
291+
292+ < div className = "space-y-2" >
293+ < Label htmlFor = "image" className = "font-medium text-sm" >
294+ Image
295+ </ Label >
296+ < div className = "w-full px-1 pb-1" >
297+ < FileInput
298+ accept = { { "image/*" : [ ] } }
299+ setValue = { handleImageUpload }
300+ value = { image || imageUri }
301+ className = "!rounded-md aspect-square h-24 w-full"
302+ isDisabled = { uploadingImage }
303+ selectOrUpload = "Upload"
304+ helperText = "image"
305+ fileUrl = { imageUri }
306+ />
307+ </ div >
308+ </ div >
309+ </ div >
310+ </ div >
311+ </ div >
312+ </ div >
313+
141314 { error && < div className = "text-red-500 text-sm" > { error } </ div > }
142315
143316 < div className = "flex gap-2" >
@@ -146,31 +319,7 @@ export function CheckoutLinkForm() {
146319 variant = "outline"
147320 className = "flex-1"
148321 disabled = { isLoading || ! isFormComplete }
149- onClick = { async ( ) => {
150- if (
151- ! chainId ||
152- ! recipientAddress ||
153- ! tokenAddressWithChain ||
154- ! amount
155- ) {
156- toast . error ( "Please fill in all fields first" ) ;
157- return ;
158- }
159- const inputs = await parseInputs (
160- client ,
161- chainId ,
162- tokenAddressWithChain ,
163- recipientAddress ,
164- amount ,
165- ) ;
166- const params = new URLSearchParams ( {
167- chainId : inputs . chainId . toString ( ) ,
168- recipientAddress : inputs . recipientAddress ,
169- tokenAddress : inputs . tokenAddress ,
170- amount : inputs . amount . toString ( ) ,
171- } ) ;
172- window . open ( `/checkout?${ params . toString ( ) } ` , "_blank" ) ;
173- } }
322+ onClick = { handlePreview }
174323 >
175324 Preview
176325 </ Button >
@@ -220,9 +369,7 @@ async function parseInputs(
220369 throw new Error ( "Invalid recipient address" ) ;
221370 }
222371
223- const amountInWei = BigInt (
224- Number . parseFloat ( decimalAmount ) * 10 ** currencyMetadata . decimals ,
225- ) ;
372+ const amountInWei = toUnits ( decimalAmount , currencyMetadata . decimals ) ;
226373
227374 return {
228375 chainId,
0 commit comments