Skip to content

Commit 51871d6

Browse files
committed
feat: checkout page
1 parent 736c3f8 commit 51871d6

File tree

4 files changed

+223
-5
lines changed

4 files changed

+223
-5
lines changed

apps/dashboard/redirects.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,6 @@ async function redirects() {
100100
destination: "/auth",
101101
permanent: false,
102102
},
103-
{
104-
source: "/checkout",
105-
destination: "/connect",
106-
permanent: false,
107-
},
108103
{
109104
source: "/extensions",
110105
destination: "/build",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
import { useV5DashboardChain } from "lib/v5-adapter";
3+
import { useTheme } from "next-themes";
4+
import { useMemo } from "react";
5+
import { createThirdwebClient, toTokens } from "thirdweb";
6+
import { AutoConnect, PayEmbed } from "thirdweb/react";
7+
8+
export function CheckoutEmbed({
9+
chainId,
10+
recipientAddress,
11+
amount,
12+
token,
13+
name,
14+
image,
15+
redirectUri,
16+
clientId,
17+
}: {
18+
chainId: number;
19+
recipientAddress: string;
20+
amount: bigint;
21+
token: { name: string; symbol: string; address: string; decimals: number };
22+
name?: string;
23+
image?: string;
24+
redirectUri?: string;
25+
clientId: string;
26+
}) {
27+
const client = useMemo(() => createThirdwebClient({ clientId }), [clientId]);
28+
const chain = useV5DashboardChain(chainId);
29+
const { theme } = useTheme();
30+
31+
return (
32+
<>
33+
<AutoConnect client={client} />
34+
<PayEmbed
35+
client={client}
36+
theme={theme === "light" ? "light" : "dark"}
37+
payOptions={{
38+
metadata: {
39+
name,
40+
image,
41+
},
42+
mode: "direct_payment",
43+
paymentInfo: {
44+
chain,
45+
sellerAddress: recipientAddress,
46+
amount: toTokens(amount, token.decimals),
47+
token,
48+
},
49+
onPurchaseSuccess: (result) => {
50+
if (!redirectUri) return;
51+
const url = new URL(redirectUri);
52+
if (result.type === "transaction") {
53+
url.searchParams.set("txHash", result.transactionHash);
54+
return window.open(url.toString());
55+
}
56+
if (result.status.status === "NOT_FOUND") {
57+
throw new Error("Transaction not found");
58+
}
59+
const txHash = result.status.source?.transactionHash;
60+
if (typeof txHash === "string") {
61+
url.searchParams.set("txHash", txHash);
62+
}
63+
},
64+
}}
65+
/>
66+
</>
67+
);
68+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { cn } from "@/lib/utils";
2+
import { ThemeProvider } from "next-themes";
3+
import { Inter } from "next/font/google";
4+
import { ThirdwebProvider } from "thirdweb/react";
5+
6+
const fontSans = Inter({
7+
subsets: ["latin"],
8+
variable: "--font-sans",
9+
display: "swap",
10+
});
11+
12+
export default function CheckoutLayout({
13+
children,
14+
}: { children: React.ReactNode }) {
15+
return (
16+
<html lang="en" suppressHydrationWarning>
17+
<ThirdwebProvider>
18+
<ThemeProvider
19+
attribute="class"
20+
disableTransitionOnChange
21+
enableSystem={false}
22+
defaultTheme="dark"
23+
>
24+
<body
25+
className={cn(
26+
"bg-background font-sans antialiased",
27+
fontSans.variable,
28+
)}
29+
>
30+
{children}
31+
</body>
32+
</ThemeProvider>
33+
</ThirdwebProvider>
34+
</html>
35+
);
36+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import "../../global.css";
2+
import { getThirdwebClient } from "@/constants/thirdweb.server";
3+
import type { Metadata } from "next";
4+
import { createThirdwebClient, defineChain, getContract } from "thirdweb";
5+
import { name, symbol } from "thirdweb/extensions/common";
6+
import { decimals } from "thirdweb/extensions/erc20";
7+
import { checksumAddress } from "thirdweb/utils";
8+
import { CheckoutEmbed } from "./components/client/CheckoutEmbed.client";
9+
10+
const title = "Universal Bridge: Swap, Bridge, and On-Ramp";
11+
const description =
12+
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
13+
14+
export const metadata: Metadata = {
15+
title,
16+
description,
17+
openGraph: {
18+
title,
19+
description,
20+
},
21+
};
22+
23+
export default async function RoutesPage({
24+
searchParams,
25+
}: { searchParams: Record<string, string | string[]> }) {
26+
const {
27+
chainId,
28+
recipientAddress,
29+
tokenAddress,
30+
amount,
31+
clientId,
32+
redirectUri,
33+
} = searchParams;
34+
35+
if (!chainId || Array.isArray(chainId)) {
36+
throw new Error("A single chainId parameter is required.");
37+
}
38+
if (!recipientAddress || Array.isArray(recipientAddress)) {
39+
throw new Error("A single recipientAddress parameter is required.");
40+
}
41+
if (!tokenAddress || Array.isArray(tokenAddress)) {
42+
throw new Error("A single tokenAddress parameter is required.");
43+
}
44+
if (!amount || Array.isArray(amount)) {
45+
throw new Error("An single amount parameter is required.");
46+
}
47+
if (Array.isArray(clientId)) {
48+
throw new Error("A single clientId parameter is required.");
49+
}
50+
if (Array.isArray(redirectUri)) {
51+
throw new Error("A single redirectUri parameter is required.");
52+
}
53+
54+
// Use any provided clientId or use the dashboard client
55+
const client =
56+
clientId && !Array.isArray(clientId)
57+
? createThirdwebClient({ clientId })
58+
: getThirdwebClient(undefined);
59+
60+
const token = await (async () => {
61+
if (
62+
checksumAddress(tokenAddress) ===
63+
"0x0000000000000000000000000000000000000000" ||
64+
checksumAddress(tokenAddress) ===
65+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
66+
) {
67+
return {
68+
name: "Ether",
69+
symbol: "ETH",
70+
address: tokenAddress,
71+
decimals: 18,
72+
};
73+
} else {
74+
const tokenContract = getContract({
75+
client,
76+
// eslint-disable-next-line no-restricted-syntax
77+
chain: defineChain(Number(chainId)),
78+
address: tokenAddress,
79+
});
80+
const symbolPromise = symbol({ contract: tokenContract });
81+
const namePromise = name({ contract: tokenContract });
82+
const decimalsPromise = decimals({ contract: tokenContract });
83+
84+
const [symbolResult, nameResult, decimalsResult] = await Promise.all([
85+
symbolPromise,
86+
namePromise,
87+
decimalsPromise,
88+
]);
89+
return {
90+
name: nameResult,
91+
symbol: symbolResult,
92+
address: tokenAddress,
93+
decimals: Number(decimalsResult),
94+
};
95+
}
96+
})();
97+
98+
return (
99+
<div className="relative mx-auto flex h-screen w-screen flex-col items-center justify-center overflow-hidden border py-10">
100+
<main className="container z-10 flex justify-center">
101+
<CheckoutEmbed
102+
redirectUri={redirectUri}
103+
chainId={Number(chainId)}
104+
recipientAddress={recipientAddress}
105+
amount={BigInt(amount)}
106+
token={token}
107+
clientId={client.clientId}
108+
/>
109+
</main>
110+
111+
{/* eslint-disable-next-line @next/next/no-img-element */}
112+
<img
113+
alt=""
114+
src="/assets/login/background.svg"
115+
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
116+
/>
117+
</div>
118+
);
119+
}

0 commit comments

Comments
 (0)