Skip to content

Commit 8b21022

Browse files
committed
feat: add title and image upload
1 parent 04018ab commit 8b21022

File tree

2 files changed

+196
-49
lines changed

2 files changed

+196
-49
lines changed

apps/dashboard/src/@/api/universal-bridge/tokens.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type TokenMetadata = {
1111
iconUri?: string;
1212
};
1313

14-
export async function getUniversalBrigeTokens(props: {
14+
export async function getUniversalBridgeTokens(props: {
1515
clientId?: string;
1616
chainId?: number;
1717
}) {

apps/dashboard/src/app/checkout/components/client/CheckoutLinkForm.client.tsx

Lines changed: 195 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77
import { Input } from "@/components/ui/input";
88
import { Label } from "@/components/ui/label";
99
import { 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";
1212
import { 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";
1419
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
20+
import { resolveScheme, upload } from "thirdweb/storage";
21+
import { FileInput } from "../../../../components/shared/FileInput";
1522
import { resolveEns } from "../../../../lib/ens";
1623

1724
export 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+
// Resolve the IPFS URI for display
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

Comments
 (0)