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 f25d1d8 commit b70d69c
Show file tree
Hide file tree
Showing 12 changed files with 393 additions and 108 deletions.
5 changes: 5 additions & 0 deletions .changeset/fluffy-pets-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Fix caching issues for headless component; improve code coverage
14 changes: 13 additions & 1 deletion packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import { getChainMetadata } from "../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useChainContext } from "./provider.js";

Expand Down Expand Up @@ -119,7 +120,18 @@ export function ChainIcon({
}: ChainIconProps) {
const { chain } = useChainContext();
const iconQuery = useQuery({
queryKey: ["_internal_chain_icon_", chain.id] as const,
queryKey: [
"_internal_chain_icon_",
chain.id,
{
resolver:
typeof iconResolver === "string"
? iconResolver
: typeof iconResolver === "function"
? getFunctionId(iconResolver)
: undefined,
},
] as const,
queryFn: async () => {
if (typeof iconResolver === "string") {
return iconResolver;
Expand Down
45 changes: 36 additions & 9 deletions packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ 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 } from "./name.js";
import { ChainName, fetchChainName } from "./name.js";
import { ChainProvider } from "./provider.js";

describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
it("should return the correct chain name, if the name exists in the chain object", () => {
it("should return the correct chain name, if the name exists in the chain object", async () => {
render(
<ChainProvider chain={ethereum}>
<ChainName />
</ChainProvider>,
);
waitFor(() =>
await waitFor(() =>
expect(
screen.getByText("Ethereum", {
exact: true,
Expand All @@ -22,13 +22,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
);
});

it("should return the correct chain name, if the name is loaded from the server", () => {
it("should return the correct chain name, if the name is loaded from the server", async () => {
render(
<ChainProvider chain={defineChain(1)}>
<ChainName />
</ChainProvider>,
);
waitFor(() =>
await waitFor(() =>
expect(
screen.getByText("Ethereum Mainnet", {
exact: true,
Expand All @@ -38,13 +38,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
);
});

it("should return the correct FORMATTED chain name", () => {
it("should return the correct FORMATTED chain name", async () => {
render(
<ChainProvider chain={ethereum}>
<ChainName formatFn={(str: string) => `${str}-formatted`} />
</ChainProvider>,
);
waitFor(() =>
await waitFor(() =>
expect(
screen.getByText("Ethereum-formatted", {
exact: true,
Expand All @@ -54,14 +54,14 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
);
});

it("should fallback properly when fail to resolve chain name", () => {
it("should fallback properly when fail to resolve chain name", async () => {
render(
<ChainProvider chain={defineChain(-1)}>
<ChainName fallbackComponent={<span>oops</span>} />
</ChainProvider>,
);

waitFor(() =>
await waitFor(() =>
expect(
screen.getByText("oops", {
exact: true,
Expand All @@ -70,4 +70,31 @@ describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
).toBeInTheDocument(),
);
});

it("fetchChainName should respect nameResolver as a string", async () => {
const res = await fetchChainName({
chain: ethereum,
nameResolver: "eth_mainnet",
});
expect(res).toBe("eth_mainnet");
});

it("fetchChainName should respect nameResolver as a non-async function", async () => {
const res = await fetchChainName({
chain: ethereum,
nameResolver: () => "eth_mainnet",
});
expect(res).toBe("eth_mainnet");
});

it("fetchChainName should respect nameResolver as an async function", async () => {
const res = await fetchChainName({
chain: ethereum,
nameResolver: async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return "eth_mainnet";
},
});
expect(res).toBe("eth_mainnet");
});
});
48 changes: 35 additions & 13 deletions packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type React from "react";
import type { JSX } from "react";
import type { Chain } from "../../../../../chains/types.js";
import { getChainMetadata } from "../../../../../chains/utils.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
import { useChainContext } from "./provider.js";

/**
Expand Down Expand Up @@ -155,19 +157,19 @@ export function ChainName({
}: ChainNameProps) {
const { chain } = useChainContext();
const nameQuery = useQuery({
queryKey: ["_internal_chain_name_", chain.id] as const,
queryFn: async () => {
if (typeof nameResolver === "string") {
return nameResolver;
}
if (typeof nameResolver === "function") {
return nameResolver();
}
if (chain.name) {
return chain.name;
}
return getChainMetadata(chain).then((data) => data.name);
},
queryKey: [
"_internal_chain_name_",
chain.id,
{
resolver:
typeof nameResolver === "string"
? nameResolver

Check warning on line 166 in packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L166 was not covered by tests
: typeof nameResolver === "function"
? getFunctionId(nameResolver)

Check warning on line 168 in packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L168 was not covered by tests
: undefined,
},
] as const,
queryFn: async () => fetchChainName({ chain, nameResolver }),
...queryOptions,
});

Expand All @@ -183,3 +185,23 @@ export function ChainName({

return <span {...restProps}>{displayValue}</span>;
}

/**
* @internal Exported for tests only
*/
export async function fetchChainName(props: {
chain: Chain;
nameResolver?: string | (() => string) | (() => Promise<string>);
}) {
const { nameResolver, chain } = props;
if (typeof nameResolver === "string") {
return nameResolver;
}
if (typeof nameResolver === "function") {
return nameResolver();
}
if (chain.name) {
return chain.name;
}
return getChainMetadata(chain).then((data) => data.name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
import { useNFTContext } from "./provider.js";
import { getNFTInfo } from "./utils.js";

Expand Down Expand Up @@ -100,7 +101,7 @@ export function NFTDescription({
typeof descriptionResolver === "string"
? descriptionResolver
: typeof descriptionResolver === "function"
? descriptionResolver.toString()
? getFunctionId(descriptionResolver)

Check warning on line 104 in packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx#L104

Added line #L104 was not covered by tests
: undefined,
},
],
Expand Down
3 changes: 2 additions & 1 deletion packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
import { MediaRenderer } from "../../MediaRenderer/MediaRenderer.js";
import type { MediaRendererProps } from "../../MediaRenderer/types.js";
import { useNFTContext } from "./provider.js";
Expand Down Expand Up @@ -138,7 +139,7 @@ export function NFTMedia({
typeof mediaResolver === "object"
? mediaResolver
: typeof mediaResolver === "function"
? mediaResolver.toString()
? getFunctionId(mediaResolver)

Check warning on line 142 in packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/NFT/media.tsx#L142

Added line #L142 was not covered by tests
: undefined,
},
],
Expand Down
3 changes: 2 additions & 1 deletion packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
import { useNFTContext } from "./provider.js";
import { getNFTInfo } from "./utils.js";

Expand Down Expand Up @@ -100,7 +101,7 @@ export function NFTName({
typeof nameResolver === "string"
? nameResolver
: typeof nameResolver === "function"
? nameResolver.toString()
? getFunctionId(nameResolver)

Check warning on line 104 in packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/NFT/name.tsx#L104

Added line #L104 was not covered by tests
: undefined,
},
],
Expand Down
15 changes: 14 additions & 1 deletion packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getChainMetadata } from "../../../../../chains/utils.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { getContract } from "../../../../../contract/contract.js";
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useTokenContext } from "./provider.js";

Expand Down Expand Up @@ -115,7 +116,19 @@ export function TokenIcon({
}: TokenIconProps) {
const { address, client, chain } = useTokenContext();
const iconQuery = useQuery({
queryKey: ["_internal_token_icon_", chain.id, address] as const,
queryKey: [
"_internal_token_icon_",
chain.id,
address,
{
resolver:
typeof iconResolver === "string"
? iconResolver
: typeof iconResolver === "function"
? getFunctionId(iconResolver)
: undefined,
},
] as const,
queryFn: async () => {
if (typeof iconResolver === "string") {
return iconResolver;
Expand Down
86 changes: 86 additions & 0 deletions packages/thirdweb/src/react/web/ui/prebuilt/Token/name.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import {} from "~test/react-render.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import {
UNISWAPV3_FACTORY_CONTRACT,
USDT_CONTRACT,
} from "~test/test-contracts.js";
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { fetchTokenName } from "./name.js";

const client = TEST_CLIENT;

describe.runIf(process.env.TW_SECRET_KEY)("TokenName component", () => {
it("fetchTokenName should respect the nameResolver being a string", async () => {
const res = await fetchTokenName({
address: "thing",
client,
chain: ANVIL_CHAIN,
nameResolver: "tw",
});
expect(res).toBe("tw");
});

it("fetchTokenName should respect the nameResolver being a non-async function", async () => {
const res = await fetchTokenName({
address: "thing",
client,
chain: ANVIL_CHAIN,
nameResolver: () => "tw",
});

expect(res).toBe("tw");
});

it("fetchTokenName should respect the nameResolver being an async function", async () => {
const res = await fetchTokenName({
address: "thing",
client,
chain: ANVIL_CHAIN,
nameResolver: async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return "tw";
},
});

expect(res).toBe("tw");
});

it("fetchTokenName should work for contract with `name` function", async () => {
const res = await fetchTokenName({
address: USDT_CONTRACT.address,
client,
chain: USDT_CONTRACT.chain,
});

expect(res).toBe("Tether USD");
});

it("fetchTokenName should work for native token", async () => {
const res = await fetchTokenName({
address: NATIVE_TOKEN_ADDRESS,
client,
chain: ethereum,
});

expect(res).toBe("Ether");
});

it("fetchTokenName should try to fallback to the contract metadata if fails to resolves from `name()`", async () => {
// todo: find a contract with name in contractMetadata, but does not have a name function
});

it("fetchTokenName should throw in the end where all fallback solutions failed to resolve to any name", async () => {
await expect(() =>
fetchTokenName({
address: UNISWAPV3_FACTORY_CONTRACT.address,
client,
chain: UNISWAPV3_FACTORY_CONTRACT.chain,
}),
).rejects.toThrowError(
"Failed to resolve name from both name() and contract metadata",
);
});
});
Loading

0 comments on commit b70d69c

Please sign in to comment.