Skip to content

Commit 1250074

Browse files
committed
update
1 parent ce0da51 commit 1250074

File tree

10 files changed

+447
-1
lines changed

10 files changed

+447
-1
lines changed

.changeset/hip-houses-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Add headless components for Wallets: WalletProvider, WalletIcon and WalletName

apps/portal/src/app/react/v5/components/onchain/page.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,27 @@ Build your own UI and interact with onchain data using headless components.
136136
description="Component to display the name of a chain"
137137
/>
138138

139+
### Wallets
140+
141+
<ArticleIconCard
142+
title="WalletProvider"
143+
icon={ReactIcon}
144+
href="/references/typescript/v5/WalletProvider"
145+
description="Component to provide the Wallet context to your app"
146+
/>
147+
148+
<ArticleIconCard
149+
title="WalletIcon"
150+
icon={ReactIcon}
151+
href="/references/typescript/v5/WalletIcon"
152+
description="Component to display the icon of a wallet"
153+
/>
154+
155+
<ArticleIconCard
156+
title="WalletName"
157+
icon={ReactIcon}
158+
href="/references/typescript/v5/WalletName"
159+
description="Component to display the name of a wallet"
160+
/>
161+
139162
</Stack>

packages/thirdweb/src/exports/react.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,17 @@ export {
272272

273273
// Utils
274274
export { getLastAuthProvider } from "../react/web/utils/storage.js";
275+
276+
// Wallet
277+
export {
278+
WalletProvider,
279+
type WalletProviderProps,
280+
} from "../react/web/ui/prebuilt/Wallet/provider.js";
281+
export {
282+
WalletIcon,
283+
type WalletIconProps,
284+
} from "../react/web/ui/prebuilt/Wallet/icon.js";
285+
export {
286+
WalletName,
287+
type WalletNameProps,
288+
} from "../react/web/ui/prebuilt/Wallet/name.js";

packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export interface ChainNameProps
4848
* name was not fetched succesfully
4949
* @example
5050
* ```tsx
51-
* <ChainName fallbackComponent={"Failed to load"}
51+
* <ChainName fallbackComponent={<span>Failed to load</span>}
5252
* />
5353
* ```
5454
*/

packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const ChainProviderContext = /* @__PURE__ */ createContext<
4848
* ```
4949
* @component
5050
* @chain
51+
* @beta
5152
*/
5253
export function ChainProvider(
5354
props: React.PropsWithChildren<ChainProviderProps>,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"use client";
2+
3+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
4+
import type { JSX } from "react";
5+
import { getWalletInfo } from "../../../../../wallets/__generated__/getWalletInfo.js";
6+
import type { WalletId } from "../../../../../wallets/wallet-types.js";
7+
import { useWalletContext } from "./provider.js";
8+
9+
export interface WalletIconProps
10+
extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> {
11+
/**
12+
* This component will be shown while the icon of the wallet is being fetched
13+
* If not passed, the component will return `null`.
14+
*
15+
* You can/should pass a loading sign or spinner to this prop.
16+
* @example
17+
* ```tsx
18+
* <WalletIcon loadingComponent={<Spinner />} />
19+
* ```
20+
*/
21+
loadingComponent?: JSX.Element;
22+
/**
23+
* This component will be shown if the icon fails to be retreived
24+
* If not passed, the component will return `null`.
25+
*
26+
* You can/should pass a descriptive text/component to this prop, indicating that the
27+
* icon was not fetched succesfully
28+
* @example
29+
* ```tsx
30+
* <WalletIcon fallbackComponent={<span>Failed to load</span>}
31+
* />
32+
* ```
33+
*/
34+
fallbackComponent?: JSX.Element;
35+
/**
36+
* Optional `useQuery` params
37+
*/
38+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
39+
}
40+
41+
/**
42+
* This component tries to resolve the icon of a given wallet, then return an image.
43+
* @returns an <img /> with the src of the wallet icon
44+
*
45+
* @example
46+
* ### Basic usage
47+
* ```tsx
48+
* import { WalletProvider, WalletIcon } from "thirdweb/react";
49+
*
50+
* <WalletProvider id="io.metamask">
51+
* <WalletIcon />
52+
* </WalletProvider>
53+
* ```
54+
*
55+
* Result: An <img /> component with the src of the icon
56+
* ```html
57+
* <img src="metamask-icon.png" />
58+
* ```
59+
*
60+
* ### Show a loading sign while the icon is being loaded
61+
* ```tsx
62+
* <WalletIcon loadingComponent={<Spinner />} />
63+
* ```
64+
*
65+
* ### Fallback to a dummy image if the wallet icon fails to resolve
66+
* ```tsx
67+
* <WalletIcon fallbackComponent={<img src="blank-image.png" />} />
68+
* ```
69+
*
70+
* ### Usage with queryOptions
71+
* WalletIcon uses useQuery() from tanstack query internally.
72+
* It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
73+
* ```tsx
74+
* <WalletIcon queryOptions={{ enabled: someLogic, retry: 3, }} />
75+
* ```
76+
*
77+
* @component
78+
* @wallet
79+
* @beta
80+
*/
81+
export function WalletIcon({
82+
loadingComponent,
83+
fallbackComponent,
84+
queryOptions,
85+
...restProps
86+
}: WalletIconProps) {
87+
const imageQuery = useWalletIcon({ queryOptions });
88+
if (imageQuery.isLoading) {
89+
return loadingComponent || null;
90+
}
91+
if (!imageQuery.data) {
92+
return fallbackComponent || null;
93+
}
94+
return <img src={imageQuery.data} {...restProps} alt={restProps.alt} />;
95+
}
96+
97+
/**
98+
* @internal
99+
*/
100+
function useWalletIcon(props: {
101+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
102+
}) {
103+
const { id } = useWalletContext();
104+
const imageQuery = useQuery({
105+
queryKey: ["walletIcon", id],
106+
queryFn: async () => fetchWalletImage({ id }),
107+
...props.queryOptions,
108+
});
109+
return imageQuery;
110+
}
111+
112+
/**
113+
* @internal Exported for tests only
114+
*/
115+
async function fetchWalletImage(props: {
116+
id: WalletId;
117+
}) {
118+
const image_src = await getWalletInfo(props.id, true);
119+
return image_src;
120+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { fetchWalletName } from "./name.js";
3+
4+
vi.mock("./WalletName", () => ({
5+
useWalletName: vi.fn(),
6+
}));
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("WalletName", () => {
9+
it("fetchWalletName: should fetch wallet name from id", async () => {
10+
const name = await fetchWalletName({ id: "io.metamask" });
11+
expect(name).toBe("MetaMask");
12+
});
13+
});
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"use client";
2+
3+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
4+
import type { JSX } from "react";
5+
import { getWalletInfo } from "../../../../../wallets/__generated__/getWalletInfo.js";
6+
import type { WalletId } from "../../../../../wallets/wallet-types.js";
7+
import { useWalletContext } from "./provider.js";
8+
9+
/**
10+
* Props for the WalletName component
11+
* @component
12+
* @wallet
13+
*/
14+
export interface WalletNameProps
15+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
16+
/**
17+
* This component will be shown while the name of the wallet name is being fetched
18+
* If not passed, the component will return `null`.
19+
*
20+
* You can/should pass a loading sign or spinner to this prop.
21+
* @example
22+
* ```tsx
23+
* <WalletName loadingComponent={<Spinner />} />
24+
* ```
25+
*/
26+
loadingComponent?: JSX.Element;
27+
/**
28+
* This component will be shown if the name fails to be retreived
29+
* If not passed, the component will return `null`.
30+
*
31+
* You can/should pass a descriptive text/component to this prop, indicating that the
32+
* name was not fetched succesfully
33+
* @example
34+
* ```tsx
35+
* <WalletName fallbackComponent={<span>Failed to load</span>}
36+
* />
37+
* ```
38+
*/
39+
fallbackComponent?: JSX.Element;
40+
/**
41+
* Optional `useQuery` params
42+
*/
43+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
44+
/**
45+
* A function to format the name's display value
46+
* ```tsx
47+
* <WalletName formatFn={(str: string) => doSomething()} />
48+
* ```
49+
*/
50+
formatFn?: (str: string) => string;
51+
}
52+
53+
/**
54+
* This component fetches then shows the name of a wallet.
55+
* It inherits all the attributes of a HTML <span> component, hence you can style it just like how you would style a normal <span>
56+
*
57+
* @example
58+
* ### Basic usage
59+
* ```tsx
60+
* import { WalletProvider, WalletName } from "thirdweb/react";
61+
*
62+
* <WalletProvider id="io.metamask">
63+
* <WalletName />
64+
* </WalletProvider>
65+
* ```
66+
* Result:
67+
* ```html
68+
* <span>MetaMask</span>
69+
* ```
70+
*
71+
* ### Show a loading sign when the name is being fetched
72+
* ```tsx
73+
* import { WalletProvider, WalletName } from "thirdweb/react";
74+
*
75+
* <WalletProvider {...props}>
76+
* <WalletName loadingComponent={<Spinner />} />
77+
* </WalletProvider>
78+
* ```
79+
*
80+
* ### Fallback to something when the name fails to resolve
81+
* ```tsx
82+
* <WalletProvider {...props}>
83+
* <WalletName fallbackComponent={<span>Failed to load</span>} />
84+
* </WalletProvider>
85+
* ```
86+
*
87+
* ### Custom query options for useQuery
88+
* This component uses `@tanstack-query`'s useQuery internally.
89+
* You can use the `queryOptions` prop for more fine-grained control
90+
* ```tsx
91+
* <WalletName
92+
* queryOptions={{
93+
* enabled: isEnabled,
94+
* retry: 4,
95+
* }}
96+
* />
97+
* @component
98+
* @beta
99+
* @wallet
100+
*/
101+
export function WalletName({
102+
loadingComponent,
103+
fallbackComponent,
104+
queryOptions,
105+
formatFn,
106+
...restProps
107+
}: WalletNameProps) {
108+
const nameQuery = useWalletName({ queryOptions });
109+
if (nameQuery.isLoading) {
110+
return loadingComponent || null;
111+
}
112+
if (!nameQuery.data) {
113+
return fallbackComponent || null;
114+
}
115+
if (typeof formatFn === "function") {
116+
return <span {...restProps}>{formatFn(nameQuery.data)}</span>;
117+
}
118+
return <span {...restProps}>{nameQuery.data}</span>;
119+
}
120+
121+
/**
122+
* @internal
123+
*/
124+
function useWalletName(props: {
125+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
126+
}) {
127+
const { id } = useWalletContext();
128+
const nameQuery = useQuery({
129+
queryKey: ["walletName", id],
130+
queryFn: async () => fetchWalletName({ id }),
131+
...props.queryOptions,
132+
});
133+
return nameQuery;
134+
}
135+
136+
/**
137+
* @internal Exported for tests only
138+
*/
139+
export async function fetchWalletName(props: {
140+
id: WalletId;
141+
}) {
142+
const info = await getWalletInfo(props.id);
143+
return info.name;
144+
}

0 commit comments

Comments
 (0)