-
Notifications
You must be signed in to change notification settings - Fork 636
[MNY-342] Dashboard: Add Buy Widget iframe #8613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| "use client"; | ||
|
|
||
| import { useMemo } from "react"; | ||
| import { type Address, defineChain } from "thirdweb"; | ||
| import { BuyWidget, type SupportedFiatCurrency } from "thirdweb/react"; | ||
| import { NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID } from "@/constants/public-envs"; | ||
| import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server"; | ||
|
|
||
| export function BuyWidgetEmbed({ | ||
| chainId, | ||
| tokenAddress, | ||
| amount, | ||
| showThirdwebBranding, | ||
| theme, | ||
| currency, | ||
| title, | ||
| description, | ||
| image, | ||
| paymentMethods, | ||
| buttonLabel, | ||
| receiverAddress, | ||
| country, | ||
| }: { | ||
| chainId?: number; | ||
| tokenAddress?: Address; | ||
| amount?: string; | ||
| showThirdwebBranding?: boolean; | ||
| theme: "light" | "dark"; | ||
| currency?: SupportedFiatCurrency; | ||
| title?: string; | ||
| description?: string; | ||
| image?: string; | ||
| paymentMethods?: ("crypto" | "card")[]; | ||
| buttonLabel?: string; | ||
| receiverAddress?: Address; | ||
| country?: string; | ||
| }) { | ||
| const client = useMemo( | ||
| () => | ||
| getConfiguredThirdwebClient({ | ||
| clientId: NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID, | ||
| secretKey: undefined, | ||
| teamId: undefined, | ||
| }), | ||
| [], | ||
| ); | ||
|
|
||
| const chain = useMemo(() => { | ||
| if (!chainId) return undefined; | ||
| // eslint-disable-next-line no-restricted-syntax | ||
| return defineChain(chainId); | ||
| }, [chainId]); | ||
|
|
||
| return ( | ||
| <BuyWidget | ||
| className="shadow-xl" | ||
| client={client} | ||
| chain={chain} | ||
| tokenAddress={tokenAddress} | ||
| amount={amount} | ||
| showThirdwebBranding={showThirdwebBranding} | ||
| theme={theme} | ||
| currency={currency} | ||
| title={title} | ||
| description={description} | ||
| image={image} | ||
| paymentMethods={paymentMethods} | ||
| buttonLabel={buttonLabel} | ||
| receiverAddress={receiverAddress} | ||
| country={country} | ||
| onSuccess={() => { | ||
| sendMessageToParent({ | ||
| source: "buy-widget", | ||
| type: "success", | ||
| }); | ||
| }} | ||
| onError={(error) => { | ||
| sendMessageToParent({ | ||
| source: "buy-widget", | ||
| type: "error", | ||
| message: error.message, | ||
| }); | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function sendMessageToParent(content: object) { | ||
| try { | ||
| window.parent.postMessage(content, "*"); | ||
| } catch (error) { | ||
| console.error("Failed to send post message to parent window"); | ||
| console.error(error); | ||
| } | ||
| } | ||
|
Comment on lines
+88
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: Restrict Using a wildcard origin 🔒 Recommended fix: Validate and restrict target origin-function sendMessageToParent(content: object) {
+function sendMessageToParent(content: object) {
+ // Only send to expected parent origins
+ const allowedOrigins = [
+ 'https://thirdweb.com',
+ 'https://dashboard.thirdweb.com',
+ // Add other allowed origins
+ ];
+
+ const parentOrigin = document.referrer ? new URL(document.referrer).origin : '*';
+
try {
- window.parent.postMessage(content, "*");
+ if (allowedOrigins.includes(parentOrigin) || parentOrigin === window.location.origin) {
+ window.parent.postMessage(content, parentOrigin);
+ } else {
+ console.warn('Parent origin not in allowlist:', parentOrigin);
+ }
} catch (error) {
console.error("Failed to send post message to parent window");
console.error(error);
}
}Alternatively, if the expected parent origin is known at build time or can be passed as a query parameter, use that instead of 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Inter } from "next/font/google"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from "@/lib/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fontSans = Inter({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: "swap", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| subsets: ["latin"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variable: "--font-sans", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function BuyWidgetLayout({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| children, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| children: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <html lang="en" suppressHydrationWarning> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <body | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "min-h-dvh bg-background font-sans antialiased flex flex-col", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fontSans.variable, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </body> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </html> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nested layouts must not declare In Next.js App Router, only the root Additionally, per coding guidelines, layouts should reuse 🔧 Proposed fix: Remove html/body wrapper-export default function BuyWidgetLayout({
+export default async function BuyWidgetLayout({
children,
}: {
children: React.ReactNode;
}) {
- return (
- <html lang="en" suppressHydrationWarning>
- <body
- className={cn(
- "min-h-dvh bg-background font-sans antialiased flex flex-col",
- fontSans.variable,
- )}
- >
- {children}
- </body>
- </html>
- );
+ return <>{children}</>;
}If custom styling is needed, wrap Based on coding guidelines: "Reuse 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import type { Metadata } from "next"; | ||
| import "@workspace/ui/global.css"; | ||
| import type { SupportedFiatCurrency } from "thirdweb/react"; | ||
| import { isValidCurrency } from "../_common/isValidCurrency"; | ||
| import { | ||
| onlyAddress, | ||
| onlyNumber, | ||
| parseQueryParams, | ||
| } from "../_common/parseQueryParams"; | ||
| import { BridgeProvidersLite } from "../(general)/components/client/Providers.client"; | ||
| import { BuyWidgetEmbed } from "./BuyWidgetEmbed.client"; | ||
MananTank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const title = "thirdweb Buy: Purchase Crypto with Ease"; | ||
| const description = | ||
| "Buy crypto tokens with credit card or other payment methods. Simple and secure purchases with thirdweb."; | ||
|
|
||
| export const metadata: Metadata = { | ||
| description, | ||
| openGraph: { | ||
| description, | ||
| title, | ||
| }, | ||
| title, | ||
| }; | ||
|
|
||
| type SearchParams = { | ||
| [key: string]: string | string[] | undefined; | ||
| }; | ||
|
|
||
| export default async function Page(props: { | ||
| searchParams: Promise<SearchParams>; | ||
| }) { | ||
| const searchParams = await props.searchParams; | ||
|
|
||
| // Token params | ||
| const chainId = parseQueryParams(searchParams.chain, onlyNumber); | ||
| const tokenAddress = parseQueryParams(searchParams.tokenAddress, onlyAddress); | ||
| const amount = parseQueryParams(searchParams.amount, (v) => v); | ||
|
|
||
| // Optional params | ||
| const showThirdwebBranding = parseQueryParams( | ||
| searchParams.showThirdwebBranding, | ||
| // biome-ignore lint/complexity/noUselessTernary: this is easier to understand | ||
| (v) => (v === "false" ? false : true), | ||
| ); | ||
|
|
||
| const theme = | ||
| parseQueryParams(searchParams.theme, (v) => | ||
| v === "light" ? "light" : "dark", | ||
| ) || "dark"; | ||
|
|
||
| const currency = parseQueryParams(searchParams.currency, (v) => | ||
| isValidCurrency(v) ? (v as SupportedFiatCurrency) : undefined, | ||
| ); | ||
|
|
||
| // Metadata params | ||
| const widgetTitle = parseQueryParams(searchParams.title, (v) => v); | ||
| const widgetDescription = parseQueryParams( | ||
| searchParams.description, | ||
| (v) => v, | ||
| ); | ||
| const image = parseQueryParams(searchParams.image, (v) => v); | ||
|
|
||
| // Payment params | ||
| const paymentMethods = parseQueryParams(searchParams.paymentMethods, (v) => { | ||
| if (v === "crypto" || v === "card") { | ||
| return [v] as ("crypto" | "card")[]; | ||
| } | ||
| return undefined; | ||
| }); | ||
MananTank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const buttonLabel = parseQueryParams(searchParams.buttonLabel, (v) => v); | ||
| const receiverAddress = parseQueryParams(searchParams.receiver, onlyAddress); | ||
| const country = parseQueryParams(searchParams.country, (v) => v); | ||
|
|
||
| return ( | ||
| <BridgeProvidersLite forcedTheme={theme}> | ||
| <div className="flex min-h-screen items-center justify-center bg-background px-4 py-8"> | ||
| <BuyWidgetEmbed | ||
| chainId={chainId} | ||
| tokenAddress={tokenAddress} | ||
| amount={amount} | ||
| showThirdwebBranding={showThirdwebBranding} | ||
| theme={theme} | ||
| currency={currency} | ||
| title={widgetTitle} | ||
| description={widgetDescription} | ||
| image={image} | ||
| paymentMethods={paymentMethods} | ||
| buttonLabel={buttonLabel} | ||
| receiverAddress={receiverAddress} | ||
| country={country} | ||
| /> | ||
| </div> | ||
| </BridgeProvidersLite> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid importing
.servermodules in client components.Importing
getConfiguredThirdwebClientfrom@/constants/thirdweb.serverin a client component violates the server/client boundary. The.serversuffix indicates this module is intended for server-side use only and may contain server-specific logic or configurations.Consider either:
getConfiguredThirdwebClient.client.ts)createThirdwebClientfrom the thirdweb SDKAlternative: Direct client initialization
🤖 Prompt for AI Agents