Skip to content

Commit 5222ff1

Browse files
committed
feat: checkout page
1 parent 736c3f8 commit 5222ff1

File tree

4 files changed

+244
-5
lines changed

4 files changed

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

0 commit comments

Comments
 (0)