Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"use client";

import { useState } from "react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { arbitrum } from "thirdweb/chains";
import { TabButtons } from "@/components/ui/tab-buttons";
import { LeftSection } from "../components/LeftSection";
import { RightSection } from "../components/RightSection";
import type { BridgeComponentsPlaygroundOptions } from "../components/types";

const defaultOptions: BridgeComponentsPlaygroundOptions = {
integrationType: "react",
payOptions: {
buyTokenAddress: undefined,
buyTokenAmount: "0.002",
Expand All @@ -29,16 +32,70 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
},
};

export function BuyPlayground() {
const [options, setOptions] =
useState<BridgeComponentsPlaygroundOptions>(defaultOptions);
function updatePageUrl(
tab: BridgeComponentsPlaygroundOptions["integrationType"],
) {
const url = new URL(window.location.href);
if (tab === defaultOptions.integrationType) {
url.searchParams.delete("tab");
} else {
url.searchParams.set("tab", tab || "");
}

window.history.replaceState({}, "", url.toString());
}

export function BuyPlayground(props: { defaultTab?: "iframe" | "react" }) {
const { theme } = useTheme();

const [options, setOptions] = useState<BridgeComponentsPlaygroundOptions>(
() => ({
...defaultOptions,
integrationType: props.defaultTab || defaultOptions.integrationType,
}),
);

// Change theme on global theme change
useEffect(() => {
setOptions((prev) => ({
...prev,
theme: {
...prev.theme,
type: theme === "dark" ? "dark" : "light",
},
}));
}, [theme]);

useEffect(() => {
updatePageUrl(options.integrationType);
}, [options.integrationType]);

return (
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<LeftSection widget="buy" options={options} setOptions={setOptions} />
<div>
<TabButtons
tabs={[
{
name: "React",
onClick: () => setOptions({ ...options, integrationType: "react" }),
isActive: options.integrationType === "react",
},
{
name: "Iframe",
onClick: () =>
setOptions({ ...options, integrationType: "iframe" }),
isActive: options.integrationType === "iframe",
},
]}
/>

<div className="h-6" />

<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<LeftSection widget="buy" options={options} setOptions={setOptions} />
</div>
<RightSection widget="buy" options={options} />
</div>
<RightSection widget="buy" options={options} />
</div>
);
}
12 changes: 10 additions & 2 deletions apps/playground-web/src/app/bridge/buy-widget/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ export const metadata = createMetadata({
},
});

