Compare commits

..
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
6994310d16 Version Packages (#5741)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-17 01:08:47 +13:00
Joaquim VergesandGitHub e42ffc6a93 Fix migration to enclave wallets in react native (#5746) 2024-12-17 00:27:00 +13:00
gregfromstl 0ddc123807 [Dashboard] Fix: Nebula send transaction params (#5744)
<!-- start pr-codex -->

## PR-Codex overview
This PR enhances the `SendTransactionButton` component in `Chats.tsx` by introducing improved transaction handling and validation through the addition of a blockchain `chain` context and the `sendTransaction` method.

### Detailed summary
- Added import for `sendTransaction` from `thirdweb`.
- Introduced `chain` variable using `useV5DashboardChain`.
- Updated transaction validation to check for both `txData` and `chain`.
- Refactored transaction sending logic to use `sendTransaction` with detailed parameters.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-15 23:38:20 +00:00
gregfromstl f91f6310e9 [CI] Feature: Deploy typedoc to permalink (#5740)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces a new `TypeDoc` workflow for generating documentation and updates the `parse.mjs` script to process documentation data. It also modifies the `package.json` to include the `typedoc-better-json` dependency and adjusts the `pnpm-lock.yaml` accordingly.

### Detailed summary
- Added `TypeDoc` GitHub Actions workflow in `.github/workflows/typedoc.yml`.
- Created a new `parse.mjs` script in `packages/thirdweb/scripts` for transforming documentation data.
- Updated `package.json` to include `typedoc-better-json` as a dependency.
- Adjusted `pnpm-lock.yaml` to reflect changes in dependencies and versions.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-14 17:43:16 +00:00
gregfromstl dc1e07b5e4 [CI] Chore: Only run actions for contributors (#5739)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the GitHub workflows for pull requests by adding conditions for author associations and expanding issue types.

### Detailed summary
- In `.github/workflows/auto-assign.yml`, added checks to assign authors only if they are `MEMBER`, `OWNER`, or `COLLABORATOR`.
- In `.github/workflows/issue.yml`, expanded the `types` to include `ready_for_review`.
- Added logic to automatically pass checks for external contributors.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-14 17:38:51 +00:00
kien-ngo 2b5080d901 Playground: Examples for Wallet components (#5692)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `Wallet` navigation link and implements a new `Wallet` page with components for displaying wallet icons and names, enhancing the user interface for crypto wallet applications.

### Detailed summary
- Added a new navigation link for `Wallet`.
- Created `page.tsx` for the `Wallet` section with metadata and layout.
- Implemented `APIHeader` for the `Wallet` page.
- Added three components: `WalletIconBasic`, `WalletNameBasic`, and `WalletNameFormat` in `wallet-examples.tsx`.
- Each component includes a description and code example for usage.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-14 11:27:08 +00:00
kien-ngo 5be197bac9 [SDK] Headless Wallet components | CNCT-2620 (#5691)
CNCT-2620

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new headless components for wallet management in the `thirdweb` library, enhancing user interface options for wallet context, icons, and names.

### Detailed summary
- Added `WalletProvider`, `WalletIcon`, and `WalletName` components.
- Updated `ChainProvider` to mark it as `@beta`.
- Introduced utility functions `getQueryKeys` for both chain and wallet components.
- Updated documentation and tests for new wallet components.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-14 10:26:48 +00:00
MananTank 34165cb608 [DASH-616] Nebula - Add Alpha badge in sidebar (#5738)
## Problem solved

DASH-616
2024-12-13 21:33:58 +00:00
MananTank 9954b37b01 [DASH-624] Add tracking in insight playground (#5737)
## Problem solved

DASH-624

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `CodeBlockContainer` and related components to support an `onCopy` callback, allowing for custom actions when code is copied. It also introduces tracking for user interactions within the `BlueprintPlayground` component.

### Detailed summary
- Added `onCopy` prop to `CodeBlockContainer`, `RenderCode`, `PlainTextCodeBlock`, and `CodeClient`.
- Updated button click handler in `CodeBlockContainer` to call `onCopy` and pass the copied code.
- Integrated `useTrack` for tracking user events in `BlueprintPlayground`.
- Added tracking for "run", "copy-url", and "copy-response" actions in relevant components.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-13 21:10:48 +00:00
MananTank 9b58a086b6 [Dashboard] Nebula - Update context filters + Add OG image (#5736)
## Problem solved

DASH-615

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the context filter functionality within the application by introducing support for `walletAddresses`, updating related types, and modifying components to accommodate these changes.

### Detailed summary
- Added `walletAddresses` to various types and API parameters.
- Updated `SessionContextFilter` to include `wallet_addresses`.
- Modified `ChatPageContent` to handle and display `walletAddresses`.
- Enhanced `ContextFiltersButton` to show wallet address badges.
- Updated form handling to include validation for `walletAddresses`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-13 20:08:49 +00:00
gregandGitHub 22bc557090 [CI] Chore: Update allowed teams (#5735) 2024-12-13 10:31:00 -08:00
jnsdls 80b54a937c use resolved ENS name if available (#5734)
fixes: DASH-572
2024-12-13 18:14:08 +00:00
Will CoryandGitHub be6433dcf2 docs: Add more context on guest mode (#5733)
Signed-off-by: Will Cory <[email protected]>
2024-12-13 09:42:05 -08:00
MananTank f6c040719c [DASH-612] Nebula: Add title and description meta (#5732)
DASH-612
2024-12-13 17:11:58 +00:00
MananTank 2db85824f0 [DASH-610] Fix useActiveWalletChain missing chain overrides added in dashboard (#5731)
Fixes: DASH-610
2024-12-13 16:59:01 +00:00
kien-ngo 498a26ebbc [Playground] Chain components (#5678)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new `Chain` components to enhance the user interface for blockchain applications, allowing for the display of chain icons and names. It includes new pages and components that integrate these features into the existing playground web application.

### Detailed summary
- Added a new `Chain` entry in `navLinks.ts`.
- Created a new page in `page.tsx` for `Chain` components with metadata.
- Integrated `ThirdwebProvider`, `APIHeader`, `ChainIconBasic`, and `ChainNameBasic` components.
- Implemented `ChainIconBasic` and `ChainNameBasic` components in `chain-examples.tsx` to showcase chain icons and names.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-13 16:14:41 +00:00
kumaryash90 8761fe6102 Process deploy param ref only when non-empty (#5730)
PROT-935

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the handling of `implementationConstructorParams` and `initializeParams` by adding null checks to ensure that they are defined before processing. This improves the robustness of the code.

### Detailed summary
- Changed `processedImplParams` and `processedInitializeParams` from constants to `undefined` initially.
- Added checks to confirm that `implementationConstructorParams` and `initializeParams` are defined before processing.
- Retained the logic for processing parameters using `processRefDeployments`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-12-13 09:16:09 +00:00
Joaquim VergesandGitHub 77c8d0eff0 test: Add wallet connection hook tests (#5689) 2024-12-13 20:25:13 +13:00
60 changed files with 2452 additions and 227 deletions
+5 -1
View File
@@ -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]
+13 -2
View File
@@ -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');
+50
View File
@@ -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`);
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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>
@@ -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

+25 -2
View File
@@ -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,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>
+8 -1
View File
@@ -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>
);
}
+8
View File
@@ -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
+5 -5
View File
@@ -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"
},
+10
View File
@@ -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
+6 -5
View File
@@ -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"
+19
View File
@@ -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);
}
+14
View File
@@ -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");
+21 -4
View File
@@ -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();
}
}
+2
View File
@@ -1,5 +1,7 @@
# @thirdweb-dev/wagmi-adapter
## 0.1.2
## 0.1.1
### Patch Changes
+1 -1
View File
@@ -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"
+869 -91
View File
File diff suppressed because it is too large Load Diff