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
20 changes: 20 additions & 0 deletions .changeset/breezy-scissors-scream.md
Original file line number Diff line number Diff line change
@@ -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";

<SiteEmbed
src="https://thirdweb.com"
client={client}
ecosystem={ecosystem}
/>
```

Note: Embedded sites must include `<AutoConnect />` and support frame-ancestors in their Content Security Policy.
3 changes: 3 additions & 0 deletions packages/thirdweb/src/exports/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,6 @@ export type {
FarcasterProfile,
LensProfile,
} from "../social/types.js";

// Site Embed
export { SiteEmbed } from "../react/web/ui/SiteEmbed.js";
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -43,8 +45,25 @@
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);
}

Check warning on line 64 in packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts#L53-L64

Added lines #L53 - L64 were not covered by tests

if (walletId) {
lastActiveWalletId = walletId;
lastConnectedWalletIds = lastConnectedWalletIds?.includes(walletId)
? lastConnectedWalletIds
Expand Down
39 changes: 39 additions & 0 deletions packages/thirdweb/src/react/web/ui/SiteEmbed.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<SiteEmbed src={testUrl} client={TEST_CLIENT} />,
);

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(<SiteEmbed src={testUrl} client={{} as any} />),
).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(
<SiteEmbed src={testUrl} client={TEST_CLIENT} />,
{
setConnectedWallet: true,
},
);

const iframe = container.querySelector("iframe");
expect(iframe).toBeTruthy();
await waitFor(() => expect(iframe?.src).toContain("walletId="));
});
});
90 changes: 90 additions & 0 deletions packages/thirdweb/src/react/web/ui/SiteEmbed.tsx
Original file line number Diff line number Diff line change
@@ -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 <AutoConnect /> 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";
*
* <SiteEmbed src="https://thirdweb.com" clientId="thirdweb-client-id" />
* ```
*/
export function SiteEmbed({
src,
client,
ecosystem,
...props
}: {
src: string;
client: ThirdwebClient;
ecosystem?: Ecosystem;
} & React.DetailedHTMLProps<
React.IframeHTMLAttributes<HTMLIFrameElement>,
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);

Check warning on line 75 in packages/thirdweb/src/react/web/ui/SiteEmbed.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/SiteEmbed.tsx#L75

Added line #L75 was not covered by tests
}
if (authCookie) {
url.searchParams.set("authCookie", authCookie);

Check warning on line 78 in packages/thirdweb/src/react/web/ui/SiteEmbed.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/SiteEmbed.tsx#L78

Added line #L78 was not covered by tests
}

return (
<iframe
src={encodeURI(url.toString())}
width="100%"
height="100%"
allowFullScreen
{...props}
/>
);
}
15 changes: 11 additions & 4 deletions packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
walletId?: WalletId;
authResult?: AuthStoredTokenWithCookieReturnType;
authProvider?: AuthOption;
authCookie?: string;
} {
if (!window?.location) {
// Not in web
Expand All @@ -20,18 +21,24 @@
const authResultString = params.get("authResult");
const walletId = params.get("walletId") as WalletId | undefined;
const authProvider = params.get("authProvider") as AuthOption | undefined;
const authCookie = params.get("authCookie") as string | undefined;

if (authResultString && walletId) {
const authResult = JSON.parse(authResultString);
params.delete("authResult");
if ((authCookie || authResultString) && walletId) {
const authResult = (() => {
if (authResultString) {
params.delete("authResult");
return JSON.parse(decodeURIComponent(authResultString));
}
})();

Check warning on line 32 in packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts#L27-L32

Added lines #L27 - L32 were not covered by tests
params.delete("walletId");
params.delete("authProvider");
params.delete("authCookie");

Check warning on line 35 in packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts#L35

Added line #L35 was not covered by tests
window.history.pushState(
{},
"",
`${window.location.pathname}?${params.toString()}`,
);
return { walletId, authResult, authProvider };
return { walletId, authResult, authProvider, authCookie };

Check warning on line 41 in packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/get-url-token.ts#L41

Added line #L41 was not covered by tests
}
return {};
}
Loading