Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6994310d16 | ||
|
|
e42ffc6a93 | ||
|
|
0ddc123807 | ||
|
|
f91f6310e9 | ||
|
|
dc1e07b5e4 | ||
|
|
2b5080d901 | ||
|
|
5be197bac9 | ||
|
|
34165cb608 | ||
|
|
9954b37b01 | ||
|
|
9b58a086b6 | ||
|
|
22bc557090 | ||
|
|
80b54a937c | ||
|
|
be6433dcf2 | ||
|
|
f6c040719c | ||
|
|
2db85824f0 | ||
|
|
498a26ebbc | ||
|
|
8761fe6102 | ||
|
|
77c8d0eff0 |
@@ -2,7 +2,7 @@ name: Auto Author Assign
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, reopened ]
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -10,5 +10,9 @@ permissions:
|
||||
jobs:
|
||||
assign-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.pull_request.author_association == 'MEMBER' ||
|
||||
github.event.pull_request.author_association == 'OWNER' ||
|
||||
github.event.pull_request.author_association == 'COLLABORATOR'
|
||||
steps:
|
||||
- uses: toshimaru/[email protected]
|
||||
|
||||
@@ -2,10 +2,10 @@ name: Linked Issue
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
types: [opened, edited, ready_for_review]
|
||||
|
||||
env:
|
||||
VALID_ISSUE_PREFIXES: "CNCT|DASH|PROT|INSIGHT"
|
||||
VALID_ISSUE_PREFIXES: "CNCT|DASH|PROT|INSIGHT|ENGINE|CS|DES|BIL|DEVX|SOLU|NEB"
|
||||
|
||||
jobs:
|
||||
linear:
|
||||
@@ -22,6 +22,17 @@ jobs:
|
||||
pull_number: context.issue.number
|
||||
});
|
||||
|
||||
// Check if contributor is external
|
||||
const isInternalContributor = ['MEMBER', 'OWNER', 'COLLABORATOR'].includes(
|
||||
context.payload.pull_request.author_association
|
||||
);
|
||||
|
||||
// Automatically pass for external contributors
|
||||
if (!isInternalContributor) {
|
||||
console.log('External contributor detected - automatically passing check');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = pr.data.body || '';
|
||||
const branchName = pr.data.head.ref;
|
||||
const issueRegex = new RegExp(`(${process.env.VALID_ISSUE_PREFIXES})-\\d+`, 'i');
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: TypeDoc
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment
|
||||
concurrency:
|
||||
group: "typedoc"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Generate TypeDoc"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Run TypeDoc
|
||||
run: pnpm typedoc
|
||||
|
||||
- name: Update Gist
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GIST_TOKEN }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./packages/thirdweb/typedoc/parsed.json', 'utf8');
|
||||
const gistId = '678fe1f331a01270bb002fee660f285d';
|
||||
|
||||
await github.rest.gists.update({
|
||||
gist_id: gistId,
|
||||
files: {
|
||||
'data.json': {
|
||||
content: content
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Permalink: https://gist.githubusercontent.com/raw/${gistId}/data.json`);
|
||||
Vendored
+1
-1
@@ -3,4 +3,4 @@
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbo",
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
|
||||
"start": "next start",
|
||||
"format": "biome format ./src --write",
|
||||
|
||||
@@ -14,6 +14,7 @@ export function CodeBlockContainer(props: {
|
||||
scrollableContainerClassName?: string;
|
||||
copyButtonClassName?: string;
|
||||
shadowColor?: string;
|
||||
onCopy?: (code: string) => void;
|
||||
}) {
|
||||
const { hasCopied, onCopy } = useClipboard(props.codeToCopy);
|
||||
|
||||
@@ -38,7 +39,10 @@ export function CodeBlockContainer(props: {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onCopy}
|
||||
onClick={() => {
|
||||
onCopy();
|
||||
props.onCopy?.(props.codeToCopy);
|
||||
}}
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3.5 h-auto bg-background p-2",
|
||||
props.copyButtonClassName,
|
||||
|
||||
@@ -8,6 +8,7 @@ export function RenderCode(props: {
|
||||
copyButtonClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
onCopy?: (code: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<CodeBlockContainer
|
||||
@@ -17,6 +18,7 @@ export function RenderCode(props: {
|
||||
scrollableClassName={props.scrollableClassName}
|
||||
scrollableContainerClassName={props.scrollableContainerClassName}
|
||||
shadowColor={props.shadowColor}
|
||||
onCopy={props.onCopy}
|
||||
>
|
||||
<div
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: we know what we're doing here
|
||||
|
||||
@@ -15,6 +15,7 @@ export type CodeProps = {
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
onCopy?: (code: string) => void;
|
||||
};
|
||||
|
||||
export const CodeClient: React.FC<CodeProps> = ({
|
||||
@@ -27,6 +28,7 @@ export const CodeClient: React.FC<CodeProps> = ({
|
||||
ignoreFormattingErrors,
|
||||
scrollableContainerClassName,
|
||||
shadowColor,
|
||||
onCopy,
|
||||
}) => {
|
||||
const codeQuery = useQuery({
|
||||
queryKey: ["html", code],
|
||||
@@ -49,6 +51,7 @@ export const CodeClient: React.FC<CodeProps> = ({
|
||||
copyButtonClassName={copyButtonClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
shadowColor={shadowColor}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -62,6 +65,7 @@ export const CodeClient: React.FC<CodeProps> = ({
|
||||
copyButtonClassName={copyButtonClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
shadowColor={shadowColor}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export function PlainTextCodeBlock(props: {
|
||||
codeClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
onCopy?: (code: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<CodeBlockContainer
|
||||
@@ -18,6 +19,7 @@ export function PlainTextCodeBlock(props: {
|
||||
scrollableClassName={props.scrollableClassName}
|
||||
scrollableContainerClassName={props.scrollableContainerClassName}
|
||||
shadowColor={props.shadowColor}
|
||||
onCopy={props.onCopy}
|
||||
>
|
||||
<code className={cn("block whitespace-pre", props.codeClassName)}>
|
||||
{props.code}
|
||||
|
||||
@@ -48,12 +48,7 @@ export const CustomConnectWallet = (props: {
|
||||
// chains
|
||||
const favChainIdsQuery = useFavoriteChainIds();
|
||||
const recentChainIds = useStore(recentlyUsedChainIdsStore);
|
||||
const { idToChain, allChains } = useAllChainsData();
|
||||
|
||||
const allChainsWithMetadata = useMemo(
|
||||
() => allChains.map(mapV4ChainToV5Chain),
|
||||
[allChains],
|
||||
);
|
||||
const { idToChain, allChainsV5 } = useAllChainsData();
|
||||
|
||||
const recentlyUsedChainsWithMetadata = useMemo(
|
||||
() =>
|
||||
@@ -170,7 +165,7 @@ export const CustomConnectWallet = (props: {
|
||||
detailsButton={{
|
||||
className: props.detailsButtonClassName,
|
||||
}}
|
||||
chains={allChainsWithMetadata}
|
||||
chains={allChainsV5}
|
||||
detailsModal={{
|
||||
networkSelector: {
|
||||
sections: chainSections,
|
||||
|
||||
@@ -14,7 +14,7 @@ export function ProfileUI(props: {
|
||||
|
||||
return (
|
||||
<div className="container pt-8 pb-20">
|
||||
<ProfileHeader profileAddress={profileAddress} />
|
||||
<ProfileHeader profileAddress={profileAddress} ensName={ensName} />
|
||||
<div className="h-8" />
|
||||
|
||||
<div>
|
||||
|
||||
+20
-12
@@ -12,7 +12,10 @@ import {
|
||||
} from "thirdweb/react";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
|
||||
export function ProfileHeader(props: { profileAddress: string }) {
|
||||
export function ProfileHeader(props: {
|
||||
profileAddress: string;
|
||||
ensName: string | undefined;
|
||||
}) {
|
||||
const client = useThirdwebClient();
|
||||
return (
|
||||
<AccountProvider address={props.profileAddress} client={client}>
|
||||
@@ -27,17 +30,22 @@ export function ProfileHeader(props: { profileAddress: string }) {
|
||||
/>
|
||||
<div>
|
||||
<h1 className="font-semibold text-4xl tracking-tight">
|
||||
<AccountName
|
||||
fallbackComponent={
|
||||
<AccountAddress
|
||||
formatFn={(addr) =>
|
||||
shortenIfAddress(replaceDeployerAddress(addr))
|
||||
}
|
||||
/>
|
||||
}
|
||||
loadingComponent={<Skeleton className="h-8 w-40" />}
|
||||
formatFn={(name) => replaceDeployerAddress(name)}
|
||||
/>
|
||||
{/* if we already have an ensName just use it */}
|
||||
{props.ensName ? (
|
||||
props.ensName
|
||||
) : (
|
||||
<AccountName
|
||||
fallbackComponent={
|
||||
<AccountAddress
|
||||
formatFn={(addr) =>
|
||||
shortenIfAddress(replaceDeployerAddress(addr))
|
||||
}
|
||||
/>
|
||||
}
|
||||
loadingComponent={<Skeleton className="h-8 w-40" />}
|
||||
formatFn={(name) => replaceDeployerAddress(name)}
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default async function Page(props: PageProps) {
|
||||
|
||||
return (
|
||||
<ProfileUI
|
||||
ensName={resolvedInfo.ensName}
|
||||
ensName={replaceDeployerAddress(resolvedInfo.ensName || "")}
|
||||
profileAddress={resolvedInfo.address}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ExecuteConfig } from "./types";
|
||||
export type ContextFilters = {
|
||||
chainIds?: string[];
|
||||
contractAddresses?: string[];
|
||||
walletAddresses?: string[];
|
||||
};
|
||||
|
||||
export async function promptNebula(params: {
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function createSession(params: {
|
||||
body.context_filter = {
|
||||
chain_ids: params.contextFilters.chainIds || [],
|
||||
contract_addresses: params.contextFilters.contractAddresses || [],
|
||||
wallet_addresses: params.contextFilters.walletAddresses || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +63,7 @@ export async function updateSession(params: {
|
||||
body.context_filter = {
|
||||
chain_ids: params.contextFilters.chainIds || [],
|
||||
contract_addresses: params.contextFilters.contractAddresses || [],
|
||||
wallet_addresses: params.contextFilters.walletAddresses || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ type ClientConfig = {
|
||||
|
||||
export type ExecuteConfig = EngineConfig | SessionKeyConfig | ClientConfig;
|
||||
|
||||
type SessionContextFilter = {
|
||||
chain_ids: string[] | null;
|
||||
contract_addresses: string[] | null;
|
||||
wallet_addresses: string[] | null;
|
||||
};
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string;
|
||||
account_id: string;
|
||||
@@ -37,10 +43,7 @@ export type SessionInfo = {
|
||||
archived_at: string | null;
|
||||
title: string | null;
|
||||
is_public: boolean | null;
|
||||
context_filter: {
|
||||
chain_ids: string[];
|
||||
contract_addresses: string[];
|
||||
} | null;
|
||||
context_filter: SessionContextFilter | null;
|
||||
// memory
|
||||
// action: array<object> | null; <-- type of this is not available on https://nebula-api.thirdweb-dev.com/docs#/default/get_session_session__session_id__get
|
||||
};
|
||||
@@ -50,10 +53,7 @@ export type UpdatedSessionInfo = {
|
||||
modal_name: string;
|
||||
account_id: string;
|
||||
execute_config: ExecuteConfig | null;
|
||||
context_filter: {
|
||||
chain_ids: string[];
|
||||
contract_addresses: string[];
|
||||
} | null;
|
||||
context_filter: SessionContextFilter | null;
|
||||
};
|
||||
|
||||
export type DeletedSessionInfo = {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { type ContextFilters, promptNebula } from "../api/chat";
|
||||
import { createSession, updateSession } from "../api/session";
|
||||
import type { ExecuteConfig, SessionInfo } from "../api/types";
|
||||
@@ -22,6 +23,7 @@ export function ChatPageContent(props: {
|
||||
type: "landing" | "new-chat";
|
||||
account: Account;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const client = useThirdwebClient();
|
||||
const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
|
||||
const [messages, setMessages] = useState<Array<ChatMessage>>(() => {
|
||||
@@ -36,17 +38,47 @@ export function ChatPageContent(props: {
|
||||
});
|
||||
|
||||
const [_config, setConfig] = useState<ExecuteConfig | null>();
|
||||
const [contextFilters, setContextFilters] = useState<
|
||||
const [hasUserUpdatedContextFilters, setHasUserUpdatedContextFilters] =
|
||||
useState(false);
|
||||
|
||||
const [_contextFilters, _setContextFilters] = useState<
|
||||
ContextFilters | undefined
|
||||
>(() => {
|
||||
const contextFilterRes = props.session?.context_filter;
|
||||
if (contextFilterRes) {
|
||||
const value: ContextFilters = {
|
||||
chainIds: contextFilterRes?.chain_ids || undefined,
|
||||
contractAddresses: contextFilterRes?.contract_addresses || undefined,
|
||||
walletAddresses: contextFilterRes?.wallet_addresses || undefined,
|
||||
};
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
function setContextFilters(filters: ContextFilters | undefined) {
|
||||
_setContextFilters(filters);
|
||||
setHasUserUpdatedContextFilters(true);
|
||||
}
|
||||
|
||||
const isNewSession = !props.session;
|
||||
|
||||
// if this is a new session, user has not manually updated context filters
|
||||
// and no wallet address is set in context filters, add the current wallet address
|
||||
const contextFilters = useMemo(() => {
|
||||
if (
|
||||
isNewSession &&
|
||||
!hasUserUpdatedContextFilters &&
|
||||
address &&
|
||||
(!_contextFilters?.walletAddresses ||
|
||||
_contextFilters.walletAddresses.length === 0)
|
||||
) {
|
||||
return {
|
||||
chainIds: contextFilterRes.chain_ids,
|
||||
contractAddresses: contextFilterRes.contract_addresses,
|
||||
..._contextFilters,
|
||||
walletAddresses: [address],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return _contextFilters;
|
||||
}, [_contextFilters, address, isNewSession, hasUserUpdatedContextFilters]);
|
||||
|
||||
const config = _config || {
|
||||
mode: "client",
|
||||
@@ -293,7 +325,7 @@ export function ChatPageContent(props: {
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col overflow-hidden">
|
||||
<header className="flex justify-end border-b bg-background p-4">
|
||||
<header className="flex justify-start border-b bg-background p-4">
|
||||
<ContextFiltersButton
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { ChevronRightIcon, MessageSquareDashedIcon } from "lucide-react";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
FlaskConicalIcon,
|
||||
MessageSquareDashedIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { TruncatedSessionInfo } from "../api/types";
|
||||
import { useNewChatPageLink } from "../hooks/useNewChatPageLink";
|
||||
@@ -23,10 +28,15 @@ export function ChatSidebar(props: {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col p-2">
|
||||
<div className="flex justify-start p-2">
|
||||
<div className="flex items-center justify-start gap-3 p-2 lg:justify-between">
|
||||
<Link href="/">
|
||||
<NebulaIcon className="size-8 text-foreground" />
|
||||
</Link>
|
||||
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<FlaskConicalIcon className="size-2.5" />
|
||||
Alpha
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
|
||||
@@ -15,10 +15,13 @@ import {
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { sendTransaction } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { getThirdwebClient } from "../../../../@/constants/thirdweb.server";
|
||||
import { TransactionButton } from "../../../../components/buttons/TransactionButton";
|
||||
import { MarkdownRenderer } from "../../../../components/contract-components/published-contract/markdown-renderer";
|
||||
import { useV5DashboardChain } from "../../../../lib/v5-adapter";
|
||||
import { submitFeedback } from "../api/feedback";
|
||||
import { NebulaIcon } from "../icons/NebulaIcon";
|
||||
|
||||
@@ -241,16 +244,28 @@ function SendTransactionButton(props: {
|
||||
twAccount: TWAccount;
|
||||
}) {
|
||||
const account = useActiveAccount();
|
||||
const chain = useV5DashboardChain(props.txData?.chainId);
|
||||
|
||||
const sendTxMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (!account) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
if (!props.txData) {
|
||||
if (!props.txData || !chain) {
|
||||
throw new Error("Invalid transaction");
|
||||
}
|
||||
return account.sendTransaction(props.txData);
|
||||
|
||||
return sendTransaction({
|
||||
account,
|
||||
transaction: {
|
||||
...props.txData,
|
||||
nonce: Number(props.txData.nonce),
|
||||
to: props.txData.to || undefined, // Get rid of the potential null value
|
||||
chain,
|
||||
client: getThirdwebClient(),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -31,20 +31,70 @@ export const Mobile: Story = {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10 lg:p-10">
|
||||
<Variant contextFilters={undefined} label="No Filters Set" />
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
chainIds: ["1", "2", "3"],
|
||||
chainIds: ["137"],
|
||||
}}
|
||||
label="Chain ids Set"
|
||||
label="1 chain"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
chainIds: ["137", "10", "421614"],
|
||||
}}
|
||||
label="Few chains"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
contractAddresses: ["0x1E51e33F9838A5a043E099C60409f62aA564272f"],
|
||||
}}
|
||||
label="Contract addresses set"
|
||||
label="1 contract"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
contractAddresses: [
|
||||
"0x1E51e33F9838A5a043E099C60409f62aA564272f",
|
||||
"0xF61c8d5492139b40af09bDB353733d5F0a348aCf",
|
||||
],
|
||||
}}
|
||||
label="Few contracts"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
walletAddresses: ["0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37"],
|
||||
}}
|
||||
label="1 wallet"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
walletAddresses: [
|
||||
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
"0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
|
||||
],
|
||||
}}
|
||||
label="Few wallets"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
contextFilters={{
|
||||
chainIds: ["137", "10", "421614"],
|
||||
contractAddresses: [
|
||||
"0x1E51e33F9838A5a043E099C60409f62aA564272f",
|
||||
"0xF61c8d5492139b40af09bDB353733d5F0a348aCf",
|
||||
],
|
||||
walletAddresses: [
|
||||
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
|
||||
"0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
|
||||
],
|
||||
}}
|
||||
label="chains + wallets + contracts"
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -42,14 +43,46 @@ export default function ContextFiltersButton(props: {
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const chainIds = props.contextFilters?.chainIds;
|
||||
const contractAddresses = props.contextFilters?.contractAddresses;
|
||||
const walletAddresses = props.contextFilters?.walletAddresses;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline" className="gap-2">
|
||||
<SlidersHorizontalIcon className="size-4" />
|
||||
{props.contextFilters
|
||||
? "Edit Context Filters"
|
||||
: "Set context filters"}
|
||||
<Button size="sm" variant="outline" className="max-w-full gap-2">
|
||||
<SlidersHorizontalIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
Context Filters
|
||||
<div className="flex gap-1 overflow-hidden">
|
||||
{chainIds && chainIds.length > 0 && (
|
||||
<Badge className="gap-1 truncate">
|
||||
Chain
|
||||
<span className="max-sm:hidden">{chainIds.join(", ")}</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{contractAddresses && contractAddresses.length > 0 && (
|
||||
<Badge className="gap-1 truncate">
|
||||
Contract
|
||||
<span className="max-sm:hidden">
|
||||
{contractAddresses
|
||||
.map((add) => `${add.trim().slice(0, 6)}..`)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{walletAddresses && walletAddresses.length > 0 && (
|
||||
<Badge className="gap-1 truncate">
|
||||
Wallet
|
||||
<span className="max-sm:hidden">
|
||||
{walletAddresses
|
||||
.map((add) => `${add.trim().slice(0, 6)}..`)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="p-0">
|
||||
@@ -74,6 +107,18 @@ export default function ContextFiltersButton(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const commaSeparateListOfAddresses = z.string().refine(
|
||||
(s) => {
|
||||
if (s.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
return s.split(",").every((s) => isAddress(s.trim()));
|
||||
},
|
||||
{
|
||||
message: "Must be a comma-separated list of valid addresses",
|
||||
},
|
||||
);
|
||||
|
||||
const formSchema = z.object({
|
||||
chainIds: z.string().refine(
|
||||
(s) => {
|
||||
@@ -89,18 +134,8 @@ const formSchema = z.object({
|
||||
message: "Chain IDs must be a comma-separated list of integers",
|
||||
},
|
||||
),
|
||||
contractAddresses: z.string().refine(
|
||||
(s) => {
|
||||
if (s.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
return s.trim().split(",").every(isAddress);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Contract addresses must be a comma-separated list of valid addresses",
|
||||
},
|
||||
),
|
||||
contractAddresses: commaSeparateListOfAddresses,
|
||||
walletAddresses: commaSeparateListOfAddresses,
|
||||
});
|
||||
|
||||
function ContextFilterDialogContent(props: {
|
||||
@@ -117,25 +152,31 @@ function ContextFilterDialogContent(props: {
|
||||
contractAddresses: props.contextFilters?.contractAddresses
|
||||
? props.contextFilters.contractAddresses.join(",")
|
||||
: "",
|
||||
walletAddresses: props.contextFilters?.walletAddresses
|
||||
? props.contextFilters.walletAddresses.join(",")
|
||||
: "",
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const { chainIds, contractAddresses } = values;
|
||||
const chainIdsArray = chainIds.trim().split(",").filter(Boolean);
|
||||
const { chainIds, contractAddresses, walletAddresses } = values;
|
||||
|
||||
const chainIdsArray = chainIds.split(",").filter((id) => id.trim());
|
||||
|
||||
const contractAddressesArray = contractAddresses
|
||||
.trim()
|
||||
.split(",")
|
||||
.filter(Boolean);
|
||||
if (chainIdsArray.length === 0 && contractAddressesArray.length === 0) {
|
||||
props.updateFilters(undefined);
|
||||
} else {
|
||||
props.updateFilters({
|
||||
chainIds: chainIdsArray,
|
||||
contractAddresses: contractAddressesArray,
|
||||
});
|
||||
}
|
||||
.filter((v) => v.trim());
|
||||
|
||||
const walletAddressesArray = walletAddresses
|
||||
.split(",")
|
||||
.filter((v) => v.trim());
|
||||
|
||||
props.updateFilters({
|
||||
chainIds: chainIdsArray,
|
||||
contractAddresses: contractAddressesArray,
|
||||
walletAddresses: walletAddressesArray,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -195,6 +236,27 @@ function ContextFilterDialogContent(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="walletAddresses"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Wallet Addresses</FormLabel>
|
||||
<FormControl>
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of wallet addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex justify-end gap-3 border-t bg-muted/50 p-6">
|
||||
|
||||
@@ -32,7 +32,7 @@ export function MobileNav(props: {
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="h-auto w-auto p-0.5"
|
||||
>
|
||||
<MenuIcon className="size-6" />
|
||||
<MenuIcon className="size-8" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
const title =
|
||||
"thirdweb Nebula: The Most powerful AI for interacting with the blockchain";
|
||||
const description =
|
||||
"The most powerful AI for interacting with the blockchain, with real-time access to EVM chains and their data";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return props.children;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -2,11 +2,16 @@
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { useMemo } from "react";
|
||||
import { ThirdwebProvider, useActiveAccount } from "thirdweb/react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import {
|
||||
ThirdwebProvider,
|
||||
useActiveAccount,
|
||||
useConnectionManager,
|
||||
} from "thirdweb/react";
|
||||
import { CustomConnectWallet } from "../@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { PosthogIdentifier } from "../components/wallets/PosthogIdentifier";
|
||||
import { isSanctionedAddress } from "../data/eth-sanctioned-addresses";
|
||||
import { useAllChainsData } from "../hooks/chains/allChains";
|
||||
import { SyncChainStores } from "../stores/chainStores";
|
||||
import { TWAutoConnect } from "./components/autoconnect";
|
||||
|
||||
@@ -17,6 +22,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SyncChainStores />
|
||||
<ThirdwebProvider>
|
||||
<SyncChainDefinitionsToConnectionManager />
|
||||
<TWAutoConnect />
|
||||
<PosthogIdentifier />
|
||||
<ThemeProvider
|
||||
@@ -34,6 +40,23 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SyncChainDefinitionsToConnectionManager() {
|
||||
const { allChainsV5 } = useAllChainsData();
|
||||
const connectionManager = useConnectionManager();
|
||||
|
||||
// whenever user updates chains, update connection manager store
|
||||
// This is the same pattern used in ConnectButton for updating the connection manager when props.chain or props.chains change
|
||||
// this is added to root layout because ConnectButton is not always rendered in the page
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (allChainsV5.length > 0) {
|
||||
connectionManager.defineChains(allChainsV5);
|
||||
}
|
||||
}, [allChainsV5, connectionManager]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const SanctionedAddressesChecker = ({
|
||||
children,
|
||||
}: {
|
||||
|
||||
+28
@@ -28,9 +28,12 @@ import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useTrack } from "../../../../../../hooks/analytics/useTrack";
|
||||
import { getVercelEnv } from "../../../../../../lib/vercel-utils";
|
||||
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";
|
||||
|
||||
const trackingCategory = "insightBlueprint";
|
||||
|
||||
export function BlueprintPlayground(props: {
|
||||
metadata: BlueprintPathMetadata;
|
||||
backLink: string;
|
||||
@@ -151,6 +154,7 @@ export function BlueprintPlaygroundUI(props: {
|
||||
projectSettingsLink: string;
|
||||
supportedChainIds: number[];
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
const parameters = useMemo(() => {
|
||||
return modifyParametersForPlayground(props.metadata.parameters);
|
||||
}, [props.metadata.parameters]);
|
||||
@@ -182,6 +186,12 @@ export function BlueprintPlaygroundUI(props: {
|
||||
intent: "run",
|
||||
});
|
||||
|
||||
trackEvent({
|
||||
category: trackingCategory,
|
||||
action: "click",
|
||||
label: "run",
|
||||
url: url,
|
||||
});
|
||||
props.onRun(url);
|
||||
}
|
||||
|
||||
@@ -306,6 +316,8 @@ function PlaygroundHeader(props: {
|
||||
domain: string;
|
||||
path: string;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
return (
|
||||
<div className="border-b px-4 py-4 lg:flex lg:justify-center lg:py-3">
|
||||
@@ -325,6 +337,14 @@ function PlaygroundHeader(props: {
|
||||
values: props.getFormValues(),
|
||||
intent: "copy",
|
||||
});
|
||||
|
||||
trackEvent({
|
||||
category: trackingCategory,
|
||||
action: "click",
|
||||
label: "copy-url",
|
||||
url: url,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 500);
|
||||
@@ -561,6 +581,7 @@ function ResponseSection(props: {
|
||||
| undefined;
|
||||
abortRequest: () => void;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
const formattedData = useMemo(() => {
|
||||
if (!props.response?.data) return undefined;
|
||||
try {
|
||||
@@ -628,6 +649,13 @@ function ResponseSection(props: {
|
||||
scrollableContainerClassName="h-full"
|
||||
scrollableClassName="h-full"
|
||||
shadowColor="hsl(var(--muted)/50%)"
|
||||
onCopy={() => {
|
||||
trackEvent({
|
||||
category: trackingCategory,
|
||||
action: "click",
|
||||
label: "copy-response",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { isProd } from "@/constants/env";
|
||||
import { createStore, useStore } from "@/lib/reactive";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { Chain, ChainMetadata } from "thirdweb/chains";
|
||||
import { mapV4ChainToV5Chain } from "../../contexts/map-chains";
|
||||
import {
|
||||
type StoredChain,
|
||||
chainOverridesStore,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
|
||||
type AllChainsStore = {
|
||||
allChains: StoredChain[];
|
||||
allChainsV5: Chain[];
|
||||
idToChain: Map<number, StoredChain | undefined>;
|
||||
nameToChain: Map<string, StoredChain | undefined>;
|
||||
slugToChain: Map<string, StoredChain | undefined>;
|
||||
@@ -20,6 +22,7 @@ type AllChainsStore = {
|
||||
function createAllChainsStore() {
|
||||
const store = createStore<AllChainsStore>({
|
||||
allChains: [],
|
||||
allChainsV5: [],
|
||||
idToChain: new Map(),
|
||||
nameToChain: new Map(),
|
||||
slugToChain: new Map(),
|
||||
@@ -47,6 +50,7 @@ function createAllChainsStore() {
|
||||
// but don't ignore if chainOverrides is empty!
|
||||
|
||||
const allChains: StoredChain[] = [];
|
||||
const allChainsV5: Chain[] = [];
|
||||
const idToChain: Map<number, StoredChain | undefined> = new Map();
|
||||
const nameToChain: Map<string, StoredChain | undefined> = new Map();
|
||||
const slugToChain: Map<string, StoredChain | undefined> = new Map();
|
||||
@@ -71,6 +75,8 @@ function createAllChainsStore() {
|
||||
chainOverrides.find((x) => x.chainId === _chain.chainId) || _chain;
|
||||
|
||||
allChains.push(chain);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
allChainsV5.push(mapV4ChainToV5Chain(chain));
|
||||
idToChain.set(chain.chainId, chain);
|
||||
nameToChain.set(chain.name, chain);
|
||||
slugToChain.set(chain.slug, chain);
|
||||
@@ -88,6 +94,7 @@ function createAllChainsStore() {
|
||||
|
||||
store.setValue({
|
||||
allChains,
|
||||
allChainsV5,
|
||||
idToChain: idToChain,
|
||||
nameToChain: nameToChain,
|
||||
slugToChain: slugToChain,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { APIHeader } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
ChainIconBasic,
|
||||
ChainNameBasic,
|
||||
} from "@/components/headless-ui/chain-examples";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase,
|
||||
title: "Chain Components",
|
||||
description:
|
||||
"Enhance your applications with our Chain components, featuring a collection of chain icons, names, and symbols. These customizable components simplify the integration of blockchain information, allowing developers to easily display and manage multiple chains in their user interfaces.",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<main className="container px-0 pb-20">
|
||||
<APIHeader
|
||||
title="Chain Components"
|
||||
description={
|
||||
<>
|
||||
Enhance your applications with our Chain components, featuring a
|
||||
collection of chain icons, names, and symbols. These customizable
|
||||
components simplify the integration of blockchain information,
|
||||
allowing developers to easily display and manage multiple chains
|
||||
in their user interfaces.
|
||||
</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/components/onchain#chains"
|
||||
heroLink="/headless-ui-header.png"
|
||||
/>
|
||||
<section className="space-y-8">
|
||||
<ChainIconBasic />
|
||||
</section>
|
||||
<section className="space-y-8">
|
||||
<ChainNameBasic />
|
||||
</section>
|
||||
</main>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { APIHeader } from "@/components/blocks/APIHeader";
|
||||
import {} from "@/components/headless-ui/nft-examples";
|
||||
import {
|
||||
TokenCard,
|
||||
TokenImageBasic,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { APIHeader } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
WalletIconBasic,
|
||||
WalletNameBasic,
|
||||
WalletNameFormat,
|
||||
} from "@/components/headless-ui/wallet-examples";
|
||||
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase,
|
||||
title: "Wallet Components",
|
||||
description:
|
||||
"Boost your crypto wallet applications with our React headless UI components, optimized for digital asset management. These flexible, unstyled elements simplify cryptocurrency operations while granting developers complete control over the user interface design.",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<main className="container px-0 pb-20">
|
||||
<APIHeader
|
||||
title="Wallet Components"
|
||||
description={
|
||||
<>
|
||||
Boost your crypto wallet applications with our React headless UI
|
||||
components, optimized for digital asset management. These
|
||||
flexible, unstyled elements simplify cryptocurrency operations
|
||||
while granting developers complete control over the user interface
|
||||
design.
|
||||
</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/connecting-wallets/ui-components"
|
||||
heroLink="/headless-ui-header.png"
|
||||
/>
|
||||
<section className="space-y-8">
|
||||
<WalletIconBasic />
|
||||
</section>
|
||||
<section className="space-y-8">
|
||||
<WalletNameBasic />
|
||||
</section>
|
||||
<section className="space-y-8">
|
||||
<WalletNameFormat />
|
||||
</section>
|
||||
</main>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
@@ -127,6 +127,14 @@ export const navLinks: SidebarLink[] = [
|
||||
name: "Token",
|
||||
href: "/connect/ui/token",
|
||||
},
|
||||
{
|
||||
name: "Chain",
|
||||
href: "/connect/ui/chain",
|
||||
},
|
||||
{
|
||||
name: "Wallet",
|
||||
href: "/connect/ui/wallet",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { avalanche } from "thirdweb/chains";
|
||||
import { ChainIcon, ChainName, ChainProvider } from "thirdweb/react";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
|
||||
export function ChainIconBasic() {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
|
||||
ChainIcon
|
||||
</h2>
|
||||
<p className="max-w-[600px] text-lg">
|
||||
Show the native icon of a network
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainIcon
|
||||
client={THIRDWEB_CLIENT}
|
||||
className="h-auto w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</ChainProvider>
|
||||
}
|
||||
code={`import { ChainProvider, ChainIcon } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainIcon
|
||||
client={THIRDWEB_CLIENT}
|
||||
className="h-auto w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</ChainProvider>
|
||||
)
|
||||
}`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChainNameBasic() {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-8 space-y-2">
|
||||
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
|
||||
ChainName
|
||||
</h2>
|
||||
<p className="max-w-[600px] text-lg">Show the name of the chain</p>
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainName loadingComponent={<span>Loading...</span>} />
|
||||
</ChainProvider>
|
||||
}
|
||||
code={`import { ChainProvider, ChainName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ChainProvider chain={avalanche}>
|
||||
<ChainName loadingComponent={<span>Loading...</span>} />
|
||||
</ChainProvider>
|
||||
)
|
||||
}`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { WalletIcon, WalletName, WalletProvider } from "thirdweb/react";
|
||||
import { CodeExample } from "../code/code-example";
|
||||
|
||||
export function WalletIconBasic() {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
|
||||
WalletIcon
|
||||
</h2>
|
||||
<p className="max-w-[600px] text-lg">
|
||||
Show the icon of a crypto wallet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletIcon
|
||||
className="h-20 w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</WalletProvider>
|
||||
}
|
||||
code={`import { WalletProvider, WalletIcon } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletIcon
|
||||
className="h-20 w-20 rounded-full"
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
/>
|
||||
</WalletProvider>
|
||||
)
|
||||
}`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletNameBasic() {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-8 space-y-2">
|
||||
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
|
||||
WalletName
|
||||
</h2>
|
||||
<p className="max-w-[600px] text-lg">
|
||||
Show the name of a crypto wallet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName loadingComponent={<span>Loading...</span>} />
|
||||
</WalletProvider>
|
||||
}
|
||||
code={`import { WalletProvider, WalletName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName loadingComponent={<span>Loading...</span>} />
|
||||
</WalletProvider>
|
||||
)
|
||||
}`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletNameFormat() {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-8 space-y-2">
|
||||
<p className="max-w-[600px] text-lg">
|
||||
Transform the wallet name using the <b>formatFn</b> prop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
preview={
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName formatFn={(str: string) => `${str} Wallet`} />
|
||||
</WalletProvider>
|
||||
}
|
||||
code={`import { WalletProvider, WalletName } from "thirdweb/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName
|
||||
loadingComponent={<span>Loading...</span>}
|
||||
formatFn={(str: string) => \`\${str} Wallet\`}
|
||||
/>
|
||||
</WalletProvider>
|
||||
)
|
||||
}`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Guest Mode
|
||||
|
||||
Sometimes users want to get started using your app without signing in. You can now give users an in-memory "guest account" that can then be converted into a standard account by linking another auth method.
|
||||
Sometimes users want to get started using your app without signing in. You can now give users an in-memory "guest account" that can then be converted into a standard account by linking another auth method. An example is if you want to wait til a user tries to make a transaction before forcing them to onboard. A guest mode account is not persisted durably and only exists in memory until linked to an Auth method.
|
||||
|
||||
## Logging in with Guest Mode
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/react-native-adapter
|
||||
|
||||
## 1.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5746](https://github.com/thirdweb-dev/js/pull/5746) [`e42ffc6`](https://github.com/thirdweb-dev/js/commit/e42ffc6a931a8d80492a091d79e2d9b38e4ba1d7) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Downgrade aws libraries
|
||||
|
||||
## 1.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/react-native-adapter",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -28,9 +28,9 @@
|
||||
"src/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-kms": "3.709.0",
|
||||
"@aws-sdk/client-lambda": "3.709.0",
|
||||
"@aws-sdk/credential-providers": "3.709.0",
|
||||
"@aws-sdk/client-kms": "3.592.0",
|
||||
"@aws-sdk/client-lambda": "3.592.0",
|
||||
"@aws-sdk/credential-providers": "3.592.0",
|
||||
"@mobile-wallet-protocol/client": "0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -47,7 +47,7 @@
|
||||
"react-native": ">=0.70",
|
||||
"react-native-aes-gcm-crypto": "^0.2",
|
||||
"react-native-get-random-values": "^1",
|
||||
"react-native-quick-crypto": ">=0.7.0-rc.6 || >=0.7",
|
||||
"react-native-quick-crypto": ">=0.7",
|
||||
"react-native-svg": "^15",
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.79.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5691](https://github.com/thirdweb-dev/js/pull/5691) [`5be197b`](https://github.com/thirdweb-dev/js/commit/5be197bac97393edf39bf7287b5fa258cf83ec06) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Add headless components for Wallets: WalletProvider, WalletIcon and WalletName
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5746](https://github.com/thirdweb-dev/js/pull/5746) [`e42ffc6`](https://github.com/thirdweb-dev/js/commit/e42ffc6a931a8d80492a091d79e2d9b38e4ba1d7) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix migration to enclave in react native
|
||||
|
||||
## 5.78.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.78.0",
|
||||
"version": "5.79.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -312,7 +312,7 @@
|
||||
"test:ui": "vitest dev -c ./test/vitest.config.ts --coverage --ui",
|
||||
"test:dev": "vitest run -c ./test/vitest.config.ts",
|
||||
"test:react": "vitest run -c ./test/vitest.config.ts dev --ui src/react",
|
||||
"typedoc": "bun run scripts/typedoc.mjs",
|
||||
"typedoc": "node scripts/typedoc.mjs && node scripts/parse.mjs",
|
||||
"update-version": "node scripts/version.mjs",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
@@ -321,9 +321,9 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-kms": "3.709.0",
|
||||
"@aws-sdk/client-lambda": "3.709.0",
|
||||
"@aws-sdk/credential-providers": "3.709.0",
|
||||
"@aws-sdk/client-kms": "3.592.0",
|
||||
"@aws-sdk/client-lambda": "3.592.0",
|
||||
"@aws-sdk/credential-providers": "3.592.0",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@codspeed/vitest-plugin": "4.0.0",
|
||||
@@ -369,6 +369,7 @@
|
||||
"size-limit": "11.1.6",
|
||||
"storybook": "8.4.7",
|
||||
"typedoc": "0.27.4",
|
||||
"typedoc-better-json": "0.9.4",
|
||||
"typescript": "5.7.2",
|
||||
"vite": "6.0.3",
|
||||
"vitest": "2.1.8"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { transform } from "typedoc-better-json";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
const inputPath = "typedoc/documentation.json";
|
||||
const outputPath = "typedoc/parsed.json";
|
||||
|
||||
try {
|
||||
const fileContent = await readFile(inputPath, "utf-8");
|
||||
const fileData = JSON.parse(fileContent);
|
||||
|
||||
const transformedData = transform(fileData);
|
||||
|
||||
await writeFile(outputPath, JSON.stringify(transformedData, null, 2));
|
||||
|
||||
console.log(`File saved at ${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -150,28 +150,35 @@ export async function deployContractfromDeployMetadata(
|
||||
salt,
|
||||
} = options;
|
||||
|
||||
const processedImplParams: Record<string, string | string[]> = {};
|
||||
for (const key in implementationConstructorParams) {
|
||||
processedImplParams[key] = await processRefDeployments({
|
||||
client,
|
||||
account,
|
||||
chain,
|
||||
paramValue: implementationConstructorParams[key] as
|
||||
| string
|
||||
| ImplementationConstructorParam,
|
||||
});
|
||||
let processedImplParams: Record<string, string | string[]> | undefined;
|
||||
let processedInitializeParams: Record<string, string | string[]> | undefined;
|
||||
|
||||
if (implementationConstructorParams) {
|
||||
processedImplParams = {};
|
||||
for (const key in implementationConstructorParams) {
|
||||
processedImplParams[key] = await processRefDeployments({
|
||||
client,
|
||||
account,
|
||||
chain,
|
||||
paramValue: implementationConstructorParams[key] as
|
||||
| string
|
||||
| ImplementationConstructorParam,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const processedInitializeParams: Record<string, string | string[]> = {};
|
||||
for (const key in initializeParams) {
|
||||
processedInitializeParams[key] = await processRefDeployments({
|
||||
client,
|
||||
account,
|
||||
chain,
|
||||
paramValue: initializeParams[key] as
|
||||
| string
|
||||
| ImplementationConstructorParam,
|
||||
});
|
||||
if (initializeParams) {
|
||||
processedInitializeParams = {};
|
||||
for (const key in initializeParams) {
|
||||
processedInitializeParams[key] = await processRefDeployments({
|
||||
client,
|
||||
account,
|
||||
chain,
|
||||
paramValue: initializeParams[key] as
|
||||
| string
|
||||
| ImplementationConstructorParam,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (deployMetadata?.deployType) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MockStorage } from "../../../../../test/src/mocks/storage.js";
|
||||
import { TEST_CLIENT } from "../../../../../test/src/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "../../../../../test/src/test-wallets.js";
|
||||
import { createWalletAdapter } from "../../../../adapters/wallet-adapter.js";
|
||||
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
|
||||
import { createConnectionManager } from "../../../../wallets/manager/index.js";
|
||||
import { ConnectionManagerCtx } from "../../providers/connection-manager.js";
|
||||
import { useAddConnectedWallet } from "./useAddConnectedWallet.js";
|
||||
|
||||
describe("useAddConnectedWallet", () => {
|
||||
// Mock the connection manager
|
||||
const mockStorage = new MockStorage();
|
||||
const manager = createConnectionManager(mockStorage);
|
||||
|
||||
// Create a wrapper component with the mocked context
|
||||
const wrapper = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ConnectionManagerCtx.Provider value={manager}>
|
||||
{children}
|
||||
</ConnectionManagerCtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const wallet = createWalletAdapter({
|
||||
adaptedAccount: TEST_ACCOUNT_A,
|
||||
client: TEST_CLIENT,
|
||||
chain: ethereum,
|
||||
onDisconnect: () => {},
|
||||
switchChain: () => {},
|
||||
});
|
||||
|
||||
it("should add a wallet to the connection manager", async () => {
|
||||
// Render the hook
|
||||
const { result } = renderHook(() => useAddConnectedWallet(), { wrapper });
|
||||
result.current(wallet);
|
||||
|
||||
expect(manager.connectedWallets.getValue()).toHaveLength(1);
|
||||
expect(manager.connectedWallets.getValue()[0]).toEqual(wallet);
|
||||
// add connected wallet should not set the active wallet
|
||||
expect(manager.activeWalletStore.getValue()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw an error when used outside of ThirdwebProvider", () => {
|
||||
// Render the hook without a provider
|
||||
expect(() => {
|
||||
renderHook(() => useAddConnectedWallet());
|
||||
}).toThrow("useAddConnectedWallet must be used within <ThirdwebProvider>");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { MockStorage } from "../../../../../test/src/mocks/storage.js";
|
||||
import { TEST_CLIENT } from "../../../../../test/src/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "../../../../../test/src/test-wallets.js";
|
||||
import { createWalletAdapter } from "../../../../adapters/wallet-adapter.js";
|
||||
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
|
||||
import {
|
||||
type ConnectionManager,
|
||||
createConnectionManager,
|
||||
} from "../../../../wallets/manager/index.js";
|
||||
import { ConnectionManagerCtx } from "../../providers/connection-manager.js";
|
||||
import { useActiveWalletConnectionStatus } from "./useActiveWalletConnectionStatus.js";
|
||||
import { useConnect } from "./useConnect.js";
|
||||
|
||||
describe("useAddConnectedWallet", () => {
|
||||
// Mock the connection manager
|
||||
const mockStorage = new MockStorage();
|
||||
let manager: ConnectionManager;
|
||||
|
||||
// Create a wrapper component with the mocked context
|
||||
const wrapper = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ConnectionManagerCtx.Provider value={manager}>
|
||||
{children}
|
||||
</ConnectionManagerCtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const wallet = createWalletAdapter({
|
||||
adaptedAccount: TEST_ACCOUNT_A,
|
||||
client: TEST_CLIENT,
|
||||
chain: ethereum,
|
||||
onDisconnect: () => {},
|
||||
switchChain: () => {},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
manager = createConnectionManager(mockStorage);
|
||||
});
|
||||
|
||||
it("should connect a wallet to the connection manager", async () => {
|
||||
const { result: statusResult } = renderHook(
|
||||
() => useActiveWalletConnectionStatus(),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(() => useConnect(), { wrapper });
|
||||
expect(statusResult.current).toEqual("disconnected");
|
||||
await result.current.connect(async () => wallet);
|
||||
expect(statusResult.current).toEqual("connected");
|
||||
|
||||
// should add to connected wallets
|
||||
expect(manager.connectedWallets.getValue()).toHaveLength(1);
|
||||
expect(manager.connectedWallets.getValue()[0]).toEqual(wallet);
|
||||
// should set the active wallet
|
||||
expect(manager.activeWalletStore.getValue()).toEqual(wallet);
|
||||
});
|
||||
|
||||
it("should handle a function that returns a wallet", async () => {
|
||||
const { result: statusResult } = renderHook(
|
||||
() => useActiveWalletConnectionStatus(),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(() => useConnect(), { wrapper });
|
||||
expect(statusResult.current).toEqual("disconnected");
|
||||
await result.current.connect(async () => wallet);
|
||||
expect(statusResult.current).toEqual("connected");
|
||||
|
||||
// should add to connected wallets
|
||||
expect(manager.connectedWallets.getValue()).toHaveLength(1);
|
||||
expect(manager.connectedWallets.getValue()[0]).toEqual(wallet);
|
||||
// should set the active wallet
|
||||
expect(manager.activeWalletStore.getValue()).toEqual(wallet);
|
||||
});
|
||||
|
||||
it("should handle an error when connecting a wallet", async () => {
|
||||
const { result: statusResult } = renderHook(
|
||||
() => useActiveWalletConnectionStatus(),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
expect(statusResult.current).toEqual("disconnected");
|
||||
const { result } = renderHook(() => useConnect(), { wrapper });
|
||||
await result.current.connect(async () => {
|
||||
throw new Error("test");
|
||||
});
|
||||
|
||||
expect(statusResult.current).toEqual("disconnected");
|
||||
// should set the active wallet
|
||||
expect(manager.activeWalletStore.getValue()).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should throw an error when used outside of ThirdwebProvider", () => {
|
||||
// Render the hook without a provider
|
||||
expect(() => {
|
||||
renderHook(() => useConnect());
|
||||
}).toThrow("useConnect must be used within <ThirdwebProvider>");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MockStorage } from "../../../../../test/src/mocks/storage.js";
|
||||
import { TEST_CLIENT } from "../../../../../test/src/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "../../../../../test/src/test-wallets.js";
|
||||
import { createWalletAdapter } from "../../../../adapters/wallet-adapter.js";
|
||||
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
|
||||
import { createConnectionManager } from "../../../../wallets/manager/index.js";
|
||||
import { ConnectionManagerCtx } from "../../providers/connection-manager.js";
|
||||
import { useSetActiveWallet } from "./useSetActiveWallet.js";
|
||||
|
||||
describe("useAddConnectedWallet", () => {
|
||||
// Mock the connection manager
|
||||
const mockStorage = new MockStorage();
|
||||
const manager = createConnectionManager(mockStorage);
|
||||
|
||||
// Create a wrapper component with the mocked context
|
||||
const wrapper = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ConnectionManagerCtx.Provider value={manager}>
|
||||
{children}
|
||||
</ConnectionManagerCtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const wallet = createWalletAdapter({
|
||||
adaptedAccount: TEST_ACCOUNT_A,
|
||||
client: TEST_CLIENT,
|
||||
chain: ethereum,
|
||||
onDisconnect: () => {},
|
||||
switchChain: () => {},
|
||||
});
|
||||
|
||||
it("should add a wallet to the connection manager", async () => {
|
||||
// Render the hook
|
||||
const { result } = renderHook(() => useSetActiveWallet(), { wrapper });
|
||||
result.current(wallet);
|
||||
|
||||
// should add to connected wallets
|
||||
expect(manager.connectedWallets.getValue()).toHaveLength(1);
|
||||
expect(manager.connectedWallets.getValue()[0]).toEqual(wallet);
|
||||
// should set the active wallet
|
||||
expect(manager.activeWalletStore.getValue()).toEqual(wallet);
|
||||
});
|
||||
|
||||
it("should throw an error when used outside of ThirdwebProvider", () => {
|
||||
// Render the hook without a provider
|
||||
expect(() => {
|
||||
renderHook(() => useSetActiveWallet());
|
||||
}).toThrow("useSetActiveWallet must be used within <ThirdwebProvider>");
|
||||
});
|
||||
});
|
||||
@@ -143,7 +143,7 @@ export function ChainIcon({
|
||||
}
|
||||
// Check if the chain object already has "icon"
|
||||
if (chain.icon?.url) {
|
||||
return chain.icon.url;
|
||||
return resolveScheme({ uri: chain.icon.url, client });
|
||||
}
|
||||
const possibleUrl = await getChainMetadata(chain).then(
|
||||
(data) => data.icon?.url,
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
@@ -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<string>);
|
||||
}) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ const ChainProviderContext = /* @__PURE__ */ createContext<
|
||||
* ```
|
||||
* @component
|
||||
* @chain
|
||||
* @beta
|
||||
*/
|
||||
export function ChainProvider(
|
||||
props: React.PropsWithChildren<ChainProviderProps>,
|
||||
|
||||
@@ -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(
|
||||
<WalletProvider id="io.cosmostation">
|
||||
<WalletIcon />
|
||||
</WalletProvider>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector("img")).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
*/
|
||||
export async function fetchWalletImage(props: {
|
||||
id: WalletId;
|
||||
}) {
|
||||
const image_src = await getWalletInfo(props.id, true);
|
||||
return image_src;
|
||||
}
|
||||
@@ -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(
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName />
|
||||
</WalletProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector("span")).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<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, formatFn });
|
||||
if (nameQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
if (!nameQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
return <span {...restProps}>{nameQuery.data}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function useWalletName(props: {
|
||||
formatFn?: (str: string) => string;
|
||||
queryOptions?: Omit<UseQueryOptions<string>, "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;
|
||||
}
|
||||
@@ -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 <WalletProvider /> component",
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("useWalletContext should return the context value when used within WalletProvider", () => {
|
||||
const wrapper: FC = ({ children }: React.PropsWithChildren) => (
|
||||
<WalletProvider id="io.metamask">{children}</WalletProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useWalletContext(), { wrapper });
|
||||
|
||||
expect(result.current.id).toStrictEqual("io.metamask");
|
||||
});
|
||||
|
||||
it("should render children correctly", () => {
|
||||
render(
|
||||
<WalletProvider id="io.metamask">
|
||||
<div>Child Component</div>
|
||||
</WalletProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Child Component")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should provide context values to children", () => {
|
||||
function WalletConsumer() {
|
||||
const context = useContext(WalletProviderContext);
|
||||
if (!context) {
|
||||
return <div>No context</div>;
|
||||
}
|
||||
return <div>{String(context.id)}</div>;
|
||||
}
|
||||
render(
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletConsumer />
|
||||
</WalletProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("io.metamask")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
*
|
||||
* <WalletProvider id="io.metamask">
|
||||
* <WalletIcon />
|
||||
* <WalletName />
|
||||
* </WalletProvider>
|
||||
* ```
|
||||
* @beta
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export function WalletProvider(
|
||||
props: React.PropsWithChildren<WalletProviderProps>,
|
||||
) {
|
||||
return (
|
||||
<WalletProviderContext.Provider value={props}>
|
||||
{props.children}
|
||||
</WalletProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 <WalletProvider /> component",
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export const GENERATE_RECOVERY_PASSWORD_LAMBDA_FUNCTION_V1 =
|
||||
export const GENERATE_RECOVERY_PASSWORD_LAMBDA_FUNCTION_V2 =
|
||||
"arn:aws:lambda:us-west-2:324457261097:function:lambda-thirdweb-auth-enc-key-prod-ThirdwebAuthEncKeyFunction";
|
||||
export const ENCLAVE_KMS_KEY_ARN =
|
||||
"arn:aws:kms:us-west-2:324457261097:key/8b2a8cd5-9b22-4ea0-a6bc-463824a78f20";
|
||||
"arn:aws:kms:us-west-2:324457261097:key/ccfb9ecd-f45d-4f37-864a-25fe72dcb49e";
|
||||
|
||||
// TODO allow overriding domain
|
||||
const DOMAIN_URL_2023 = getThirdwebBaseUrl("inAppWallet");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { getThirdwebDomains } from "../../../src/utils/domains.js";
|
||||
import type { AsyncStorage } from "../../../src/utils/storage/AsyncStorage.js";
|
||||
|
||||
export const handlers = [
|
||||
http.post(
|
||||
@@ -7,11 +8,11 @@ export const handlers = [
|
||||
async ({ request }) => {
|
||||
console.log("MSW handler hit for IPFS upload");
|
||||
const formData = await request.formData();
|
||||
const files = formData.getAll('file');
|
||||
const files = formData.getAll("file");
|
||||
const fileNames = files.map((file: any) => file.name);
|
||||
|
||||
|
||||
const mockIpfsHash = "QmTest1234567890TestHash";
|
||||
|
||||
|
||||
if (fileNames.length === 1 && fileNames[0] === "file.txt") {
|
||||
return HttpResponse.json({ IpfsHash: mockIpfsHash });
|
||||
} else {
|
||||
@@ -20,7 +21,7 @@ export const handlers = [
|
||||
files: fileNames.reduce((acc, fileName) => {
|
||||
acc[fileName] = { cid: mockIpfsHash };
|
||||
return acc;
|
||||
}, {})
|
||||
}, {}),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -40,3 +41,19 @@ export const handlers = [
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
export class MockStorage implements AsyncStorage {
|
||||
private storage: Map<string, string> = new Map();
|
||||
|
||||
getItem(key: string): Promise<string | null> {
|
||||
return Promise.resolve(this.storage.get(key) ?? null);
|
||||
}
|
||||
setItem(key: string, value: string): Promise<void> {
|
||||
this.storage.set(key, value);
|
||||
return Promise.resolve();
|
||||
}
|
||||
removeItem(key: string): Promise<void> {
|
||||
this.storage.delete(key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.1.2
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/wagmi-adapter",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
Generated
+869
-91
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user