Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
kien-ngo committed Dec 11, 2024
1 parent 2fccfc0 commit 9b5b581
Show file tree
Hide file tree
Showing 10 changed files with 447 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/hip-houses-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Add headless components for Wallets: WalletProvider, WalletIcon and WalletName
23 changes: 23 additions & 0 deletions apps/portal/src/app/react/v5/components/onchain/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

<ArticleIconCard
title="WalletProvider"
icon={ReactIcon}
href="/references/typescript/v5/WalletProvider"
description="Component to provide the Wallet context to your app"
/>

<ArticleIconCard
title="WalletIcon"
icon={ReactIcon}
href="/references/typescript/v5/WalletIcon"
description="Component to display the icon of a wallet"
/>

<ArticleIconCard
title="WalletName"
icon={ReactIcon}
href="/references/typescript/v5/WalletName"
description="Component to display the name of a wallet"
/>

</Stack>
14 changes: 14 additions & 0 deletions packages/thirdweb/src/exports/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,17 @@ export {
ChainIcon,
type ChainIconProps,
} from "../react/web/ui/prebuilt/Chain/icon.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";
2 changes: 1 addition & 1 deletion packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface ChainNameProps
* name was not fetched succesfully
* @example
* ```tsx
* <ChainName fallbackComponent={"Failed to load"}
* <ChainName fallbackComponent={<span>Failed to load</span>}
* />
* ```
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const ChainProviderContext = /* @__PURE__ */ createContext<
* ```
* @component
* @chain
* @beta
*/
export function ChainProvider(
props: React.PropsWithChildren<ChainProviderProps>,
Expand Down
120 changes: 120 additions & 0 deletions packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx
Original file line number Diff line number Diff line change
@@ -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<React.ImgHTMLAttributes<HTMLImageElement>, "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
* <WalletIcon loadingComponent={<Spinner />} />
* ```
*/
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
* <WalletIcon fallbackComponent={<span>Failed to load</span>}
* />
* ```
*/
fallbackComponent?: JSX.Element;
/**
* Optional `useQuery` params
*/
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
}

/**
* This component tries to resolve the icon of a given wallet, then return an image.
* @returns an <img /> with the src of the wallet icon
*
* @example
* ### Basic usage
* ```tsx
* import { WalletProvider, WalletIcon } from "thirdweb/react";
*
* <WalletProvider id="io.metamask">
* <WalletIcon />
* </WalletProvider>
* ```
*
* Result: An <img /> component with the src of the icon
* ```html
* <img src="metamask-icon.png" />
* ```
*
* ### Show a loading sign while the icon is being loaded
* ```tsx
* <WalletIcon loadingComponent={<Spinner />} />
* ```
*
* ### Fallback to a dummy image if the wallet icon fails to resolve
* ```tsx
* <WalletIcon fallbackComponent={<img src="blank-image.png" />} />
* ```
*
* ### 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
* <WalletIcon queryOptions={{ enabled: someLogic, retry: 3, }} />
* ```
*
* @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 <img src={imageQuery.data} {...restProps} alt={restProps.alt} />;
}

/**
* @internal
*/
function useWalletIcon(props: {
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
}) {
const { id } = useWalletContext();
const imageQuery = useQuery({
queryKey: ["walletIcon", id],
queryFn: async () => fetchWalletImage({ id }),
...props.queryOptions,
});
return imageQuery;
}

/**
* @internal Exported for tests only
*/
async function fetchWalletImage(props: {
id: WalletId;
}) {
const image_src = await getWalletInfo(props.id, true);
return image_src;
}
13 changes: 13 additions & 0 deletions packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it, vi } from "vitest";
import { fetchWalletName } from "./name.js";

vi.mock("./WalletName", () => ({
useWalletName: vi.fn(),
}));

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");
});
});
144 changes: 144 additions & 0 deletions packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"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";

/**
* Props for the WalletName component
* @component
* @wallet
*/
export interface WalletNameProps
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "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
* <WalletName loadingComponent={<Spinner />} />
* ```
*/
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
* <WalletName fallbackComponent={<span>Failed to load</span>}
* />
* ```
*/
fallbackComponent?: JSX.Element;
/**
* Optional `useQuery` params
*/
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
/**
* A function to format the name's display value
* ```tsx
* <WalletName formatFn={(str: string) => doSomething()} />
* ```
*/
formatFn?: (str: string) => string;
}

/**
* This component fetches then shows the name of a wallet.
* It inherits all the attributes of a HTML <span> component, hence you can style it just like how you would style a normal <span>
*
* @example
* ### Basic usage
* ```tsx
* import { WalletProvider, WalletName } from "thirdweb/react";
*
* <WalletProvider id="io.metamask">
* <WalletName />
* </WalletProvider>
* ```
* Result:
* ```html
* <span>MetaMask</span>
* ```
*
* ### Show a loading sign when the name is being fetched
* ```tsx
* import { WalletProvider, WalletName } from "thirdweb/react";
*
* <WalletProvider {...props}>
* <WalletName loadingComponent={<Spinner />} />
* </WalletProvider>
* ```
*
* ### Fallback to something when the name fails to resolve
* ```tsx
* <WalletProvider {...props}>
* <WalletName fallbackComponent={<span>Failed to load</span>} />
* </WalletProvider>
* ```
*
* ### 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
* <WalletName
* queryOptions={{
* enabled: isEnabled,
* retry: 4,
* }}
* />
* @component
* @beta
* @wallet
*/
export function WalletName({
loadingComponent,
fallbackComponent,
queryOptions,
formatFn,
...restProps
}: WalletNameProps) {
const nameQuery = useWalletName({ queryOptions });
if (nameQuery.isLoading) {
return loadingComponent || null;
}
if (!nameQuery.data) {
return fallbackComponent || null;
}
if (typeof formatFn === "function") {
return <span {...restProps}>{formatFn(nameQuery.data)}</span>;
}
return <span {...restProps}>{nameQuery.data}</span>;
}

/**
* @internal
*/
function useWalletName(props: {
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
}) {
const { id } = useWalletContext();
const nameQuery = useQuery({
queryKey: ["walletName", id],
queryFn: async () => fetchWalletName({ id }),
...props.queryOptions,
});
return nameQuery;
}

/**
* @internal Exported for tests only
*/
export async function fetchWalletName(props: {
id: WalletId;
}) {
const info = await getWalletInfo(props.id);
return info.name;
}
Loading

0 comments on commit 9b5b581

Please sign in to comment.