From 5be197bac97393edf39bf7287b5fa258cf83ec06 Mon Sep 17 00:00:00 2001 From: kien-ngo Date: Sat, 14 Dec 2024 10:26:48 +0000 Subject: [PATCH] [SDK] Headless Wallet components | CNCT-2620 (#5691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CNCT-2620 --- ## PR-Codex overview This PR introduces new headless components for wallet management in the `thirdweb` library, enhancing user interface options for wallet context, icons, and names. ### Detailed summary - Added `WalletProvider`, `WalletIcon`, and `WalletName` components. - Updated `ChainProvider` to mark it as `@beta`. - Introduced utility functions `getQueryKeys` for both chain and wallet components. - Updated documentation and tests for new wallet components. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .changeset/hip-houses-hear.md | 5 + .../app/react/v5/components/onchain/page.mdx | 23 +++ packages/thirdweb/src/exports/react.ts | 14 ++ .../react/web/ui/prebuilt/Chain/name.test.tsx | 20 ++- .../src/react/web/ui/prebuilt/Chain/name.tsx | 32 ++-- .../react/web/ui/prebuilt/Chain/provider.tsx | 1 + .../web/ui/prebuilt/Wallet/icon.test.tsx | 30 ++++ .../src/react/web/ui/prebuilt/Wallet/icon.tsx | 120 +++++++++++++ .../web/ui/prebuilt/Wallet/name.test.tsx | 55 ++++++ .../src/react/web/ui/prebuilt/Wallet/name.tsx | 164 ++++++++++++++++++ .../web/ui/prebuilt/Wallet/provider.test.tsx | 61 +++++++ .../react/web/ui/prebuilt/Wallet/provider.tsx | 65 +++++++ 12 files changed, 576 insertions(+), 14 deletions(-) create mode 100644 .changeset/hip-houses-hear.md create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.test.tsx create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx diff --git a/.changeset/hip-houses-hear.md b/.changeset/hip-houses-hear.md new file mode 100644 index 00000000000..f6b5453b9fb --- /dev/null +++ b/.changeset/hip-houses-hear.md @@ -0,0 +1,5 @@ +--- +"thirdweb": minor +--- + +Add headless components for Wallets: WalletProvider, WalletIcon and WalletName diff --git a/apps/portal/src/app/react/v5/components/onchain/page.mdx b/apps/portal/src/app/react/v5/components/onchain/page.mdx index 17c72de5d27..301490bd514 100644 --- a/apps/portal/src/app/react/v5/components/onchain/page.mdx +++ b/apps/portal/src/app/react/v5/components/onchain/page.mdx @@ -136,4 +136,27 @@ Build your own UI and interact with onchain data using headless components. description="Component to display the name of a chain" /> +### Wallets + + + + + + + diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index 7f1cefba34c..329df064db3 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -273,3 +273,17 @@ export { // Utils export { getLastAuthProvider } from "../react/web/utils/storage.js"; + +// Wallet +export { + WalletProvider, + type WalletProviderProps, +} from "../react/web/ui/prebuilt/Wallet/provider.js"; +export { + WalletIcon, + type WalletIconProps, +} from "../react/web/ui/prebuilt/Wallet/icon.js"; +export { + WalletName, + type WalletNameProps, +} from "../react/web/ui/prebuilt/Wallet/name.js"; diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx index 1279dbceb3f..402a36fb614 100644 --- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx @@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest"; import { render, screen, waitFor } from "~test/react-render.js"; import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js"; import { defineChain } from "../../../../../chains/utils.js"; -import { ChainName, fetchChainName } from "./name.js"; +import { getFunctionId } from "../../../../../utils/function-id.js"; +import { ChainName, fetchChainName, getQueryKeys } from "./name.js"; import { ChainProvider } from "./provider.js"; describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => { @@ -97,4 +98,21 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => { }); expect(res).toBe("eth_mainnet"); }); + + it("getQueryKeys should work without nameResolver", () => { + expect(getQueryKeys({ chainId: 1 })).toStrictEqual([ + "_internal_chain_name_", + 1, + ]); + }); + + it("getQueryKeys should work WITH nameResolver", () => { + const nameResolver = () => "tw"; + const fnId = getFunctionId(nameResolver); + expect(getQueryKeys({ chainId: 1, nameResolver })).toStrictEqual([ + "_internal_chain_name_", + 1, + { resolver: fnId }, + ]); + }); }); diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx index 7cbfa0e6a0f..3cf9c80b83e 100644 --- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx @@ -48,7 +48,7 @@ export interface ChainNameProps * name was not fetched succesfully * @example * ```tsx - * Failed to load} * /> * ``` */ @@ -157,18 +157,7 @@ export function ChainName({ }: ChainNameProps) { const { chain } = useChainContext(); const nameQuery = useQuery({ - queryKey: [ - "_internal_chain_name_", - chain.id, - { - resolver: - typeof nameResolver === "string" - ? nameResolver - : typeof nameResolver === "function" - ? getFunctionId(nameResolver) - : undefined, - }, - ] as const, + queryKey: getQueryKeys({ chainId: chain.id, nameResolver }), queryFn: async () => fetchChainName({ chain, nameResolver }), ...queryOptions, }); @@ -205,3 +194,20 @@ export async function fetchChainName(props: { } return getChainMetadata(chain).then((data) => data.name); } + +/** + * @internal Exported for tests + */ +export function getQueryKeys(props: { + chainId: number; + nameResolver?: string | (() => string) | (() => Promise); +}) { + if (typeof props.nameResolver === "function") { + return [ + "_internal_chain_name_", + props.chainId, + { resolver: getFunctionId(props.nameResolver) }, + ] as const; + } + return ["_internal_chain_name_", props.chainId] as const; +} diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx index 97c21ca52d6..a1079668aa6 100644 --- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx @@ -48,6 +48,7 @@ const ChainProviderContext = /* @__PURE__ */ createContext< * ``` * @component * @chain + * @beta */ export function ChainProvider( props: React.PropsWithChildren, diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx new file mode 100644 index 00000000000..b4c7bc66615 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { render, waitFor } from "~test/react-render.js"; +import { WalletIcon, fetchWalletImage } from "./icon.js"; +import { WalletProvider } from "./provider.js"; + +describe("WalletIcon", () => { + it("should fetch wallet image", async () => { + expect(await fetchWalletImage({ id: "io.metamask" })).toBe( + "data:image/webp;base64,UklGRsgFAABXRUJQVlA4ILwFAAAwHgCdASqAAIAAPm0ylEWkIyIXTGzMQAbEoAzQSPDffG8qhx7LpfeDUfnDee/tP6gP2u9Yf0a/7L0/+pc3nivc0JlTXBvMVYW1Wt7JbdRJ1a5UhpH9+sdhAvGug20WzG+wRD+P/7/raQ1NqZ5EfcOwosQsP7NRVflPNArBLCbFJWE3EvTLNOmMfR2f+r+E2y6y1lWanc3n9RtF4lYbcW38t3UG/W2jXng4loXFOy3Vjm/r/1KHwXlpAuhAIzK/XF+ChayRFhBD+lbRrgQ3CSVjdmXw6agYPoMQ+tpjPUN/3qdGcvnN1sKWt57OGozQVSjMu0aA58MPKVWiAP77l9VfptKhZ7in3+r1DqZZQEYqO1p+MENah/WS/QQDvPuew8P0OXfahcjbCg7h4QEEEjDyfoFUbQHdw+meAsydryxVs/Ij+eB619Uj/sMgtwIifIyTieCT/hdsDmpLqGD+vOIArZfzSkordRkngojxVgvWRht84IaCHCs5Dn208UHFmyQE4KCxHef1iLYeAEPxnIAovGHIl7rCjiYE12obAM3ZCnt+RFqcT3q2rorCswc4/8f8TOhCSo63/dszpMkNQonGwtyhAFnm1EKqLPDcy99ggKQL95VYePoHcv8sHOG5zJ7lircX7VPpxkloNVWnlp/drJjQrp0h5BsOnqYn756+wcFw5qapAOnYCKUHujsoSz2BlLs1702rFpvi1iVczo2aO9GN1TaM3zBqMX0NN0YEU8pz/3+xwkK5M5q+Qb4FNJeugjdOp2kcYoOCbyAg/0OyGswYHPwDN/opVaDHWj0FcKzLq43uEjUZMG8t7O6BkoK0FmpYOeTQrinuF5F6W6ENly78daVmGPYTU22R9No5+xr8F2ESYTaJzOR3UoouY15xHrsxhDCukfebOiHljS6jUjF0TWaIAb58k57DZ3gdjkpxnJDyEmCsCOlASNWMn1ay3G8PDriQOdwsL7bgx/jZXN6XZHVx2hst/6Qcljnnn4um8eD1NcP1rTV7HMZzAb/d7ntki9zU1IFl695zPs3YT+h0PS4JuQYLzYoxpmMUQ35sVx/3KkL4Pm/TlvuNpnopM0+l2b/0OS6+5GwJYEMpzM4peYZS3qZkz/kxWNPTLv3UcCguDqpGxZ39l/VQJhSImKPZBa1RWCtSj29VCzCEZlX/eAdAKgZwhMKR4C5osznEbxjfRdHHPxjZwstCwugFa5w/WykOTFGAzk0sLSuvxO9kw/f++0WvG5La11GQaqbDDJ/ks7bcELqEGtvLHiJgxOXW9PqCk5Ap9EYelPTmdhHZgzWxtz4idN+JX+DGyYtJYjtW+Ay4kJ4Hol+Iavfje1f/22S69z1XSZ8OOcrDHKQdO1JlGaTkKQmCg5Tr05Qy9NQSfh67Vpui6OyMu38BnbG+cfWPRx/MjGQITY8w8sb1GUGd5hD9eVqycVtz8yFlYrXYQSBkAxMYgkBaiWe6kk24SoOCgt74nNs3pyTWUERw9ENESB6PyO9HjcsUZWIh97RKlf6thPtqxdlfr9i3McON9zvI7M2TtYcneXhOnqN8V4wqqII2J3DQ6/DZFNj5eNkCp2ijt7UWuKduhEmbZlLahfxD8eqBHAZR/H5rulzc4oVlyXr6qPXf9LCEuDRSDE8VNFY4NuTcaRTZO33RrWmWzbXAwpEKeH/Xf78XMoeynLBSyB+pS6y8Fqh7ExdmnvtW2gW3pwNrLc5lXlZJW8VcBzaSpy1Dmqj6Xll9BS8RyWqNx0O3fY8NJpf1exbNYMWA8juddzn2d9lHypEbiym0ASxI/jgGkbis5fecZ60QtJusJgdC8HAJEh64A9EuOCAnnISc5CElwpsvJTdEZhYv3t7MtLDvHp20lAtylt5l9yxqfCy/sqC6qZ/8+tsTAHzziGF6NKoaD0FVhZ2CER5AonJdBz5sg0rcD7arFe96uzujFDCQAAAA", + ); + }); + + it("should throw error if WalletId is not supported", async () => { + await expect(() => + // @ts-ignore For test + fetchWalletImage({ id: "__undefined__" }), + ).rejects.toThrowError("Wallet with id __undefined__ not found"); + }); + + it("should render an image", async () => { + const { container } = render( + + + , + ); + await waitFor(() => { + expect(container.querySelector("img")).not.toBe(null); + }); + }); +}); diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx new file mode 100644 index 00000000000..90451b13e7c --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; +import type { JSX } from "react"; +import { getWalletInfo } from "../../../../../wallets/__generated__/getWalletInfo.js"; +import type { WalletId } from "../../../../../wallets/wallet-types.js"; +import { useWalletContext } from "./provider.js"; + +export interface WalletIconProps + extends Omit, "src"> { + /** + * This component will be shown while the icon of the wallet is being fetched + * If not passed, the component will return `null`. + * + * You can/should pass a loading sign or spinner to this prop. + * @example + * ```tsx + * } /> + * ``` + */ + loadingComponent?: JSX.Element; + /** + * This component will be shown if the icon fails to be retreived + * If not passed, the component will return `null`. + * + * You can/should pass a descriptive text/component to this prop, indicating that the + * icon was not fetched succesfully + * @example + * ```tsx + * Failed to load} + * /> + * ``` + */ + fallbackComponent?: JSX.Element; + /** + * Optional `useQuery` params + */ + queryOptions?: Omit, "queryFn" | "queryKey">; +} + +/** + * This component tries to resolve the icon of a given wallet, then return an image. + * @returns an with the src of the wallet icon + * + * @example + * ### Basic usage + * ```tsx + * import { WalletProvider, WalletIcon } from "thirdweb/react"; + * + * + * + * + * ``` + * + * Result: An component with the src of the icon + * ```html + * + * ``` + * + * ### Show a loading sign while the icon is being loaded + * ```tsx + * } /> + * ``` + * + * ### Fallback to a dummy image if the wallet icon fails to resolve + * ```tsx + * } /> + * ``` + * + * ### Usage with queryOptions + * WalletIcon uses useQuery() from tanstack query internally. + * It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic + * ```tsx + * + * ``` + * + * @component + * @wallet + * @beta + */ +export function WalletIcon({ + loadingComponent, + fallbackComponent, + queryOptions, + ...restProps +}: WalletIconProps) { + const imageQuery = useWalletIcon({ queryOptions }); + if (imageQuery.isLoading) { + return loadingComponent || null; + } + if (!imageQuery.data) { + return fallbackComponent || null; + } + return {restProps.alt}; +} + +/** + * @internal + */ +function useWalletIcon(props: { + queryOptions?: Omit, "queryFn" | "queryKey">; +}) { + const { id } = useWalletContext(); + const imageQuery = useQuery({ + queryKey: ["walletIcon", id], + queryFn: async () => fetchWalletImage({ id }), + ...props.queryOptions, + }); + return imageQuery; +} + +/** + * @internal Exported for tests only + */ +export async function fetchWalletImage(props: { + id: WalletId; +}) { + const image_src = await getWalletInfo(props.id, true); + return image_src; +} diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx new file mode 100644 index 00000000000..c8d50206015 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { render, waitFor } from "~test/react-render.js"; +import { getFunctionId } from "../../../../../utils/function-id.js"; +import { WalletName, fetchWalletName, getQueryKeys } from "./name.js"; +import { WalletProvider } from "./provider.js"; + +describe.runIf(process.env.TW_SECRET_KEY)("WalletName", () => { + it("fetchWalletName: should fetch wallet name from id", async () => { + const name = await fetchWalletName({ id: "io.metamask" }); + expect(name).toBe("MetaMask"); + }); + + it("fetchWalletName should throw error if failed to get name", async () => { + // @ts-ignore for test + await expect(() => fetchWalletName({ id: "test___" })).rejects.toThrowError( + "Wallet with id test___ not found", + ); + }); + + it("fetchWalletName should work with formatFn", async () => { + const formatFn = (str: string) => `${str} Wallet`; + expect(await fetchWalletName({ id: "io.metamask", formatFn })).toBe( + "MetaMask Wallet", + ); + }); + + it("getQueryKeys should work without a formatFn", () => { + expect(getQueryKeys({ id: "ai.hacken" })).toStrictEqual([ + "walletName", + "ai.hacken", + ]); + }); + + it("getQueryKeys should work WITH a formatFn", () => { + const fn = (str: string) => `test:${str}`; + const fnId = getFunctionId(fn); + expect(getQueryKeys({ id: "ai.hacken", formatFn: fn })).toStrictEqual([ + "walletName", + "ai.hacken", + { resolver: fnId }, + ]); + }); + + it("should render a span", async () => { + const { container } = render( + + + , + ); + + await waitFor(() => { + expect(container.querySelector("span")).not.toBe(null); + }); + }); +}); diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx new file mode 100644 index 00000000000..92a8fac34d6 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; +import type { JSX } from "react"; +import { getFunctionId } from "../../../../../utils/function-id.js"; +import { getWalletInfo } from "../../../../../wallets/__generated__/getWalletInfo.js"; +import type { WalletId } from "../../../../../wallets/wallet-types.js"; +import { useWalletContext } from "./provider.js"; + +/** + * Props for the WalletName component + * @component + * @wallet + */ +export interface WalletNameProps + extends Omit, "children"> { + /** + * This component will be shown while the name of the wallet name is being fetched + * If not passed, the component will return `null`. + * + * You can/should pass a loading sign or spinner to this prop. + * @example + * ```tsx + * } /> + * ``` + */ + loadingComponent?: JSX.Element; + /** + * This component will be shown if the name fails to be retreived + * If not passed, the component will return `null`. + * + * You can/should pass a descriptive text/component to this prop, indicating that the + * name was not fetched succesfully + * @example + * ```tsx + * Failed to load} + * /> + * ``` + */ + fallbackComponent?: JSX.Element; + /** + * Optional `useQuery` params + */ + queryOptions?: Omit, "queryFn" | "queryKey">; + /** + * A function to format the name's display value + * ```tsx + * doSomething()} /> + * ``` + */ + formatFn?: (str: string) => string; +} + +/** + * This component fetches then shows the name of a wallet. + * It inherits all the attributes of a HTML component, hence you can style it just like how you would style a normal + * + * @example + * ### Basic usage + * ```tsx + * import { WalletProvider, WalletName } from "thirdweb/react"; + * + * + * + * + * ``` + * Result: + * ```html + * MetaMask + * ``` + * + * ### Show a loading sign when the name is being fetched + * ```tsx + * import { WalletProvider, WalletName } from "thirdweb/react"; + * + * + * } /> + * + * ``` + * + * ### Fallback to something when the name fails to resolve + * ```tsx + * + * Failed to load} /> + * + * ``` + * + * ### Custom query options for useQuery + * This component uses `@tanstack-query`'s useQuery internally. + * You can use the `queryOptions` prop for more fine-grained control + * ```tsx + * + * @component + * @beta + * @wallet + */ +export function WalletName({ + loadingComponent, + fallbackComponent, + queryOptions, + formatFn, + ...restProps +}: WalletNameProps) { + const nameQuery = useWalletName({ queryOptions, formatFn }); + if (nameQuery.isLoading) { + return loadingComponent || null; + } + if (!nameQuery.data) { + return fallbackComponent || null; + } + return {nameQuery.data}; +} + +/** + * @internal + */ +function useWalletName(props: { + formatFn?: (str: string) => string; + queryOptions?: Omit, "queryFn" | "queryKey">; +}) { + const { id } = useWalletContext(); + const nameQuery = useQuery({ + queryKey: getQueryKeys({ id, formatFn: props.formatFn }), + queryFn: async () => fetchWalletName({ id, formatFn: props.formatFn }), + ...props.queryOptions, + }); + return nameQuery; +} + +/** + * @internal Exported for tests only + */ +export function getQueryKeys(props: { + id: WalletId; + formatFn?: (str: string) => string; +}) { + if (typeof props.formatFn === "function") { + return [ + "walletName", + props.id, + { resolver: getFunctionId(props.formatFn) }, + ] as const; + } + return ["walletName", props.id] as const; +} + +/** + * @internal Exported for tests only + */ +export async function fetchWalletName(props: { + id: WalletId; + formatFn?: (str: string) => string; +}) { + const info = await getWalletInfo(props.id); + if (typeof props.formatFn === "function") { + return props.formatFn(info.name); + } + return info.name; +} diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.test.tsx new file mode 100644 index 00000000000..db97a5ce152 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.test.tsx @@ -0,0 +1,61 @@ +import { type FC, useContext } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { render, renderHook, screen } from "~test/react-render.js"; +import { + WalletProvider, + WalletProviderContext, + useWalletContext, +} from "./provider.js"; + +describe.runIf(process.env.TW_SECRET_KEY)("WalletProvider", () => { + it("useWalletContext should throw an error when used outside of WalletProvider", () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + + expect(() => { + renderHook(() => useWalletContext()); + }).toThrow( + "WalletProviderContext not found. Make sure you are using WalletIcon, WalletName, etc. inside a component", + ); + + consoleErrorSpy.mockRestore(); + }); + + it("useWalletContext should return the context value when used within WalletProvider", () => { + const wrapper: FC = ({ children }: React.PropsWithChildren) => ( + {children} + ); + + const { result } = renderHook(() => useWalletContext(), { wrapper }); + + expect(result.current.id).toStrictEqual("io.metamask"); + }); + + it("should render children correctly", () => { + render( + +
Child Component
+
, + ); + + expect(screen.getByText("Child Component")).toBeInTheDocument(); + }); + + it("should provide context values to children", () => { + function WalletConsumer() { + const context = useContext(WalletProviderContext); + if (!context) { + return
No context
; + } + return
{String(context.id)}
; + } + render( + + + , + ); + + expect(screen.getByText("io.metamask")).toBeInTheDocument(); + }); +}); diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx new file mode 100644 index 00000000000..97d91fde1c7 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx @@ -0,0 +1,65 @@ +"use client"; + +import type React from "react"; +import { createContext, useContext } from "react"; +import type { WalletId } from "../../../../../wallets/wallet-types.js"; + +/** + * Props for the WalletProvider component + * @component + * @wallet + */ +export type WalletProviderProps = { + id: WalletId; +}; + +/** + * @internal Exported for tests only + */ +export const WalletProviderContext = /* @__PURE__ */ createContext< + WalletProviderProps | undefined +>(undefined); + +/** +/** + * A React context provider component that supplies Wallet-related data to its child components. + * + * This component serves as a wrapper around the `WalletProviderContext.Provider` and passes + * the provided wallet data down to all of its child components through the context API. + * + * @example + * ### Basic usage + * ```tsx + * import { WalletProvider, WalletIcon, WalletName } from "thirdweb/react"; + * + * + * + * + * + * ``` + * @beta + * @component + * @wallet + */ +export function WalletProvider( + props: React.PropsWithChildren, +) { + return ( + + {props.children} + + ); +} + +/** + * @internal + */ +export function useWalletContext() { + const ctx = useContext(WalletProviderContext); + if (!ctx) { + throw new Error( + "WalletProviderContext not found. Make sure you are using WalletIcon, WalletName, etc. inside a component", + ); + } + return ctx; +}