export default function Page() {
export default async function Page(props: {
searchParams: Promise<{ tab?: string }>;
}) {
const searchParams = await props.searchParams;
const defaultTab =
searchParams.tab === "iframe" || searchParams.tab === "react"
? searchParams.tab
: undefined;

return (
<ThirdwebProvider>
<PageLayout
Expand All @@ -28,7 +36,7 @@ export default function Page() {
description={description}
docsLink="https://portal.thirdweb.com/references/typescript/v5/BuyWidget?utm_source=playground"
>
<BuyPlayground />
<BuyPlayground defaultTab={defaultTab} />
</PageLayout>
</ThirdwebProvider>
);
Expand Down
21 changes: 16 additions & 5 deletions apps/playground-web/src/app/bridge/components/CodeGen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
stringifyImports,
stringifyProps,
} from "../../../lib/code-gen";
import { buildBuyIframeUrl } from "./buildBuyIframeUrl";
import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
import type { BridgeComponentsPlaygroundOptions } from "./types";

Expand All @@ -27,7 +28,8 @@ export function CodeGen(props: {
options: BridgeComponentsPlaygroundOptions;
}) {
const isIframe =
props.widget === "checkout" && props.options.integrationType === "iframe";
(props.widget === "checkout" || props.widget === "buy") &&
props.options.integrationType === "iframe";

return (
<div className="flex w-full grow flex-col">
Expand All @@ -36,7 +38,7 @@ export function CodeGen(props: {
className="grow"
code={
isIframe
? getIframeCode(props.options)
? getIframeCode(props.widget as "buy" | "checkout", props.options)
: getCode(props.widget, props.options)
}
lang={isIframe ? "html" : "tsx"}
Expand All @@ -46,13 +48,22 @@ export function CodeGen(props: {
);
}

function getIframeCode(options: BridgeComponentsPlaygroundOptions) {
const src = buildCheckoutIframeUrl(options);
function getIframeCode(
widget: "buy" | "checkout",
options: BridgeComponentsPlaygroundOptions,
) {
const src =
widget === "buy"
? buildBuyIframeUrl(options)
: buildCheckoutIframeUrl(options);

const hasImage = src.includes("image=");
const height = hasImage ? "850px" : "700px";

return `\
<iframe
src="${src}"
height="700px"
height="${height}"
width="100%"
style="border: 0;"
/>`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,8 @@ export function LeftSection(props: {

{/* Colors - disabled for iframe */}
{!(
props.widget === "checkout" && options.integrationType === "iframe"
(props.widget === "checkout" || props.widget === "buy") &&
options.integrationType === "iframe"
) && (
<ColorFormGroup
onChange={(newTheme) => {
Expand Down
64 changes: 49 additions & 15 deletions apps/playground-web/src/app/bridge/components/RightSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Button } from "../../../components/ui/button";
import { THIRDWEB_CLIENT } from "../../../lib/client";
import { cn } from "../../../lib/utils";
import { buildBuyIframeUrl } from "./buildBuyIframeUrl";
import { buildCheckoutIframeUrl } from "./buildCheckoutIframeUrl";
import { CodeGen } from "./CodeGen";
import type { BridgeComponentsPlaygroundOptions } from "./types";
Expand Down Expand Up @@ -155,21 +156,15 @@ export function RightSection(props: {
previewTab !== "code" && "items-center",
)}
>
<BackgroundPattern />

{previewTab === "ui" &&
(props.widget === "checkout" &&
props.options.integrationType === "iframe" ? (
<iframe
src={buildCheckoutIframeUrl(props.options)}
height="700px"
width="100%"
title="Checkout Widget"
className="fade-in-0 animate-in rounded-xl duration-500"
style={{
border: "0",
}}
/>
(props.options.integrationType === "iframe" ? (
props.widget === "checkout" ? (
<CheckoutIframePreview options={props.options} />
) : props.widget === "buy" ? (
<BuyWidgetIframePreview options={props.options} />
) : (
embed
)
) : (
embed
))}
Expand All @@ -182,7 +177,46 @@ export function RightSection(props: {
);
}

function BackgroundPattern() {
function CheckoutIframePreview(props: {
options: BridgeComponentsPlaygroundOptions;
}) {
const src = buildCheckoutIframeUrl(props.options);
return (
<iframe
src={src}
height="700px"
width="100%"
title="Checkout Widget"
className="fade-in-0 animate-in rounded-xl duration-500"
style={{
border: "0",
}}
/>
);
}

function BuyWidgetIframePreview(props: {
options: BridgeComponentsPlaygroundOptions;
}) {
const src = buildBuyIframeUrl(props.options);

const hasImage = src.includes("image=");
const height = hasImage ? "850px" : "700px";
return (
<iframe
src={src}
height={height}
width="100%"
title="Buy Widget"
className="fade-in-0 animate-in rounded-xl duration-500"
style={{
border: "0",
}}
/>
);
}

function _BackgroundPattern() {
const color = "hsl(var(--foreground)/15%)";
return (
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { BridgeComponentsPlaygroundOptions } from "./types";

const BUY_WIDGET_IFRAME_BASE_URL = "https://thirdweb.com/bridge/buy-widget";

export function buildBuyIframeUrl(options: BridgeComponentsPlaygroundOptions) {
const url = new URL(BUY_WIDGET_IFRAME_BASE_URL);

// Chain (optional)
if (options.payOptions.buyTokenChain?.id) {
url.searchParams.set("chain", String(options.payOptions.buyTokenChain.id));
}

// Token address (optional - if not set, native token is used)
if (options.payOptions.buyTokenAddress) {
url.searchParams.set("tokenAddress", options.payOptions.buyTokenAddress);
}

// Amount (optional)
if (options.payOptions.buyTokenAmount) {
url.searchParams.set("amount", options.payOptions.buyTokenAmount);
}

// Receiver address (optional)
if (options.payOptions.receiverAddress) {
url.searchParams.set("receiver", options.payOptions.receiverAddress);
}

// Theme (only add if light, dark is default)
if (options.theme.type === "light") {
url.searchParams.set("theme", "light");
}

// Currency (only add if not USD, USD is default)
if (options.payOptions.currency && options.payOptions.currency !== "USD") {
url.searchParams.set("currency", options.payOptions.currency);
}

// Branding
if (options.payOptions.showThirdwebBranding === false) {
url.searchParams.set("showThirdwebBranding", "false");
}

// Product info
if (options.payOptions.title) {
url.searchParams.set("title", options.payOptions.title);
}

if (options.payOptions.description) {
url.searchParams.set("description", options.payOptions.description);
}

if (options.payOptions.image) {
url.searchParams.set("image", options.payOptions.image);
}

if (options.payOptions.buttonLabel) {
url.searchParams.set("buttonLabel", options.payOptions.buttonLabel);
}

// Payment methods
if (
options.payOptions.paymentMethods &&
options.payOptions.paymentMethods.length === 1
) {
url.searchParams.set(
"paymentMethods",
options.payOptions.paymentMethods[0],
);
}

return url.toString();
}
Loading