@@ -7,11 +7,13 @@ 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" ;
1313import { type ThirdwebClient , defineChain , getContract } from "thirdweb" ;
1414import { getCurrencyMetadata } from "thirdweb/extensions/erc20" ;
15+ import { resolveScheme , upload } from "thirdweb/storage" ;
16+ import { FileInput } from "../../../../components/shared/FileInput" ;
1517import { resolveEns } from "../../../../lib/ens" ;
1618
1719export function CheckoutLinkForm ( ) {
@@ -20,23 +22,121 @@ export function CheckoutLinkForm() {
2022 const [ recipientAddress , setRecipientAddress ] = useState ( "" ) ;
2123 const [ tokenAddressWithChain , setTokenAddressWithChain ] = useState ( "" ) ;
2224 const [ amount , setAmount ] = useState ( "" ) ;
25+ const [ title , setTitle ] = useState ( "" ) ;
26+ const [ image , setImage ] = useState < File | null > ( null ) ;
27+ const [ imageUri , setImageUri ] = useState < string > ( "" ) ;
28+ const [ uploadingImage , setUploadingImage ] = useState ( false ) ;
2329 const [ isLoading , setIsLoading ] = useState ( false ) ;
2430 const [ error , setError ] = useState < string > ( ) ;
31+ const [ showAdvanced , setShowAdvanced ] = useState ( false ) ;
2532
2633 const isFormComplete = useMemo ( ( ) => {
2734 return chainId && recipientAddress && tokenAddressWithChain && amount ;
2835 } , [ chainId , recipientAddress , tokenAddressWithChain , amount ] ) ;
2936
30- const handleSubmit = async ( e : React . FormEvent ) => {
31- e . preventDefault ( ) ;
32- setError ( undefined ) ;
33- setIsLoading ( true ) ;
37+ const handleImageUpload = useCallback (
38+ async ( file : File ) => {
39+ try {
40+ setImage ( file ) ;
41+ setUploadingImage ( true ) ;
3442
35- try {
36- if ( ! chainId || ! recipientAddress || ! tokenAddressWithChain || ! amount ) {
37- throw new Error ( "All fields are required" ) ;
43+ const uri = await upload ( {
44+ client,
45+ files : [ file ] ,
46+ } ) ;
47+
48+ // Resolve the IPFS URI for display
49+ const resolvedUrl = resolveScheme ( {
50+ uri,
51+ client,
52+ } ) ;
53+
54+ setImageUri ( resolvedUrl ) ;
55+ toast . success ( "Image uploaded successfully" ) ;
56+ } catch ( error ) {
57+ console . error ( "Error uploading image:" , error ) ;
58+ toast . error ( "Failed to upload image" ) ;
59+ setImage ( null ) ;
60+ } finally {
61+ setUploadingImage ( false ) ;
3862 }
63+ } ,
64+ [ client ] ,
65+ ) ;
66+
67+ const handleSubmit = useCallback (
68+ async ( e : React . FormEvent ) => {
69+ e . preventDefault ( ) ;
70+ setError ( undefined ) ;
71+ setIsLoading ( true ) ;
72+
73+ try {
74+ if (
75+ ! chainId ||
76+ ! recipientAddress ||
77+ ! tokenAddressWithChain ||
78+ ! amount
79+ ) {
80+ throw new Error ( "All fields are required" ) ;
81+ }
82+
83+ const inputs = await parseInputs (
84+ client ,
85+ chainId ,
86+ tokenAddressWithChain ,
87+ recipientAddress ,
88+ amount ,
89+ ) ;
90+
91+ // Build checkout URL
92+ const params = new URLSearchParams ( {
93+ chainId : inputs . chainId . toString ( ) ,
94+ recipientAddress : inputs . recipientAddress ,
95+ tokenAddress : inputs . tokenAddress ,
96+ amount : inputs . amount . toString ( ) ,
97+ } ) ;
98+
99+ // Add title as name parameter if provided
100+ if ( title ) {
101+ params . set ( "name" , title ) ;
102+ }
39103
104+ // Add image URI if available
105+ if ( imageUri ) {
106+ params . set ( "image" , imageUri ) ;
107+ }
108+
109+ const checkoutUrl = `${ window . location . origin } /checkout?${ params . toString ( ) } ` ;
110+
111+ // Copy to clipboard
112+ await navigator . clipboard . writeText ( checkoutUrl ) ;
113+
114+ // Show success toast
115+ toast . success ( "Checkout link copied to clipboard." ) ;
116+ } catch ( err ) {
117+ setError ( err instanceof Error ? err . message : "An error occurred" ) ;
118+ } finally {
119+ setIsLoading ( false ) ;
120+ }
121+ } ,
122+ [
123+ amount ,
124+ chainId ,
125+ client ,
126+ imageUri ,
127+ recipientAddress ,
128+ title ,
129+ tokenAddressWithChain ,
130+ ] ,
131+ ) ;
132+
133+ const handlePreview = useCallback ( async ( ) => {
134+ if ( ! chainId || ! recipientAddress || ! tokenAddressWithChain || ! amount ) {
135+ toast . error ( "Please fill in all fields first" ) ;
136+ return ;
137+ }
138+
139+ try {
40140 const inputs = await parseInputs (
41141 client ,
42142 chainId ,
@@ -45,27 +145,36 @@ export function CheckoutLinkForm() {
45145 amount ,
46146 ) ;
47147
48- // Build checkout URL
49148 const params = new URLSearchParams ( {
50149 chainId : inputs . chainId . toString ( ) ,
51150 recipientAddress : inputs . recipientAddress ,
52151 tokenAddress : inputs . tokenAddress ,
53152 amount : inputs . amount . toString ( ) ,
54153 } ) ;
55154
56- const checkoutUrl = `${ window . location . origin } /checkout?${ params . toString ( ) } ` ;
155+ // Add title as name parameter if provided
156+ if ( title ) {
157+ params . set ( "name" , title ) ;
158+ }
57159
58- // Copy to clipboard
59- await navigator . clipboard . writeText ( checkoutUrl ) ;
160+ // Add image URI if available
161+ if ( imageUri ) {
162+ params . set ( "image" , imageUri ) ;
163+ }
60164
61- // Show success toast
62- toast . success ( "Checkout link copied to clipboard." ) ;
165+ window . open ( `/checkout?${ params . toString ( ) } ` , "_blank" ) ;
63166 } catch ( err ) {
64- setError ( err instanceof Error ? err . message : "An error occurred" ) ;
65- } finally {
66- setIsLoading ( false ) ;
167+ toast . error ( err instanceof Error ? err . message : "An error occurred" ) ;
67168 }
68- } ;
169+ } , [
170+ amount ,
171+ chainId ,
172+ client ,
173+ imageUri ,
174+ recipientAddress ,
175+ title ,
176+ tokenAddressWithChain ,
177+ ] ) ;
69178
70179 return (
71180 < Card className = "mx-auto w-full max-w-[500px]" >
@@ -138,6 +247,65 @@ export function CheckoutLinkForm() {
138247 />
139248 </ div >
140249
250+ < div className = "space-y-4" >
251+ < Button
252+ type = "button"
253+ variant = "ghost"
254+ className = "flex w-full items-center justify-between px-0 text-muted-foreground hover:bg-transparent"
255+ onClick = { ( ) => setShowAdvanced ( ! showAdvanced ) }
256+ >
257+ < span > Advanced Options</ span >
258+ < ChevronDownIcon
259+ className = { `size-4 transition-transform duration-200 ease-in-out ${
260+ showAdvanced ? "rotate-180" : ""
261+ } `}
262+ />
263+ </ Button >
264+
265+ < div
266+ className = { `grid transition-all duration-200 ease-in-out ${
267+ showAdvanced
268+ ? "grid-rows-[1fr] opacity-100"
269+ : "grid-rows-[0fr] opacity-0"
270+ } `}
271+ >
272+ < div className = "overflow-hidden" >
273+ < div className = "space-y-6 pt-2" >
274+ < div className = "space-y-2" >
275+ < Label htmlFor = "title" className = "font-medium text-sm" >
276+ Title
277+ </ Label >
278+ < Input
279+ id = "title"
280+ value = { title }
281+ onChange = { ( e ) => setTitle ( e . target . value ) }
282+ placeholder = "Checkout for..."
283+ className = "w-full"
284+ />
285+ </ div >
286+
287+ < div className = "space-y-2" >
288+ < Label htmlFor = "image" className = "font-medium text-sm" >
289+ Image
290+ </ Label >
291+ < div className = "w-full pb-1 px-1" >
292+ < FileInput
293+ accept = { { "image/*" : [ ] } }
294+ setValue = { handleImageUpload }
295+ value = { image || imageUri }
296+ className = "!rounded-md aspect-square h-24 w-full"
297+ isDisabled = { uploadingImage }
298+ selectOrUpload = "Upload"
299+ helperText = "image"
300+ fileUrl = { imageUri }
301+ />
302+ </ div >
303+ </ div >
304+ </ div >
305+ </ div >
306+ </ div >
307+ </ div >
308+
141309 { error && < div className = "text-red-500 text-sm" > { error } </ div > }
142310
143311 < div className = "flex gap-2" >
@@ -146,31 +314,7 @@ export function CheckoutLinkForm() {
146314 variant = "outline"
147315 className = "flex-1"
148316 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- } }
317+ onClick = { handlePreview }
174318 >
175319 Preview
176320 </ Button >
0 commit comments