diff --git a/.changeset/breezy-scissors-scream.md b/.changeset/breezy-scissors-scream.md new file mode 100644 index 00000000000..1c376876372 --- /dev/null +++ b/.changeset/breezy-scissors-scream.md @@ -0,0 +1,20 @@ +--- +"thirdweb": minor +--- + +Added new `SiteEmbed` React component for embedding thirdweb-supported sites with seamless wallet connection support. + +The component allows you to embed other thirdweb-enabled sites while maintaining wallet connection state, supporting both in-app and ecosystem wallets. + +Example usage: +```tsx +import { SiteEmbed } from "thirdweb/react"; + + +``` + +Note: Embedded sites must include `` and support frame-ancestors in their Content Security Policy. diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index b8fc40b4220..69f34dac43a 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -195,3 +195,6 @@ export type { FarcasterProfile, LensProfile, } from "../social/types.js"; + +// Site Embed +export { SiteEmbed } from "../react/web/ui/SiteEmbed.js"; diff --git a/packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts b/packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts index befcfdb8dce..a44eac7b537 100644 --- a/packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts +++ b/packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts @@ -2,6 +2,8 @@ import { useQuery } from "@tanstack/react-query"; import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js"; +import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import { ClientScopedStorage } from "../../../../wallets/in-app/core/authentication/client-scoped-storage.js"; import { getUrlToken } from "../../../../wallets/in-app/web/lib/get-url-token.js"; import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; import { @@ -43,8 +45,25 @@ export function useAutoConnectCore( getStoredActiveWalletId(storage), ]); - const { authResult, walletId, authProvider } = getUrlToken(); - if (authResult && walletId) { + const { authResult, walletId, authProvider, authCookie } = getUrlToken(); + const wallet = wallets.find((w) => w.id === walletId); + + // If an auth cookie is found and this site supports the wallet, we'll set the auth cookie in the client storage + if (authCookie && wallet) { + const clientStorage = new ClientScopedStorage({ + storage, + clientId: props.client.clientId, + ecosystem: isEcosystemWallet(wallet) + ? { + id: wallet.id, + partnerId: wallet.getConfig()?.partnerId, + } + : undefined, + }); + await clientStorage.saveAuthCookie(authCookie); + } + + if (walletId) { lastActiveWalletId = walletId; lastConnectedWalletIds = lastConnectedWalletIds?.includes(walletId) ? lastConnectedWalletIds diff --git a/packages/thirdweb/src/react/web/ui/SiteEmbed.test.tsx b/packages/thirdweb/src/react/web/ui/SiteEmbed.test.tsx new file mode 100644 index 00000000000..fce3460b015 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/SiteEmbed.test.tsx @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { render, waitFor } from "../../../../test/src/react-render.js"; +import { TEST_CLIENT } from "../../../../test/src/test-clients.js"; +import { SiteEmbed } from "./SiteEmbed.js"; + +describe("SiteEmbed", () => { + it("renders iframe with correct src", () => { + const testUrl = "https://example.com/"; + const { container } = render( + , + ); + + const iframe = container.querySelector("iframe"); + expect(iframe).toBeTruthy(); + expect(iframe?.src).toBe(testUrl); + }); + + it("throws error if clientId is not provided", () => { + const testUrl = "https://example.com/"; + expect(() => + // biome-ignore lint/suspicious/noExplicitAny: testing invalid input + render(), + ).toThrow("The SiteEmbed client must have a clientId"); + }); + + it("adds wallet params to url when wallet is connected", async () => { + const testUrl = "https://example.com/"; + const { container } = render( + , + { + setConnectedWallet: true, + }, + ); + + const iframe = container.querySelector("iframe"); + expect(iframe).toBeTruthy(); + await waitFor(() => expect(iframe?.src).toContain("walletId=")); + }); +}); diff --git a/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx b/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx new file mode 100644 index 00000000000..041dc148d48 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx @@ -0,0 +1,90 @@ +"use client"; +import { useQuery } from "@tanstack/react-query"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { getLastAuthProvider } from "../../../react/core/utils/storage.js"; +import { webLocalStorage } from "../../../utils/storage/webStorage.js"; +import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import { ClientScopedStorage } from "../../../wallets/in-app/core/authentication/client-scoped-storage.js"; +import type { Ecosystem } from "../../../wallets/in-app/core/wallet/types.js"; +import { useActiveWallet } from "../../core/hooks/wallets/useActiveWallet.js"; + +/** + * Embeds another thirdweb-supported site for seamless in-app and ecosystem wallet connection. + * + * @note Make sure the embedded site includes and supports frame ancestors, see [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) for more information. + * + * @note The embedded site must support the connected wallet (ecosystem or in-app). + * + * @param {Object} props - The props to pass to the iframe + * @param {String} props.src - The URL of the site to embed + * @param {ThirdwebClient} props.client - The client to use for the embedded site + * @param {Ecosystem} [props.ecosystem] - The ecosystem to use for the embedded site + * + * @example + * ```tsx + * import { SiteEmbed } from "thirdweb/react"; + * + * + * ``` + */ +export function SiteEmbed({ + src, + client, + ecosystem, + ...props +}: { + src: string; + client: ThirdwebClient; + ecosystem?: Ecosystem; +} & React.DetailedHTMLProps< + React.IframeHTMLAttributes, + HTMLIFrameElement +>) { + if (!client.clientId) { + throw new Error("The SiteEmbed client must have a clientId"); + } + + const activeWallet = useActiveWallet(); + const walletId = activeWallet?.id; + + const { + data: { authProvider, authCookie } = {}, + } = useQuery({ + queryKey: ["site-embed", walletId, src, client.clientId, ecosystem], + enabled: + activeWallet && (isEcosystemWallet(activeWallet) || walletId === "inApp"), + queryFn: async () => { + const storage = new ClientScopedStorage({ + storage: webLocalStorage, + clientId: client.clientId, + ecosystem, + }); + + const authProvider = await getLastAuthProvider(webLocalStorage); + const authCookie = await storage.getAuthCookie(); + + return { authProvider, authCookie }; + }, + }); + + const url = new URL(src); + if (walletId) { + url.searchParams.set("walletId", walletId); + } + if (authProvider) { + url.searchParams.set("authProvider", authProvider); + } + if (authCookie) { + url.searchParams.set("authCookie", authCookie); + } + + return ( +