From f5f5ae63b441e0c0848c8ec88e0d1a81638b852b Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Wed, 6 Nov 2024 21:57:03 +0000 Subject: [PATCH] [SDK] Feature: Adds SiteLink component (#5326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes CNCT-2259 --- ## PR-Codex overview This PR introduces the `SiteLink` component to facilitate wallet-aware linking between thirdweb-enabled sites, enhancing user experience by sharing wallet state seamlessly. It also updates the `SiteEmbed` component documentation and includes tests for the new `SiteLink`. ### Detailed summary - Updated comment for `SiteEmbed` to reflect wallet connection usage. - Added `SiteLink` component for creating links with wallet parameters. - Provided usage example for `SiteLink` in documentation. - Implemented tests for `SiteLink` to verify functionality and error handling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .changeset/real-cars-sell.md | 22 +++++ packages/thirdweb/src/exports/react.ts | 3 +- .../thirdweb/src/react/web/ui/SiteEmbed.tsx | 6 +- .../src/react/web/ui/SiteLink.test.tsx | 48 +++++++++++ .../thirdweb/src/react/web/ui/SiteLink.tsx | 86 +++++++++++++++++++ 5 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 .changeset/real-cars-sell.md create mode 100644 packages/thirdweb/src/react/web/ui/SiteLink.test.tsx create mode 100644 packages/thirdweb/src/react/web/ui/SiteLink.tsx diff --git a/.changeset/real-cars-sell.md b/.changeset/real-cars-sell.md new file mode 100644 index 00000000000..b54a36c0553 --- /dev/null +++ b/.changeset/real-cars-sell.md @@ -0,0 +1,22 @@ +--- +"thirdweb": minor +--- + +Add SiteLink component for creating wallet-aware links between thirdweb-enabled sites. This component automatically adds wallet connection parameters to the target URL when a wallet is connected, enabling seamless wallet state sharing between sites. + +Example: +```tsx +import { SiteLink } from "thirdweb/react"; + +function App() { + return ( + + Visit thirdweb.com with connected wallet + + ); +} +``` diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index 69f34dac43a..8ba3756c8c4 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -196,5 +196,6 @@ export type { LensProfile, } from "../social/types.js"; -// Site Embed +// Site Embed and Linking export { SiteEmbed } from "../react/web/ui/SiteEmbed.js"; +export { SiteLink } from "../react/web/ui/SiteLink.js"; diff --git a/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx b/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx index 041dc148d48..0327b0cb4df 100644 --- a/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx +++ b/packages/thirdweb/src/react/web/ui/SiteEmbed.tsx @@ -17,14 +17,14 @@ import { useActiveWallet } from "../../core/hooks/wallets/useActiveWallet.js"; * * @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 + * @param {ThirdwebClient} props.client - The current site's thirdweb client + * @param {Ecosystem} [props.ecosystem] - The ecosystem to use for the wallet connection in the embedded site * * @example * ```tsx * import { SiteEmbed } from "thirdweb/react"; * - * + * * ``` */ export function SiteEmbed({ diff --git a/packages/thirdweb/src/react/web/ui/SiteLink.test.tsx b/packages/thirdweb/src/react/web/ui/SiteLink.test.tsx new file mode 100644 index 00000000000..4dea470b0dd --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/SiteLink.test.tsx @@ -0,0 +1,48 @@ +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 { SiteLink } from "./SiteLink.js"; + +describe("SiteLink", () => { + it("renders anchor with correct href", () => { + const testUrl = "https://example.com/"; + const { container } = render( + + Test Link + , + ); + + const anchor = container.querySelector("a"); + expect(anchor).toBeTruthy(); + expect(anchor?.href).toBe(testUrl); + expect(anchor?.textContent).toBe("Test Link"); + }); + + it("throws error if clientId is not provided", () => { + const testUrl = "https://example.com/"; + expect(() => + render( + // biome-ignore lint/suspicious/noExplicitAny: testing invalid input + + Test Link + , + ), + ).toThrow("The SiteLink client must have a clientId"); + }); + + it("adds wallet params to url when wallet is connected", async () => { + const testUrl = "https://example.com/"; + const { container } = render( + + Test Link + , + { + setConnectedWallet: true, + }, + ); + + const anchor = container.querySelector("a"); + expect(anchor).toBeTruthy(); + await waitFor(() => expect(anchor?.href).toContain("walletId=")); + }); +}); diff --git a/packages/thirdweb/src/react/web/ui/SiteLink.tsx b/packages/thirdweb/src/react/web/ui/SiteLink.tsx new file mode 100644 index 00000000000..f41a77989dd --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/SiteLink.tsx @@ -0,0 +1,86 @@ +"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"; + +/** + * Creates a link to another thirdweb-supported site with wallet connection parameters. + * + * @note The target site must support the connected wallet (ecosystem or in-app). + * + * @param {Object} props - The props to pass to the anchor tag + * @param {String} props.href - The URL of the site to link to + * @param {ThirdwebClient} props.client - The current site's thirdweb client + * @param {Ecosystem} [props.ecosystem] - The ecosystem to use for the wallet connection in the target site + * @param {React.ReactNode} props.children - The content to render inside the link + * + * @example + * ```tsx + * import { SiteLink } from "thirdweb/react"; + * + * + * Visit Site + * + * ``` + */ +export function SiteLink({ + href, + client, + ecosystem, + children, + ...props +}: { + href: string; + client: ThirdwebClient; + ecosystem?: Ecosystem; + children: React.ReactNode; +} & Omit, "href">) { + if (!client.clientId) { + throw new Error("The SiteLink client must have a clientId"); + } + + const activeWallet = useActiveWallet(); + const walletId = activeWallet?.id; + + const { + data: { authProvider, authCookie } = {}, + } = useQuery({ + queryKey: ["site-link", walletId, href, 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(href); + if (walletId) { + url.searchParams.set("walletId", walletId); + } + if (authProvider) { + url.searchParams.set("authProvider", authProvider); + } + if (authCookie) { + url.searchParams.set("authCookie", authCookie); + } + + return ( + + {children} + + ); +}