Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca22e4659e | ||
|
|
411895c002 | ||
|
|
cfa1c20a91 | ||
|
|
2b16f90715 | ||
|
|
e8892b152f | ||
|
|
073b25afd7 | ||
|
|
4550bb2663 | ||
|
|
f5f8a40320 | ||
|
|
3569763501 | ||
|
|
1401f8d039 | ||
|
|
1616b7f619 | ||
|
|
aa548a359f | ||
|
|
eef52dfa7e | ||
|
|
19ec571500 | ||
|
|
47dce2e05c | ||
|
|
8b4695224c | ||
|
|
94e2d5b8dd | ||
|
|
40fa323294 | ||
|
|
2cebb420e6 | ||
|
|
e89ddb24f2 | ||
|
|
86f205e2aa | ||
|
|
6c277ae676 | ||
|
|
153651df35 | ||
|
|
8ba460b6fc | ||
|
|
1e61a9edfc | ||
|
|
d03e687aa9 | ||
|
|
e1c0dbe19e | ||
|
|
3e7df081d7 | ||
|
|
b6f189c220 | ||
|
|
fb4d0a26f1 | ||
|
|
bc345371ba | ||
|
|
7a8e4e5f86 |
@@ -1,12 +1,15 @@
|
||||
---
|
||||
title: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
---
|
||||
<!--
|
||||
|
||||
## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
|
||||
If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):
|
||||
|
||||
## Notes for the reviewer
|
||||
|
||||
Anything important to call out? Be sure to also clarify these in your comments.
|
||||
|
||||
## How to test
|
||||
|
||||
Unit tests, playground, etc.
|
||||
|
||||
-->
|
||||
|
||||
+38
-38
@@ -31,29 +31,29 @@
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@n8tb1t/use-scroll-position": "^2.0.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-hover-card": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-hover-card": "^1.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@sentry/nextjs": "8.45.1",
|
||||
"@shazow/whatsabi": "^0.18.0",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.51.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.5.2",
|
||||
"@vercel/og": "^0.6.4",
|
||||
"abitype": "1.0.7",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -62,19 +62,19 @@
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.15.0",
|
||||
"framer-motion": "12.0.6",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"papaparse": "^5.5.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -87,11 +87,11 @@
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.14.1",
|
||||
"recharts": "2.15.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.1",
|
||||
"sonner": "^1.7.2",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.18.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
@@ -103,29 +103,29 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@next/bundle-analyzer": "15.1.0",
|
||||
"@next/eslint-plugin-next": "15.1.3",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/addon-viewport": "8.4.7",
|
||||
"@storybook/blocks": "8.4.7",
|
||||
"@storybook/nextjs": "8.4.7",
|
||||
"@storybook/react": "8.5.0",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@next/bundle-analyzer": "15.1.6",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/addon-onboarding": "8.5.2",
|
||||
"@storybook/addon-viewport": "8.5.2",
|
||||
"@storybook/blocks": "8.5.2",
|
||||
"@storybook/nextjs": "8.5.2",
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@@ -134,11 +134,11 @@
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-storybook": "^0.11.1",
|
||||
"knip": "5.41.0",
|
||||
"knip": "5.43.5",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"storybook": "8.4.7",
|
||||
"postcss": "8.5.1",
|
||||
"storybook": "8.5.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export type Team = {
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ type Option = { label: string; value: string };
|
||||
export function MultiNetworkSelector(props: {
|
||||
selectedChainIds: number[];
|
||||
onChange: (chainIds: number[]) => void;
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -53,16 +55,24 @@ export function MultiNetworkSelector(props: {
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -79,6 +89,7 @@ export function MultiNetworkSelector(props: {
|
||||
disabled={allChains.length === 0}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -143,7 +154,7 @@ export function SingleNetworkSelector(props: {
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{chain.name}
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
|
||||
{!props.disableChainId && (
|
||||
@@ -168,6 +179,7 @@ export function SingleNetworkSelector(props: {
|
||||
onValueChange={(chainId) => {
|
||||
props.onChange(Number(chainId));
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
|
||||
@@ -33,6 +33,7 @@ interface SelectWithSearchProps
|
||||
popoverContentClassName?: string;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
closeOnSelect?: boolean;
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
@@ -50,6 +51,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
overrideSearchFn,
|
||||
popoverContentClassName,
|
||||
searchPlaceholder,
|
||||
closeOnSelect,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -177,7 +179,12 @@ export const SelectWithSearch = React.forwardRef<
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => onValueChange(option.value)}
|
||||
onClick={() => {
|
||||
onValueChange(option.value);
|
||||
if (closeOnSelect) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
ref={
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
@@ -469,12 +469,12 @@ export function useEngineBackendWalletBalance(params: {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) {
|
||||
const { instanceUrl, address, authToken } = params;
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const { instanceUrl, address, authToken, chainId } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId || 1),
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId),
|
||||
queryFn: async () => {
|
||||
invariant(chainId, "chainId is required");
|
||||
|
||||
|
||||
+5
-1
@@ -4,6 +4,7 @@ import {
|
||||
toTokens,
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
@@ -71,7 +72,10 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
conditions.map(async (condition) => {
|
||||
const [currencyMetadata, metadata, contractMetadata] = await Promise.all([
|
||||
await ERC20Ext.getCurrencyMetadata({
|
||||
contract: { ...options.contract, address: condition.currency },
|
||||
contract: getContract({
|
||||
...options.contract,
|
||||
address: condition.currency,
|
||||
}),
|
||||
}).then((m) => ({
|
||||
...m,
|
||||
displayValue: toTokens(condition.pricePerToken, m.decimals),
|
||||
|
||||
+1
@@ -323,6 +323,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
enableNebulaServiceByDefault={false}
|
||||
teamSlug={undefined}
|
||||
/>
|
||||
|
||||
<Alert variant="warning">
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { CircleSlash } from "lucide-react";
|
||||
import type { ContractOptions } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { ModuleCard } from "./module-card";
|
||||
import { useAllModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
export const InstalledModulesTable = (props: {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
installedModules: {
|
||||
data?: string[];
|
||||
isPending: boolean;
|
||||
|
||||
+10
-7
@@ -15,8 +15,9 @@ import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
type ContractOptions,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
} from "thirdweb";
|
||||
@@ -47,7 +48,7 @@ type FormData = {
|
||||
};
|
||||
|
||||
type InstallModuleFormProps = {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
refetchModules: () => void;
|
||||
account: Account;
|
||||
installedModules: {
|
||||
@@ -180,11 +181,13 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
|
||||
return Promise.all(
|
||||
moduleAddress.map(async (address) => {
|
||||
const result = await resolveImplementation({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
});
|
||||
const result = await resolveImplementation(
|
||||
getContract({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Failed to fetch bytecode for module");
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ import { Suspense, useEffect, useState } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ContractOptions,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
@@ -35,7 +35,7 @@ import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
type ModuleCardProps = {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
onRemoveModule: () => void;
|
||||
ownerAccount: Account | undefined;
|
||||
allModuleContractInfo: {
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {} from "@/components/ui/alert";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
@@ -70,6 +70,11 @@ export function AccountHeader(props: {
|
||||
// refresh projects
|
||||
router.refresh();
|
||||
}}
|
||||
teamSlug={
|
||||
createProjectDialogState.isOpen
|
||||
? createProjectDialogState.team.slug
|
||||
: undefined
|
||||
}
|
||||
enableNebulaServiceByDefault={
|
||||
createProjectDialogState.isOpen &&
|
||||
createProjectDialogState.team.enabledScopes.includes("nebula")
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
import { Suspense, lazy, useEffect, useState } from "react";
|
||||
import { ConnectEmbed, useActiveWalletConnectionStatus } from "thirdweb/react";
|
||||
import { createWallet, inAppWallet } from "thirdweb/wallets";
|
||||
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
|
||||
@@ -154,27 +154,6 @@ function PageContent(props: {
|
||||
router.replace(props.redirectPath);
|
||||
}
|
||||
|
||||
if (connectionStatus === "connecting") {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingCard />}>
|
||||
<LazyOnboardingUI
|
||||
account={screen.account}
|
||||
onComplete={onComplete}
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function onLogin() {
|
||||
const account = await getRawAccountAction();
|
||||
|
||||
@@ -193,6 +172,38 @@ function PageContent(props: {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
// if suddenly disconnected
|
||||
if (connectionStatus !== "connected" && screen.id !== "login") {
|
||||
setScreen({ id: "login" });
|
||||
}
|
||||
}, [connectionStatus, screen.id]);
|
||||
|
||||
if (connectionStatus === "connecting") {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingCard />}>
|
||||
<LazyOnboardingUI
|
||||
account={screen.account}
|
||||
onComplete={onComplete}
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
onLogout={() => {
|
||||
setScreen({ id: "login" });
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ export function OnboardingChoosePlan(props: {
|
||||
billingPlan="starter"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get started for free",
|
||||
title: "Get Started for free",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "starterPlan",
|
||||
},
|
||||
}}
|
||||
redirectPath={props.redirectPath}
|
||||
@@ -40,15 +41,14 @@ export function OnboardingChoosePlan(props: {
|
||||
billingPlan="growth"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Claim your 1-month free",
|
||||
hint: "Your free trial will end after 30 days.",
|
||||
title: "Get Started with Growth",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "growthPlan",
|
||||
},
|
||||
variant: "default",
|
||||
}}
|
||||
canTrialGrowth={props.canTrialGrowth}
|
||||
canTrialGrowth={false}
|
||||
highlighted
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={props.redirectToCheckout}
|
||||
|
||||
@@ -14,12 +14,14 @@ type OnboardingGeneralProps = {
|
||||
account: Account;
|
||||
onSave: (email: string) => void;
|
||||
onDuplicate: (email: string) => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
account,
|
||||
onSave,
|
||||
onDuplicate,
|
||||
onLogout,
|
||||
}) => {
|
||||
const [existing, setExisting] = useState(false);
|
||||
const activeWallet = useActiveWallet();
|
||||
@@ -27,6 +29,7 @@ export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
|
||||
async function handleLogout() {
|
||||
await doLogout();
|
||||
onLogout();
|
||||
if (activeWallet) {
|
||||
disconnect(activeWallet);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type OnboardingScreen =
|
||||
function OnboardingUI(props: {
|
||||
account: Account;
|
||||
onComplete: () => void;
|
||||
onLogout: () => void;
|
||||
// path to redirect from stripe
|
||||
redirectPath: string;
|
||||
redirectToCheckout: RedirectBillingCheckoutAction;
|
||||
@@ -68,6 +69,7 @@ function OnboardingUI(props: {
|
||||
{screen.id === "onboarding" && (
|
||||
<OnboardingGeneral
|
||||
account={account}
|
||||
onLogout={props.onLogout}
|
||||
onSave={(email) => {
|
||||
setUpdatedEmail(email);
|
||||
setScreen({
|
||||
|
||||
@@ -181,6 +181,7 @@ function SessionCard(props: {
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={`/chat/${props.session.id}`}
|
||||
prefetch={false}
|
||||
>
|
||||
{props.session.title || "Untitled"}
|
||||
</Link>
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { ExecuteConfig, SessionInfo } from "../api/types";
|
||||
import { newChatPageUrlStore, newSessionsStore } from "../stores";
|
||||
import { ChatBar } from "./ChatBar";
|
||||
import { type ChatMessage, Chats } from "./Chats";
|
||||
import ContextFiltersButton from "./ContextFilters";
|
||||
import ContextFiltersButton, { ContextFiltersForm } from "./ContextFilters";
|
||||
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
|
||||
|
||||
export function ChatPageContent(props: {
|
||||
@@ -362,69 +362,92 @@ export function ChatPageContent(props: {
|
||||
|
||||
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
|
||||
|
||||
const handleUpdateContextFilters = async (
|
||||
values: ContextFilters | undefined,
|
||||
) => {
|
||||
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
|
||||
if (sessionId) {
|
||||
await updateSession({
|
||||
authToken: props.authToken,
|
||||
config,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col overflow-hidden">
|
||||
<WalletDisconnectedDialog
|
||||
open={showConnectModal}
|
||||
onOpenChange={setShowConnectModal}
|
||||
/>
|
||||
<header className="flex justify-start border-b bg-background p-4">
|
||||
<header className="flex justify-between border-b bg-background p-4 xl:hidden">
|
||||
<ContextFiltersButton
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
updateContextFilters={async (values) => {
|
||||
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
|
||||
if (sessionId) {
|
||||
await updateSession({
|
||||
authToken: props.authToken,
|
||||
config,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
}
|
||||
}}
|
||||
updateContextFilters={handleUpdateContextFilters}
|
||||
/>
|
||||
</header>
|
||||
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
|
||||
{showEmptyState ? (
|
||||
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
|
||||
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
|
||||
<Chats
|
||||
messages={messages}
|
||||
isChatStreaming={isChatStreaming}
|
||||
authToken={props.authToken}
|
||||
sessionId={sessionId}
|
||||
className="min-w-0 pt-6 pb-32"
|
||||
twAccount={props.account}
|
||||
client={client}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
setEnableAutoScroll={setEnableAutoScroll}
|
||||
/>
|
||||
|
||||
<div className="container max-w-[800px]">
|
||||
<ChatBar
|
||||
sendMessage={handleSendMessage}
|
||||
isChatStreaming={isChatStreaming}
|
||||
abortChatStream={() => {
|
||||
chatAbortController?.abort();
|
||||
setChatAbortController(undefined);
|
||||
setIsChatStreaming(false);
|
||||
// if last message is presence, remove it
|
||||
if (messages[messages.length - 1]?.type === "presence") {
|
||||
setMessages((prev) => prev.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex grow overflow-hidden">
|
||||
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
|
||||
{showEmptyState ? (
|
||||
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
|
||||
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
|
||||
<Chats
|
||||
messages={messages}
|
||||
isChatStreaming={isChatStreaming}
|
||||
authToken={props.authToken}
|
||||
sessionId={sessionId}
|
||||
className="min-w-0 pt-6 pb-32"
|
||||
twAccount={props.account}
|
||||
client={client}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
setEnableAutoScroll={setEnableAutoScroll}
|
||||
/>
|
||||
|
||||
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
Nebula may make mistakes. Please use with discretion
|
||||
</p>
|
||||
<div className="container max-w-[800px]">
|
||||
<ChatBar
|
||||
sendMessage={handleSendMessage}
|
||||
isChatStreaming={isChatStreaming}
|
||||
abortChatStream={() => {
|
||||
chatAbortController?.abort();
|
||||
setChatAbortController(undefined);
|
||||
setIsChatStreaming(false);
|
||||
// if last message is presence, remove it
|
||||
if (messages[messages.length - 1]?.type === "presence") {
|
||||
setMessages((prev) => prev.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
Nebula may make mistakes. Please use with discretion
|
||||
</p>
|
||||
</div>
|
||||
<aside className="hidden w-[360px] flex-col border-l bg-card pt-4 xl:flex">
|
||||
<div className="px-4">
|
||||
<h3 className="font-semibold text-lg tracking-tight">Context</h3>
|
||||
<p className="mb-5 text-muted-foreground text-sm">
|
||||
Provide context to Nebula for your prompts
|
||||
</p>
|
||||
</div>
|
||||
<ContextFiltersForm
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
modal={undefined}
|
||||
updateContextFilters={handleUpdateContextFilters}
|
||||
formBodyClassName="px-4"
|
||||
formActionContainerClassName="px-4 border-t-0 pt-0 bg-transparent"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
FileCode2Icon,
|
||||
MessageSquareShareIcon,
|
||||
MessagesSquareIcon,
|
||||
SquareDashedBottomCodeIcon,
|
||||
TextIcon,
|
||||
@@ -29,8 +31,11 @@ export function ChatSidebar(props: {
|
||||
return (
|
||||
<div className="flex h-full flex-col 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 href="/" className="flex items-center gap-2">
|
||||
<NebulaIcon className="size-8 text-foreground" aria-label="Nebula" />
|
||||
<span className="font-semibold text-lg tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Badge variant="secondary" className="gap-1 py-1">
|
||||
@@ -48,6 +53,13 @@ export function ChatSidebar(props: {
|
||||
|
||||
<div className="h-3" />
|
||||
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/nebula/api-reference"
|
||||
icon={FileCode2Icon}
|
||||
label="API Reference"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SidebarIconLink
|
||||
href="/chat/history"
|
||||
icon={MessagesSquareIcon}
|
||||
@@ -79,7 +91,14 @@ export function ChatSidebar(props: {
|
||||
</ScrollShadow>
|
||||
)}
|
||||
|
||||
<div className="mb-3 border-b border-dashed pt-2 pb-3">
|
||||
<div className="mb-3 border-y border-dashed py-3">
|
||||
<SidebarIconLink
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSeM3fJRyywihRZUF1fiTNKEpJ_AzAcohRwXPpLr_3zxQ6W-tg/viewform?usp=sharing"
|
||||
icon={MessageSquareShareIcon}
|
||||
label="Take our quick survey!"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/changelog"
|
||||
icon={TextIcon}
|
||||
@@ -116,6 +135,7 @@ function SidebarIconLink(props: {
|
||||
href={props.href}
|
||||
target={props.target}
|
||||
className="!justify-start !px-3 w-full gap-2.5 rounded-lg text-left"
|
||||
prefetch={false}
|
||||
>
|
||||
<props.icon className="size-4" />
|
||||
{props.label}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -15,15 +16,14 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { AutoResizeTextarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { SlidersHorizontalIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -39,9 +39,6 @@ export default function ContextFiltersButton(props: {
|
||||
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const chainIds = props.contextFilters?.chainIds;
|
||||
const contractAddresses = props.contextFilters?.contractAddresses;
|
||||
@@ -85,21 +82,22 @@ export default function ContextFiltersButton(props: {
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="p-0">
|
||||
<ContextFilterDialogContent
|
||||
contextFilters={props.contextFilters}
|
||||
isPending={updateMutation.isPending}
|
||||
updateFilters={async (data) => {
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
toast.promise(promise, {
|
||||
success: "Context filters updated",
|
||||
error: "Failed to update context filters",
|
||||
});
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">
|
||||
Context Filters
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
promise.then(() => {
|
||||
props.setContextFilters(data);
|
||||
setIsOpen(false);
|
||||
});
|
||||
<ContextFiltersForm
|
||||
contextFilters={props.contextFilters}
|
||||
updateContextFilters={props.updateContextFilters}
|
||||
setContextFilters={props.setContextFilters}
|
||||
modal={{
|
||||
close: () => setIsOpen(false),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -138,11 +136,22 @@ const formSchema = z.object({
|
||||
walletAddresses: commaSeparateListOfAddresses,
|
||||
});
|
||||
|
||||
function ContextFilterDialogContent(props: {
|
||||
export function ContextFiltersForm(props: {
|
||||
contextFilters: ContextFilters | undefined;
|
||||
updateFilters: (filters: ContextFilters | undefined) => void;
|
||||
isPending: boolean;
|
||||
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
|
||||
setContextFilters: (filters: ContextFilters | undefined) => void;
|
||||
formBodyClassName?: string;
|
||||
formActionContainerClassName?: string;
|
||||
modal:
|
||||
| {
|
||||
close: () => void;
|
||||
}
|
||||
| undefined;
|
||||
}) {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
@@ -172,26 +181,37 @@ function ContextFilterDialogContent(props: {
|
||||
.split(",")
|
||||
.filter((v) => v.trim());
|
||||
|
||||
props.updateFilters({
|
||||
const data = {
|
||||
chainIds: chainIdsArray,
|
||||
contractAddresses: contractAddressesArray,
|
||||
walletAddresses: walletAddressesArray,
|
||||
};
|
||||
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: "Context filters updated",
|
||||
error: "Failed to update context filters",
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
props.setContextFilters(data);
|
||||
props.modal?.close?.();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">
|
||||
Context Filters
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context information to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-5 px-6">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={cn("flex w-full grow flex-col")}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-5 px-6 pb-6",
|
||||
props.formBodyClassName,
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chainIds"
|
||||
@@ -200,6 +220,8 @@ function ContextFilterDialogContent(props: {
|
||||
<FormLabel>Chain IDs</FormLabel>
|
||||
<FormControl>
|
||||
<MultiNetworkSelector
|
||||
disableChainId
|
||||
className="bg-background"
|
||||
selectedChainIds={form
|
||||
.watch()
|
||||
.chainIds.split(",")
|
||||
@@ -226,12 +248,9 @@ function ContextFilterDialogContent(props: {
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of contract addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -247,25 +266,36 @@ function ContextFilterDialogContent(props: {
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of wallet addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex justify-end gap-3 border-t bg-card p-6">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button className="min-w-28 gap-2" type="submit">
|
||||
{props.isPending && <Spinner className="size-4" />}
|
||||
Update
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-end gap-3 border-t bg-card p-6",
|
||||
props.formActionContainerClassName,
|
||||
)}
|
||||
>
|
||||
{props.modal && (
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
)}
|
||||
<Button
|
||||
className="min-w-24 gap-2"
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={updateMutation.isPending}
|
||||
>
|
||||
{updateMutation.isPending && <Spinner className="size-4" />}
|
||||
Update Context
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
sendMessage: (message: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="py-10 lg:py-16">
|
||||
<div className="relative py-10">
|
||||
<FancyBorders />
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
|
||||
+16
-15
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import { FormItem } from "@/components/ui/form";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
type BackendWallet,
|
||||
useEngineBackendWalletBalance,
|
||||
@@ -34,11 +35,9 @@ import { type ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||
import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { EngineBackendWalletOptions } from "lib/engine";
|
||||
import {
|
||||
useActiveChainAsDashboardChain,
|
||||
useV5DashboardChain,
|
||||
} from "lib/v5-adapter";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import {
|
||||
DownloadIcon,
|
||||
PencilIcon,
|
||||
@@ -52,7 +51,6 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import invariant from "tiny-invariant";
|
||||
import { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
|
||||
import { prettyPrintCurrency } from "./utils";
|
||||
|
||||
@@ -89,21 +87,21 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
});
|
||||
const chain = useV5DashboardChain(chainId);
|
||||
if (!chain || !backendWalletBalance) {
|
||||
return;
|
||||
|
||||
if (!backendWalletBalance) {
|
||||
return <Skeleton className="h-5 w-32 rounded-lg" />;
|
||||
}
|
||||
|
||||
const balanceDisplay = prettyPrintCurrency({
|
||||
amount: backendWalletBalance.displayValue,
|
||||
symbol: backendWalletBalance.symbol,
|
||||
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
|
||||
});
|
||||
|
||||
const balanceComponent = (
|
||||
<Text fontWeight={backendWalletBalance.value === "0" ? "light" : "bold"}>
|
||||
{balanceDisplay}
|
||||
</Text>
|
||||
<div className="text-muted-foreground">{balanceDisplay}</div>
|
||||
);
|
||||
|
||||
const explorer = chain.blockExplorers?.[0];
|
||||
@@ -250,6 +248,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
disclosure={sendDisclosure}
|
||||
instanceUrl={instanceUrl}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
)}
|
||||
{selectedBackendWallet && deleteDisclosure.isOpen && (
|
||||
@@ -425,24 +424,28 @@ const SendFundsModal = ({
|
||||
disclosure,
|
||||
instanceUrl,
|
||||
authToken,
|
||||
chainId,
|
||||
}: {
|
||||
fromWallet: BackendWallet;
|
||||
backendWallets: BackendWallet[];
|
||||
disclosure: UseDisclosureReturn;
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) => {
|
||||
const chain = useActiveChainAsDashboardChain();
|
||||
const form = useForm<SendFundsInput>();
|
||||
const sendTokens = useEngineSendTokens({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
});
|
||||
const { idToChain } = useAllChainsData();
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl,
|
||||
address: fromWallet.address,
|
||||
authToken,
|
||||
chainId: chainId,
|
||||
});
|
||||
const chain = idToChain.get(chainId);
|
||||
const toWalletDisclosure = useDisclosure();
|
||||
|
||||
if (!backendWalletBalance) {
|
||||
@@ -450,11 +453,9 @@ const SendFundsModal = ({
|
||||
}
|
||||
|
||||
const onSubmit = async (data: SendFundsInput) => {
|
||||
invariant(chain, "chain is required");
|
||||
|
||||
const promise = sendTokens.mutateAsync(
|
||||
{
|
||||
chainId: chain.chainId,
|
||||
chainId: chainId,
|
||||
fromAddress: fromWallet.address,
|
||||
toAddress: data.toAddress,
|
||||
amount: data.amount,
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ export const prettyPrintCurrency = ({
|
||||
symbol,
|
||||
}: {
|
||||
amount?: string | number;
|
||||
symbol: string;
|
||||
symbol?: string;
|
||||
}): string => {
|
||||
const amountNumber =
|
||||
typeof amount === "string" ? Number.parseFloat(amount) : (amount ?? 0);
|
||||
return `${amountNumber.toFixed(6)} ${symbol}`;
|
||||
return `${amountNumber.toFixed(6)} ${symbol || ""}`;
|
||||
};
|
||||
|
||||
@@ -170,6 +170,7 @@ export function TeamProjectsPage(props: {
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={isCreateProjectDialogOpen}
|
||||
onOpenChange={setIsCreateProjectDialogOpen}
|
||||
teamSlug={props.team.slug}
|
||||
onCreateAndComplete={() => {
|
||||
// refresh projects
|
||||
router.refresh();
|
||||
|
||||
+14
-1
@@ -1,5 +1,8 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getProject } from "../../../../../../@/api/projects";
|
||||
import { getAPIKeyForProjectId } from "../../../../../api/lib/getAPIKeys";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
@@ -27,9 +30,19 @@ export default async function Page(props: {
|
||||
? (searchParams.interval as "day" | "week")
|
||||
: "week";
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const apiKey = await getAPIKeyForProjectId(project.id);
|
||||
if (!apiKey) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
clientId={params.project_slug}
|
||||
clientId={apiKey.key}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
|
||||
+10
@@ -60,6 +60,16 @@ function Story() {
|
||||
payConfig: "/payConfig",
|
||||
}}
|
||||
onKeyUpdated={undefined}
|
||||
rotateSecretKey={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
data: {
|
||||
secret: new Array(86).fill("x").join(""),
|
||||
secretHash: new Array(64).fill("x").join(""),
|
||||
secretMasked: "123...4567",
|
||||
},
|
||||
};
|
||||
}}
|
||||
showNebulaSettings={false}
|
||||
/>
|
||||
|
||||
|
||||
+275
-6
@@ -1,12 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
|
||||
import { SettingsCard } from "@/components/blocks/SettingsCard";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -14,13 +23,14 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ApiKey, UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
useRevokeApiKey,
|
||||
useUpdateApiKey,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
import {
|
||||
type ServiceName,
|
||||
@@ -28,8 +38,13 @@ import {
|
||||
} from "@thirdweb-dev/service-utils";
|
||||
import { format } from "date-fns";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
RefreshCcwIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,11 +62,20 @@ type EditProjectUIPaths = {
|
||||
afterDeleteRedirectTo: string;
|
||||
};
|
||||
|
||||
type RotateSecretKeyAPIReturnType = {
|
||||
data: {
|
||||
secret: string;
|
||||
secretMasked: string;
|
||||
secretHash: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function ProjectGeneralSettingsPage(props: {
|
||||
apiKey: ApiKey;
|
||||
paths: EditProjectUIPaths;
|
||||
onKeyUpdated: (() => void) | undefined;
|
||||
showNebulaSettings: boolean;
|
||||
projectId: string;
|
||||
}) {
|
||||
const updateMutation = useUpdateApiKey();
|
||||
const deleteMutation = useRevokeApiKey();
|
||||
@@ -64,6 +88,24 @@ export function ProjectGeneralSettingsPage(props: {
|
||||
paths={props.paths}
|
||||
onKeyUpdated={props.onKeyUpdated}
|
||||
showNebulaSettings={props.showNebulaSettings}
|
||||
rotateSecretKey={async () => {
|
||||
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
|
||||
pathname: "/v2/keys/rotate-secret-key",
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
projectId: props.projectId,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -84,6 +126,7 @@ interface EditApiKeyProps {
|
||||
paths: EditProjectUIPaths;
|
||||
onKeyUpdated: (() => void) | undefined;
|
||||
showNebulaSettings: boolean;
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
}
|
||||
|
||||
type UpdateAPIForm = UseFormReturn<ProjectSettingsPageFormSchema>;
|
||||
@@ -216,7 +259,10 @@ export const ProjectGeneralSettingsPageUI: React.FC<EditApiKeyProps> = (
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
<APIKeyDetails apiKey={apiKey} />
|
||||
<APIKeyDetails
|
||||
apiKey={apiKey}
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
/>
|
||||
|
||||
<AllowedDomainsSetting
|
||||
form={form}
|
||||
@@ -609,10 +655,13 @@ function EnabledServicesSetting(props: {
|
||||
|
||||
function APIKeyDetails({
|
||||
apiKey,
|
||||
rotateSecretKey,
|
||||
}: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
apiKey: ApiKey;
|
||||
}) {
|
||||
const { createdAt, updatedAt, lastAccessedAt } = apiKey;
|
||||
const [secretKeyMasked, setSecretKeyMasked] = useState(apiKey.secretMasked);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 rounded-lg border border-border bg-card px-4 py-6 lg:px-6">
|
||||
@@ -632,7 +681,7 @@ function APIKeyDetails({
|
||||
</div>
|
||||
|
||||
{/* NOTE: for very old api keys the secret might be `null`, if that's the case we skip it */}
|
||||
{apiKey.secretMasked && (
|
||||
{secretKeyMasked && (
|
||||
<div>
|
||||
<h3>Secret Key</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
@@ -641,8 +690,17 @@ function APIKeyDetails({
|
||||
the time of creation for the full secret key.
|
||||
</p>
|
||||
|
||||
<div className="max-w-[350px] rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm">
|
||||
{apiKey.secretMasked}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm lg:w-[350px]">
|
||||
{secretKeyMasked}
|
||||
</div>
|
||||
|
||||
<RotateSecretKeyButton
|
||||
rotateSecretKey={rotateSecretKey}
|
||||
onSuccess={(data) => {
|
||||
setSecretKeyMasked(data.data.secretMasked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -726,3 +784,214 @@ function DeleteProject(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RotateSecretKeyButton(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true);
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
if (!isModalCloseAllowed) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(v);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto gap-2 rounded-lg bg-background px-4 py-3"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
Rotate Secret Key
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent
|
||||
className="overflow-hidden p-0"
|
||||
dialogCloseClassName={cn(!isModalCloseAllowed && "hidden")}
|
||||
>
|
||||
<RotateSecretKeyModalContent
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
closeModal={() => {
|
||||
setIsOpen(false);
|
||||
setIsModalCloseAllowed(true);
|
||||
}}
|
||||
disableModalClose={() => setIsModalCloseAllowed(false)}
|
||||
onSuccess={props.onSuccess}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
type RotateSecretKeyScreen =
|
||||
| { id: "initial" }
|
||||
| { id: "save-newkey"; secretKey: string };
|
||||
|
||||
function RotateSecretKeyModalContent(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
closeModal: () => void;
|
||||
disableModalClose: () => void;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
|
||||
id: "initial",
|
||||
});
|
||||
|
||||
if (screen.id === "save-newkey") {
|
||||
return (
|
||||
<SaveNewKeyScreen
|
||||
secretKey={screen.secretKey}
|
||||
closeModal={props.closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "initial") {
|
||||
return (
|
||||
<RotateSecretKeyInitialScreen
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
onSuccess={(data) => {
|
||||
props.disableModalClose();
|
||||
props.onSuccess(data);
|
||||
setScreen({ id: "save-newkey", secretKey: data.data.secret });
|
||||
}}
|
||||
closeModal={props.closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function RotateSecretKeyInitialScreen(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||
const rotateKeyMutation = useMutation({
|
||||
mutationFn: props.rotateSecretKey,
|
||||
onSuccess: (data) => {
|
||||
props.onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
toast.error("Failed to rotate secret key");
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rotate Secret Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Current secret key will stop working</AlertTitle>
|
||||
<AlertDescription>
|
||||
Rotating the secret key will invalidate the current secret key and
|
||||
generate a new one. This action is irreversible.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
checked={isConfirmed}
|
||||
onCheckedChange={(v) => setIsConfirmed(!!v)}
|
||||
/>
|
||||
I understand the consequences of rotating the secret key
|
||||
</CheckboxWithLabel>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-6">
|
||||
<Button variant="outline" onClick={props.closeModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
disabled={!isConfirmed || rotateKeyMutation.isPending}
|
||||
onClick={() => {
|
||||
rotateKeyMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{rotateKeyMutation.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
)}
|
||||
Rotate Secret Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveNewKeyScreen(props: {
|
||||
secretKey: string;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const [isSecretStored, setIsSecretStored] = useState(false);
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<div className="flex flex-col p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save New Secret Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<CopyTextButton
|
||||
textToCopy={props.secretKey}
|
||||
className="!h-auto w-full justify-between bg-card px-3 py-3 font-mono"
|
||||
textToShow={props.secretKey}
|
||||
copyIconPosition="right"
|
||||
tooltip="Copy Secret Key"
|
||||
/>
|
||||
<div className="h-4" />
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Do not share or expose your secret key</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="mb-5">
|
||||
Secret keys cannot be recovered. If you lose your secret key, you
|
||||
will need to rotate the secret key or create a new Project.
|
||||
</div>
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
checked={isSecretStored}
|
||||
onCheckedChange={(v) => {
|
||||
setIsSecretStored(!!v);
|
||||
}}
|
||||
/>
|
||||
I confirm that I've securely stored my secret key
|
||||
</CheckboxWithLabel>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!isSecretStored}
|
||||
onClick={props.closeModal}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -9,9 +9,10 @@ export function ProjectGeneralSettingsPageForTeams(props: {
|
||||
team: Team;
|
||||
project_slug: string;
|
||||
apiKey: ApiKey;
|
||||
projectId: string;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const { team, project_slug, apiKey } = props;
|
||||
const { team, project_slug, apiKey, projectId } = props;
|
||||
const projectLayout = `/team/${team.slug}/${project_slug}`;
|
||||
|
||||
// TODO - add a Project Image form field on this page
|
||||
@@ -19,6 +20,7 @@ export function ProjectGeneralSettingsPageForTeams(props: {
|
||||
return (
|
||||
<ProjectGeneralSettingsPage
|
||||
apiKey={apiKey}
|
||||
projectId={projectId}
|
||||
paths={{
|
||||
aaConfig: `${projectLayout}/connect/account-abstraction/settings`,
|
||||
inAppConfig: `${projectLayout}/connect/in-app-wallets/settings`,
|
||||
|
||||
@@ -31,6 +31,7 @@ export default async function Page(props: {
|
||||
<ProjectGeneralSettingsPageForTeams
|
||||
apiKey={apiKey}
|
||||
project_slug={project_slug}
|
||||
projectId={project.id}
|
||||
team={team}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -65,6 +65,11 @@ export function TeamHeaderLoggedIn(props: {
|
||||
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={createProjectDialogState.isOpen}
|
||||
teamSlug={
|
||||
createProjectDialogState.isOpen
|
||||
? createProjectDialogState.team.slug
|
||||
: undefined
|
||||
}
|
||||
onOpenChange={() =>
|
||||
setCreateProjectDialogState({
|
||||
isOpen: false,
|
||||
|
||||
+8
-6
@@ -25,7 +25,7 @@ import { CircleAlertIcon, ExternalLinkIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import { ZERO_ADDRESS, getContract } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import {
|
||||
deployContractfromDeployMetadata,
|
||||
@@ -567,11 +567,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
const contractAddr = await deployMutation.mutateAsync(formData);
|
||||
|
||||
// send verification request - no need to await
|
||||
verifyContract({
|
||||
address: contractAddr,
|
||||
chain: walletChain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
verifyContract(
|
||||
getContract({
|
||||
address: contractAddr,
|
||||
chain: walletChain,
|
||||
client: thirdwebClient,
|
||||
}),
|
||||
);
|
||||
|
||||
trackEvent({
|
||||
category: "custom-contract",
|
||||
|
||||
@@ -20,7 +20,10 @@ export async function InAppWalletAnalytics({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}).catch(() => null);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -57,20 +57,18 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
}, [validTeamPlan]);
|
||||
|
||||
const growthCardCta: CtaLink | undefined = useMemo(() => {
|
||||
const trialTitle = "Claim your 1-month free";
|
||||
|
||||
switch (validTeamPlan) {
|
||||
// free > growth
|
||||
case "free": {
|
||||
return {
|
||||
label: team.growthTrialEligible ? trialTitle : "Get started",
|
||||
label: "Get started",
|
||||
};
|
||||
}
|
||||
|
||||
// starter > growth
|
||||
case "starter": {
|
||||
return {
|
||||
label: team.growthTrialEligible ? trialTitle : "Upgrade",
|
||||
label: "Upgrade",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +85,7 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [team, validTeamPlan]);
|
||||
}, [validTeamPlan]);
|
||||
|
||||
const proCta: CtaLink | undefined = useMemo(() => {
|
||||
// pro > pro
|
||||
@@ -138,20 +136,16 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
title: growthCardCta.label,
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: team.growthTrialEligible
|
||||
? "claimGrowthTrial"
|
||||
: "growthPlan",
|
||||
label: "growthPlan",
|
||||
},
|
||||
variant: "default",
|
||||
hint: team.growthTrialEligible
|
||||
? "Your free trial will end after 30 days."
|
||||
: undefined,
|
||||
hint: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
canTrialGrowth={team.growthTrialEligible || false}
|
||||
canTrialGrowth={false}
|
||||
// upsell growth plan if user is on free plan
|
||||
highlighted={validTeamPlan === "free"}
|
||||
highlighted={validTeamPlan === "free" || validTeamPlan === "starter"}
|
||||
teamSlug={team.slug}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { redirectToBillingPortal, redirectToCheckout } from "@/actions/billing";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { TeamSubscription } from "@/api/team-subscription";
|
||||
import {} from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {} from "@chakra-ui/react";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { PlanInfoCard } from "../../../../app/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard";
|
||||
|
||||
@@ -53,6 +53,7 @@ function Story(props: {
|
||||
createKeyMutation={mutation}
|
||||
prefill={props.prefill}
|
||||
enableNebulaServiceByDefault={false}
|
||||
teamSlug="foo"
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { Project } from "@/api/projects";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import {
|
||||
type ApiKey,
|
||||
type CreateKeyInput,
|
||||
@@ -30,10 +33,10 @@ import {
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { type UseMutationResult, useQuery } from "@tanstack/react-query";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -54,6 +57,7 @@ export type CreateAPIKeyDialogProps = {
|
||||
onCreateAndComplete?: () => void;
|
||||
prefill?: CreateAPIKeyPrefillOptions;
|
||||
enableNebulaServiceByDefault: boolean;
|
||||
teamSlug: string | undefined;
|
||||
};
|
||||
|
||||
const CreateAPIKeyDialog = (props: CreateAPIKeyDialogProps) => {
|
||||
@@ -78,6 +82,7 @@ export const CreateAPIKeyDialogUI = (props: {
|
||||
>;
|
||||
prefill?: CreateAPIKeyPrefillOptions;
|
||||
enableNebulaServiceByDefault: boolean;
|
||||
teamSlug: string | undefined;
|
||||
}) => {
|
||||
const [screen, setScreen] = useState<
|
||||
{ id: "create" } | { id: "api-details"; key: ApiKey }
|
||||
@@ -97,7 +102,7 @@ export const CreateAPIKeyDialogUI = (props: {
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="z-[10001] p-0"
|
||||
className="z-[10001] overflow-hidden p-0"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
dialogCloseClassName={screen.id === "api-details" ? "hidden" : ""}
|
||||
>
|
||||
@@ -116,6 +121,7 @@ export const CreateAPIKeyDialogUI = (props: {
|
||||
{screen.id === "api-details" && (
|
||||
<APIKeyDetails
|
||||
apiKey={screen.key}
|
||||
teamSlug={props.teamSlug}
|
||||
onComplete={() => {
|
||||
onOpenChange(false);
|
||||
setScreen({ id: "create" });
|
||||
@@ -402,9 +408,35 @@ function DomainsAlert(props: {
|
||||
function APIKeyDetails(props: {
|
||||
apiKey: ApiKey;
|
||||
onComplete: () => void;
|
||||
teamSlug: string | undefined;
|
||||
}) {
|
||||
const { apiKey } = props;
|
||||
const [secretStored, setSecretStored] = useState(false);
|
||||
const router = useDashboardRouter();
|
||||
|
||||
// get the project.slug for the apiKey to render "View Project" button
|
||||
const projectQuery = useQuery({
|
||||
queryKey: ["project", props.teamSlug, apiKey.id],
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy<{
|
||||
result: Project[];
|
||||
}>({
|
||||
method: "GET",
|
||||
pathname: `/v1/teams/${props.teamSlug}/projects`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const projects = res.data.result;
|
||||
const project = projects.find((p) => p.publishableKey === apiKey.key);
|
||||
return project || null;
|
||||
},
|
||||
enabled: !!props.teamSlug,
|
||||
});
|
||||
|
||||
const projectSlug = projectQuery.data?.slug;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -454,7 +486,7 @@ function APIKeyDetails(props: {
|
||||
<AlertDescription>
|
||||
<div className="mb-5">
|
||||
Secret keys cannot be recovered. If you lose your secret key, you
|
||||
will need to create a project
|
||||
will need to rotate the secret key or create a new Project.
|
||||
</div>
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
@@ -474,10 +506,31 @@ function APIKeyDetails(props: {
|
||||
type="button"
|
||||
onClick={props.onComplete}
|
||||
disabled={!secretStored}
|
||||
variant="outline"
|
||||
className="min-w-28 gap-2"
|
||||
>
|
||||
Complete
|
||||
{props.teamSlug ? "Close" : "Complete"}
|
||||
</Button>
|
||||
|
||||
{props.teamSlug && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!projectSlug) {
|
||||
return;
|
||||
}
|
||||
router.push(`/team/${props.teamSlug}/${projectSlug}`);
|
||||
}}
|
||||
disabled={!secretStored || !projectSlug}
|
||||
className="min-w-28 gap-2"
|
||||
>
|
||||
View Project
|
||||
{projectQuery.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
billingEmail: "[email protected]",
|
||||
growthTrialEligible: true,
|
||||
growthTrialEligible: false,
|
||||
enabledScopes: [
|
||||
"pay",
|
||||
"storage",
|
||||
|
||||
@@ -13,28 +13,28 @@
|
||||
"fix": "eslint ./src --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abstract-foundation/agw-client": "^1.0.1",
|
||||
"@abstract-foundation/agw-react": "^1.0.1",
|
||||
"@abstract-foundation/agw-client": "^1.3.0",
|
||||
"@abstract-foundation/agw-react": "^1.4.2",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@thirdweb-dev/engine": "^0.0.17",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@thirdweb-dev/engine": "^0.0.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
"prettier": "3.3.3",
|
||||
"react": "19.0.0",
|
||||
@@ -48,15 +48,15 @@
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"postcss": "8.4.49",
|
||||
"postcss": "8.5.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const getEcosystem = () => {
|
||||
return "ecosystem.catlovers";
|
||||
}
|
||||
// prod ecosystem
|
||||
return "ecosystem.new-age";
|
||||
return "ecosystem.thirdweb-engs";
|
||||
};
|
||||
|
||||
export function EcosystemConnectEmbed(
|
||||
|
||||
+14
-14
@@ -21,21 +21,21 @@
|
||||
"@dirtycajunrice/klee": "^1.0.6",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "15.1.0",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@next/mdx": "15.1.6",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"posthog-js": "1.67.1",
|
||||
@@ -53,11 +53,11 @@
|
||||
"typedoc-better-json": "0.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.3",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@types/flexsearch": "^0.7.6",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/react-html-parser": "^2.0.6",
|
||||
"@types/tryghost__content-api": "^1.3.16",
|
||||
@@ -68,12 +68,12 @@
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-mdx": "^3.1.5",
|
||||
"eslint-plugin-svg-jsx": "^1.2.4",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"knip": "5.41.0",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"knip": "5.43.5",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"postcss": "8.5.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
We are rolling out Nebula approval in batches daily - stay tuned!
|
||||
|
||||
### Which contracts does Nebula support?
|
||||
Nebula supports the read and write context of any verified contract or any contract on the thirdweb dashboard. For deploying through Nebula, supported contracts include [Token - ERC20](https://thirdweb.com/thirdweb.eth/TokenERC20), [NFT Collection - ERC721](https://thirdweb.com/thirdweb.eth/TokenERC721), [Edition - ERC1155](https://thirdweb.com/thirdweb.eth/TokenERC1155), and [Split](https://thirdweb.com/thirdweb.eth/Split) contracts.
|
||||
|
||||
If you have a published contract you would like to enable for deployment through Nebula, please [contact us](https://thirdweb.com/contact-us).
|
||||
|
||||
### Does Nebula have memory from past conversations?
|
||||
Nebula retains memory within the confines of a session.
|
||||
|
||||
@@ -16,4 +21,4 @@ Nebula is not currently open source. We are exploring open sourcing Nebula in th
|
||||
|
||||
### When will pricing be available for Nebula?
|
||||
|
||||
Pricing options will be available in beta. Nebula is still being tested in alpha.
|
||||
Pricing options will be available in beta. Nebula is still being tested in alpha.
|
||||
|
||||
@@ -60,13 +60,12 @@ npm install thirdweb
|
||||
Setup environmental variables.
|
||||
|
||||
<Callout variant="warning" title="Storing Secret Keys">
|
||||
Ensure you keep your secret key and private key safe and do not expose it in your codebase. We recommend using a
|
||||
Ensure you keep your secret key safe and do not expose it in your codebase. We recommend using a
|
||||
secret key manager such as [AWS Secret Manager](https://aws.amazon.com/secrets-manager/) or [Google Secret Manager](https://cloud.google.com/secret-manager).
|
||||
</Callout>
|
||||
|
||||
```jsx
|
||||
THIRDWEB_SECRET_KEY=your_thirdweb_secret_key
|
||||
EOA_PRIVATE_KEY=your_wallet_private_key
|
||||
```
|
||||
</Step>
|
||||
|
||||
@@ -88,15 +87,15 @@ import {
|
||||
This function processes the API's response and executes blockchain transactions.
|
||||
|
||||
```jsx
|
||||
import { generateAccount } from "thirdweb/wallets";
|
||||
|
||||
async function handleNebulaResponse(response) {
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const account = privateKeyToAccount({
|
||||
client,
|
||||
privateKey: process.env.EOA_PRIVATE_KEY,
|
||||
});
|
||||
// You can use any wallet- see https://portal.thirdweb.com/typescript/v5/supported-wallets
|
||||
const account = await generateAccount({ client });
|
||||
|
||||
if (response.actions && response.actions.length > 0) {
|
||||
const action = response.actions[0];
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# OpenAI
|
||||
|
||||
### Chat Completions API
|
||||
|
||||
Compatible with OpenAI’s Chat Completion API to process chat history and generate context-aware responses.
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="https://nebula-api.thirdweb.com",
|
||||
api_key="{{THIRDWEB_SECRET_KEY}}",
|
||||
)
|
||||
chat_completion = client.chat.completions.create(
|
||||
model="t0",
|
||||
messages=[{"role": "user", "content": "Hello Nebula!"}],
|
||||
stream=False,
|
||||
extra_body={ "context": { "wallet_address": "0x..." }}
|
||||
)
|
||||
|
||||
print(chat_completion)
|
||||
```
|
||||
|
||||
### Models API
|
||||
|
||||
Compatible with OpenAI’s Models API to list out models used.
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
# https://nebula-api.thirdweb.com/models
|
||||
|
||||
client = OpenAI(
|
||||
base_url="https://nebula-api.thirdweb.com/",
|
||||
api_key="",
|
||||
)
|
||||
|
||||
models = client.models.list()
|
||||
print(models)
|
||||
```
|
||||
@@ -2,11 +2,7 @@ import { Callout } from '@doc'
|
||||
|
||||
# Plugins
|
||||
|
||||
- [OpenAI](/nebula/plugins/openai)
|
||||
- [Eliza](/nebula/plugins/eliza)
|
||||
|
||||
<Callout variant='info' title='Coming Soon'>
|
||||
|
||||
We are actively developing plugins to develop with other frameworks and languages.
|
||||
|
||||
If you have a specific request, please [contact us](https://thirdweb.com/contact).
|
||||
|
||||
</Callout>
|
||||
We are actively adding more integrations, for any specific requests please [contact us](https://thirdweb.com/contact).
|
||||
@@ -110,10 +110,14 @@ export const sidebar: SideBar = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Plugins",
|
||||
name: "Plugins & Integrations",
|
||||
href: "/nebula/plugins",
|
||||
icon: <Blocks />,
|
||||
links: [
|
||||
{
|
||||
name: "OpenAI",
|
||||
href: "/nebula/plugins/openai",
|
||||
},
|
||||
{
|
||||
name: "Eliza",
|
||||
href: "/nebula/plugins/eliza",
|
||||
|
||||
+12
-13
@@ -11,23 +11,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@noble/curves": "1.7.0",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "7.54.2",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^1.7.1",
|
||||
"sonner": "^1.7.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
@@ -35,19 +34,19 @@
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.3",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"knip": "5.41.0",
|
||||
"postcss": "8.4.49",
|
||||
"knip": "5.43.5",
|
||||
"postcss": "8.5.1",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
import { redirect } from "@/lib/redirect";
|
||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||
import { cookies } from "next/headers";
|
||||
import { toHex } from "thirdweb";
|
||||
import {
|
||||
type GenerateLoginPayloadParams,
|
||||
type VerifyLoginPayloadParams,
|
||||
createAuth,
|
||||
} from "thirdweb/auth";
|
||||
import { privateKeyToAccount } from "thirdweb/wallets";
|
||||
import { privateKeyToAccount, randomPrivateKey } from "thirdweb/wallets";
|
||||
import { client } from "./client";
|
||||
|
||||
const privateKey = process.env.THIRDWEB_ADMIN_PRIVATE_KEY;
|
||||
@@ -20,7 +18,7 @@ const thirdwebAuth = createAuth({
|
||||
// Fun little hack to make vercel happy at build time
|
||||
adminAccount: privateKeyToAccount({
|
||||
client,
|
||||
privateKey: privateKey || toHex(secp256k1.utils.randomPrivateKey()),
|
||||
privateKey: privateKey || randomPrivateKey(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+4
-4
@@ -41,11 +41,11 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@changesets/changelog-github": "0.5.0",
|
||||
"@changesets/cli": "2.27.10",
|
||||
"@changesets/cli": "2.27.12",
|
||||
"@manypkg/get-packages": "2.2.2",
|
||||
"@types/node": "22.10.2",
|
||||
"turbo": "2.3.3",
|
||||
"typescript": "5.7.2"
|
||||
"@types/node": "22.10.10",
|
||||
"turbo": "2.3.4",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @thirdweb-dev/service-utils
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6101](https://github.com/thirdweb-dev/js/pull/6101) [`e8892b1`](https://github.com/thirdweb-dev/js/commit/e8892b152f70d3024dfbefcddcfa74a69a12bdd0) Thanks [@jnsdls](https://github.com/jnsdls)! - pass `team` instead of `project` to `rateLimit`
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6084](https://github.com/thirdweb-dev/js/pull/6084) [`b5e327e`](https://github.com/thirdweb-dev/js/commit/b5e327e5ec745ce1beb9a79404fa5b11d0c5588e) Thanks [@jnsdls](https://github.com/jnsdls)! - add `billingPlanVersion` to `TeamResponse`
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6079](https://github.com/thirdweb-dev/js/pull/6079) [`1616b7f`](https://github.com/thirdweb-dev/js/commit/1616b7f6198d43fc48a1269b1cca93958cbf7dba) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6047](https://github.com/thirdweb-dev/js/pull/6047) [`7a8e4e5`](https://github.com/thirdweb-dev/js/commit/7a8e4e5f861070858b4716ac95ba5bc60c284924) Thanks [@arcoraven](https://github.com/arcoraven)! - Export usageV2 util functions
|
||||
|
||||
- [#6050](https://github.com/thirdweb-dev/js/pull/6050) [`fb4d0a2`](https://github.com/thirdweb-dev/js/commit/fb4d0a26f162a098878a1a63a61fef6033bcd5e9) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] replace client_id with project_id in usage_v2
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/service-utils",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"type": "module",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
@@ -50,10 +50,10 @@
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20241216.0",
|
||||
"@types/node": "22.10.2",
|
||||
"typescript": "5.7.2",
|
||||
"vitest": "2.1.8"
|
||||
"@cloudflare/workers-types": "4.20250124.3",
|
||||
"@types/node": "22.10.10",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "biome format ./src --write",
|
||||
|
||||
@@ -11,6 +11,8 @@ import type { AuthorizationResult } from "../core/authorize/types.js";
|
||||
import type { CoreAuthInput } from "../core/types.js";
|
||||
|
||||
export * from "./usage.js";
|
||||
export * from "./usageV2.js";
|
||||
export * from "../core/usageV2.js";
|
||||
export * from "../core/services.js";
|
||||
export * from "../core/rateLimit/index.js";
|
||||
|
||||
|
||||
@@ -45,11 +45,12 @@ export type TeamResponse = {
|
||||
slug: string;
|
||||
image: string | null;
|
||||
billingPlan: "free" | "starter" | "growth" | "pro";
|
||||
billingPlanVersion: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date | null;
|
||||
billingEmail: string | null;
|
||||
billingStatus: "noPayment" | "validPayment" | "invalidPayment" | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: ServiceName[];
|
||||
};
|
||||
|
||||
@@ -163,24 +164,3 @@ export async function fetchTeamAndProject(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRateLimitedAt(
|
||||
projectId: string,
|
||||
config: CoreServiceConfig,
|
||||
): Promise<void> {
|
||||
const { apiUrl, serviceScope: scope, serviceApiKey } = config;
|
||||
|
||||
const url = `${apiUrl}/usage/rateLimit`;
|
||||
|
||||
await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"x-service-api-key": serviceApiKey,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiKeyId: projectId, // projectId is the apiKeyId
|
||||
scope,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
type CoreServiceConfig,
|
||||
type ProjectResponse,
|
||||
updateRateLimitedAt,
|
||||
} from "../api.js";
|
||||
import type { CoreServiceConfig, TeamResponse } from "../api.js";
|
||||
import type { RateLimitResult } from "./types.js";
|
||||
|
||||
const RATE_LIMIT_WINDOW_SECONDS = 10;
|
||||
@@ -14,7 +10,7 @@ type IRedis = {
|
||||
};
|
||||
|
||||
export async function rateLimit(args: {
|
||||
project?: ProjectResponse;
|
||||
team: TeamResponse;
|
||||
limitPerSecond: number;
|
||||
serviceConfig: CoreServiceConfig;
|
||||
redis: IRedis;
|
||||
@@ -25,13 +21,7 @@ export async function rateLimit(args: {
|
||||
*/
|
||||
sampleRate?: number;
|
||||
}): Promise<RateLimitResult> {
|
||||
const {
|
||||
project,
|
||||
limitPerSecond,
|
||||
serviceConfig,
|
||||
redis,
|
||||
sampleRate = 1.0,
|
||||
} = args;
|
||||
const { team, limitPerSecond, serviceConfig, redis, sampleRate = 1.0 } = args;
|
||||
|
||||
const shouldSampleRequest = Math.random() < sampleRate;
|
||||
if (!shouldSampleRequest) {
|
||||
@@ -57,7 +47,7 @@ export async function rateLimit(args: {
|
||||
const timestampWindow =
|
||||
Math.floor(Date.now() / (1000 * RATE_LIMIT_WINDOW_SECONDS)) *
|
||||
RATE_LIMIT_WINDOW_SECONDS;
|
||||
const key = `rate-limit:${serviceScope}:${project?.id}:${timestampWindow}`;
|
||||
const key = `rate-limit:${serviceScope}:${team.id}:${timestampWindow}`;
|
||||
|
||||
// Increment and get the current request count in this window.
|
||||
const requestCount = await redis.incr(key);
|
||||
@@ -71,17 +61,6 @@ export async function rateLimit(args: {
|
||||
limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
|
||||
|
||||
if (requestCount > limitPerWindow) {
|
||||
/**
|
||||
* Report rate limit hits.
|
||||
* Only track rate limit when its hit for the first time.
|
||||
* Not waiting for tracking to complete as user doesn't need to wait.
|
||||
*/
|
||||
if (requestCount === limitPerWindow + 1 && project?.id) {
|
||||
updateRateLimitedAt(project.id, serviceConfig).catch(() => {
|
||||
// no-op
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
rateLimited: true,
|
||||
requestCount,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { validProjectResponse, validServiceConfig } from "../../mocks.js";
|
||||
import { updateRateLimitedAt } from "../api.js";
|
||||
import { validServiceConfig, validTeamResponse } from "../../mocks.js";
|
||||
import { rateLimit } from "./index.js";
|
||||
|
||||
const mockRedis = {
|
||||
@@ -8,11 +7,6 @@ const mockRedis = {
|
||||
expire: vi.fn(),
|
||||
};
|
||||
|
||||
// Mocking the updateRateLimitedAt function
|
||||
vi.mock("../../../src/core/api", () => ({
|
||||
updateRateLimitedAt: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
describe("rateLimit", () => {
|
||||
beforeEach(() => {
|
||||
// Clear mock function calls and reset any necessary state.
|
||||
@@ -27,7 +21,7 @@ describe("rateLimit", () => {
|
||||
|
||||
it("should not rate limit if service scope is not in rate limits", async () => {
|
||||
const result = await rateLimit({
|
||||
project: validProjectResponse,
|
||||
team: validTeamResponse,
|
||||
limitPerSecond: 0,
|
||||
serviceConfig: validServiceConfig,
|
||||
redis: mockRedis,
|
||||
@@ -44,7 +38,7 @@ describe("rateLimit", () => {
|
||||
mockRedis.incr.mockResolvedValue(50); // Current count is 50 requests in 10 seconds.
|
||||
|
||||
const result = await rateLimit({
|
||||
project: validProjectResponse,
|
||||
team: validTeamResponse,
|
||||
limitPerSecond: 5,
|
||||
serviceConfig: validServiceConfig,
|
||||
redis: mockRedis,
|
||||
@@ -55,7 +49,7 @@ describe("rateLimit", () => {
|
||||
requestCount: 50,
|
||||
rateLimit: 50,
|
||||
});
|
||||
expect(updateRateLimitedAt).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRedis.expire).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -63,7 +57,7 @@ describe("rateLimit", () => {
|
||||
mockRedis.incr.mockResolvedValue(51);
|
||||
|
||||
const result = await rateLimit({
|
||||
project: validProjectResponse,
|
||||
team: validTeamResponse,
|
||||
limitPerSecond: 5,
|
||||
serviceConfig: validServiceConfig,
|
||||
redis: mockRedis,
|
||||
@@ -77,7 +71,7 @@ describe("rateLimit", () => {
|
||||
errorMessage: `You've exceeded your storage rate limit at 5 reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
|
||||
errorCode: "RATE_LIMIT_EXCEEDED",
|
||||
});
|
||||
expect(updateRateLimitedAt).toHaveBeenCalled();
|
||||
|
||||
expect(mockRedis.expire).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -85,7 +79,7 @@ describe("rateLimit", () => {
|
||||
mockRedis.incr.mockResolvedValue(1);
|
||||
|
||||
const result = await rateLimit({
|
||||
project: validProjectResponse,
|
||||
team: validTeamResponse,
|
||||
limitPerSecond: 5,
|
||||
serviceConfig: validServiceConfig,
|
||||
redis: mockRedis,
|
||||
@@ -104,7 +98,7 @@ describe("rateLimit", () => {
|
||||
vi.spyOn(global.Math, "random").mockReturnValue(0.08);
|
||||
|
||||
const result = await rateLimit({
|
||||
project: validProjectResponse,
|
||||
team: validTeamResponse,
|
||||
limitPerSecond: 5,
|
||||
serviceConfig: validServiceConfig,
|
||||
redis: mockRedis,
|
||||
@@ -127,7 +121,7 @@ describe("rateLimit", () => {
|
||||
vi.spyOn(global.Math, "random").mockReturnValue(0.15);
|
||||
|
||||
const result = await rateLimit({
|
||||
project: validProjectResponse,
|
||||
team: validTeamResponse,
|
||||
limitPerSecond: 5,
|
||||
serviceConfig: validServiceConfig,
|
||||
redis: mockRedis,
|
||||
|
||||
@@ -21,9 +21,9 @@ export interface UsageV2Event {
|
||||
*/
|
||||
team_id: string;
|
||||
/**
|
||||
* The client ID, if available.
|
||||
* The project ID, if available.
|
||||
*/
|
||||
client_id?: string;
|
||||
project_id?: string;
|
||||
/**
|
||||
* The SDK name, if available.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ export type {
|
||||
TeamResponse,
|
||||
} from "./core/api.js";
|
||||
|
||||
export { fetchTeamAndProject, updateRateLimitedAt } from "./core/api.js";
|
||||
export { fetchTeamAndProject } from "./core/api.js";
|
||||
|
||||
export {
|
||||
authorizeBundleId,
|
||||
|
||||
@@ -42,6 +42,7 @@ export const validTeamResponse: TeamResponse = {
|
||||
createdAt: new Date("2024-06-01"),
|
||||
updatedAt: new Date("2024-06-01"),
|
||||
billingPlan: "free",
|
||||
billingPlanVersion: 1,
|
||||
billingEmail: "[email protected]",
|
||||
billingStatus: "noPayment",
|
||||
growthTrialEligible: false,
|
||||
|
||||
@@ -12,9 +12,13 @@ import type {
|
||||
} from "../core/authorize/index.js";
|
||||
import type { AuthorizationResult } from "../core/authorize/types.js";
|
||||
import type { CoreAuthInput } from "../core/types.js";
|
||||
|
||||
export * from "./usageV2.js";
|
||||
export * from "../core/usage.js";
|
||||
export * from "../core/usageV2.js";
|
||||
export * from "../core/rateLimit/index.js";
|
||||
export * from "../core/services.js";
|
||||
|
||||
type NodeServiceConfig = CoreServiceConfig;
|
||||
|
||||
export type AuthInput = CoreAuthInput & {
|
||||
|
||||
@@ -90,7 +90,7 @@ export class UsageV2Producer {
|
||||
team_id: event.team_id.startsWith("team_")
|
||||
? event.team_id.slice(5)
|
||||
: event.team_id,
|
||||
client_id: event.client_id,
|
||||
project_id: event.project_id,
|
||||
sdk_name: event.sdk_name,
|
||||
sdk_platform: event.sdk_platform,
|
||||
sdk_version: event.sdk_version,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{
|
||||
"name": "thirdweb (cjs)",
|
||||
"path": "./dist/cjs/exports/thirdweb.js",
|
||||
"limit": "120 kB"
|
||||
"limit": "130 kB"
|
||||
},
|
||||
{
|
||||
"name": "thirdweb (minimal + tree-shaking)",
|
||||
@@ -19,7 +19,7 @@
|
||||
{
|
||||
"name": "thirdweb/chains (tree-shaking)",
|
||||
"path": "./dist/esm/exports/chains.js",
|
||||
"limit": "600 B",
|
||||
"limit": "650 B",
|
||||
"import": "{ ethereum }"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,96 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.87.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#6082](https://github.com/thirdweb-dev/js/pull/6082) [`4550bb2`](https://github.com/thirdweb-dev/js/commit/4550bb26632b88964a298835575af152a355bccd) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Support Account and Wallet headless components in react native
|
||||
|
||||
You can now use the Account and Wallet headless components in react native, this lets you build your own UI, styling it however you want, but letting the components handle the logic.
|
||||
|
||||
Example Account components usage:
|
||||
|
||||
```tsx
|
||||
<AccountProvider address={account.address} client={client}>
|
||||
/* avatar */
|
||||
<AccountAvatar
|
||||
loadingComponent={
|
||||
<AccountBlobbie size={92} style={{ borderRadius: 100 }} />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie size={92} style={{ borderRadius: 100 }} />
|
||||
}
|
||||
style={{
|
||||
width: 92,
|
||||
height: 92,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
/* address */
|
||||
<AccountAddress
|
||||
style={{ fontSize: 16, color: Colors.secondary }}
|
||||
formatFn={shortenAddress}
|
||||
/>
|
||||
/* balance */
|
||||
<AccountBalance
|
||||
showBalanceInFiat={"USD"}
|
||||
chain={chain}
|
||||
loadingComponent={
|
||||
<ActivityIndicator size="large" color={Colors.accent} />
|
||||
}
|
||||
fallbackComponent={
|
||||
<Text className="text-primary">Failed to load balance</Text>
|
||||
}
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: 48,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
/>
|
||||
</AccountProvider>
|
||||
```
|
||||
|
||||
Example Wallet components usage:
|
||||
|
||||
```tsx
|
||||
<WalletProvider id={"io.metamask"}>
|
||||
<WalletIcon width={32} height={32} />
|
||||
<WalletName style={{ fontSize: 16, color: Colors.primary }} />
|
||||
</WalletProvider>
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6079](https://github.com/thirdweb-dev/js/pull/6079) [`1616b7f`](https://github.com/thirdweb-dev/js/commit/1616b7f6198d43fc48a1269b1cca93958cbf7dba) Thanks [@jnsdls](https://github.com/jnsdls)! - export `randomPrivateKey` from `thirdweb/wallets`
|
||||
|
||||
- [#6076](https://github.com/thirdweb-dev/js/pull/6076) [`1401f8d`](https://github.com/thirdweb-dev/js/commit/1401f8d0393bbea149e9e09ba686416fc0a7b4f3) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix autoconnection of inapp wallets in react native
|
||||
|
||||
- [#6079](https://github.com/thirdweb-dev/js/pull/6079) [`1616b7f`](https://github.com/thirdweb-dev/js/commit/1616b7f6198d43fc48a1269b1cca93958cbf7dba) Thanks [@jnsdls](https://github.com/jnsdls)! - updated dependencies
|
||||
|
||||
## 5.86.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6074](https://github.com/thirdweb-dev/js/pull/6074) [`94e2d5b`](https://github.com/thirdweb-dev/js/commit/94e2d5b8ddd90c0eaa985d02b08b9afa9f8e6676) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix account linking for ecosystem smart wallets
|
||||
|
||||
## 5.86.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6072](https://github.com/thirdweb-dev/js/pull/6072) [`2cebb42`](https://github.com/thirdweb-dev/js/commit/2cebb420e6a381cf41a48e17762f6c655ebe7d8c) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix showing link profiles for ecosystem smart wallets
|
||||
|
||||
## 5.86.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6068](https://github.com/thirdweb-dev/js/pull/6068) [`6c277ae`](https://github.com/thirdweb-dev/js/commit/6c277ae6764dbf1a9218fbd5d278ffb5e3dbde1f) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Respect passed in chain when connecting to inapp wallet with wallet strategy
|
||||
|
||||
## 5.86.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6057](https://github.com/thirdweb-dev/js/pull/6057) [`b6f189c`](https://github.com/thirdweb-dev/js/commit/b6f189c22023154ca3d1db92721b5e2e50b83e94) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Updated dependencies
|
||||
|
||||
## 5.86.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.86.2",
|
||||
"version": "5.87.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -211,24 +211,24 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@google/model-viewer": "2.1.1",
|
||||
"@noble/curves": "1.7.0",
|
||||
"@noble/hashes": "1.6.1",
|
||||
"@noble/curves": "1.8.1",
|
||||
"@noble/hashes": "1.7.1",
|
||||
"@passwordless-id/webauthn": "^2.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@walletconnect/ethereum-provider": "2.17.3",
|
||||
"@walletconnect/sign-client": "2.17.3",
|
||||
"abitype": "1.0.7",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@walletconnect/ethereum-provider": "2.17.5",
|
||||
"@walletconnect/sign-client": "2.17.5",
|
||||
"abitype": "1.0.8",
|
||||
"cross-spawn": "7.0.6",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"mipd": "0.0.7",
|
||||
"ox": "0.4.2",
|
||||
"ox": "0.6.9",
|
||||
"uqr": "0.1.2",
|
||||
"viem": "2.21.55"
|
||||
"viem": "2.22.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-lambda": "^3",
|
||||
@@ -334,36 +334,36 @@
|
||||
"@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",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@codspeed/vitest-plugin": "4.0.0",
|
||||
"@coinbase/wallet-mobile-sdk": "1.1.2",
|
||||
"@mobile-wallet-protocol/client": "0.1.2",
|
||||
"@react-native-async-storage/async-storage": "2.1.0",
|
||||
"@size-limit/preset-big-lib": "11.1.6",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/react": "8.5.0",
|
||||
"@storybook/react-vite": "8.4.7",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/addon-onboarding": "8.5.2",
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/react-vite": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react": "19.0.8",
|
||||
"@viem/anvil": "0.0.10",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"@vitest/ui": "2.1.8",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vitest/ui": "3.0.4",
|
||||
"dotenv-mono": "^1.3.14",
|
||||
"ethers5": "npm:ethers@5",
|
||||
"ethers6": "npm:ethers@6",
|
||||
"expo-linking": "7.0.3",
|
||||
"expo-web-browser": "14.0.1",
|
||||
"happy-dom": "15.11.7",
|
||||
"knip": "5.41.0",
|
||||
"msw": "2.6.9",
|
||||
"happy-dom": "16.7.3",
|
||||
"knip": "5.43.5",
|
||||
"msw": "2.7.0",
|
||||
"prettier": "3.3.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
@@ -375,11 +375,11 @@
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"size-limit": "11.1.6",
|
||||
"storybook": "8.4.7",
|
||||
"typedoc": "0.27.5",
|
||||
"storybook": "8.5.2",
|
||||
"typedoc": "0.27.6",
|
||||
"typedoc-better-json": "0.9.4",
|
||||
"typescript": "5.7.2",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.0.11",
|
||||
"vitest": "2.1.8"
|
||||
"vitest": "3.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { getRpcClient } from "../rpc/rpc.js";
|
||||
import { estimateGas } from "../transaction/actions/estimate-gas.js";
|
||||
import { sendTransaction } from "../transaction/actions/send-transaction.js";
|
||||
import { prepareTransaction } from "../transaction/prepare-transaction.js";
|
||||
import { getAddress } from "../utils/address.js";
|
||||
import type { Account } from "../wallets/interfaces/wallet.js";
|
||||
|
||||
/**
|
||||
@@ -201,7 +202,7 @@ export async function toViemContract<const TAbi extends Abi>(options: {
|
||||
thirdwebContract: ThirdwebContract<TAbi>;
|
||||
}): Promise<GetContractReturnType<TAbi>> {
|
||||
return {
|
||||
address: options.thirdwebContract.address,
|
||||
address: getAddress(options.thirdwebContract.address),
|
||||
abi: await resolveContractAbi(options.thirdwebContract),
|
||||
};
|
||||
}
|
||||
@@ -310,7 +311,7 @@ function toViemWalletClient(options: ToViemWalletClientOptions): WalletClient {
|
||||
|
||||
return createWalletClient({
|
||||
transport,
|
||||
account: account.address,
|
||||
account: getAddress(account.address),
|
||||
chain: viemChain,
|
||||
key: "thirdweb-wallet",
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { type ThirdwebContract, getContract } from "../contract/contract.js";
|
||||
import { isValidSignature } from "../extensions/erc1271/__generated__/isValidSignature/read/isValidSignature.js";
|
||||
import { eth_call } from "../rpc/actions/eth_call.js";
|
||||
import { getRpcClient } from "../rpc/rpc.js";
|
||||
import type { Address } from "../utils/address.js";
|
||||
import { isZkSyncChain } from "../utils/any-evm/zksync/isZkSyncChain.js";
|
||||
import { isContractDeployed } from "../utils/bytecode/is-contract-deployed.js";
|
||||
import { fromBytes } from "../utils/encoding/from-bytes.js";
|
||||
@@ -27,7 +28,8 @@ export type VerifyHashParams = {
|
||||
};
|
||||
};
|
||||
|
||||
const ZKSYNC_VALIDATOR_ADDRESS = "0xfB688330379976DA81eB64Fe4BF50d7401763B9C";
|
||||
const ZKSYNC_VALIDATOR_ADDRESS: Address =
|
||||
"0xfB688330379976DA81eB64Fe4BF50d7401763B9C";
|
||||
|
||||
/**
|
||||
* @description Verify that an address created the provided signature for a given hash using [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492). This function is interoperable with all wallet types, including EOAs.
|
||||
@@ -120,7 +122,7 @@ export async function verifyHash({
|
||||
})();
|
||||
|
||||
let verificationData: {
|
||||
to?: string;
|
||||
to?: Address;
|
||||
data: Hex;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineChain } from "../utils.js";
|
||||
|
||||
/**
|
||||
* @chain
|
||||
*/
|
||||
export const abstract = /* @__PURE__ */ defineChain({
|
||||
id: 2741,
|
||||
name: "Abstract",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Abstract Block Explorer",
|
||||
url: "https://explorer.abs.xyz",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Abi } from "abitype";
|
||||
import { eth_getCode } from "../../rpc/actions/eth_getCode.js";
|
||||
import { getRpcClient } from "../../rpc/rpc.js";
|
||||
import type { Hex } from "../../utils/encoding/hex.js";
|
||||
import type { ThirdwebContract } from "../contract.js";
|
||||
|
||||
const BYTECODE_CACHE = new WeakMap<ThirdwebContract<Abi>, Promise<Hex>>();
|
||||
const BYTECODE_CACHE = new WeakMap<ThirdwebContract, Promise<Hex>>();
|
||||
|
||||
/**
|
||||
* Retrieves the bytecode of a contract.
|
||||
@@ -17,9 +16,7 @@ const BYTECODE_CACHE = new WeakMap<ThirdwebContract<Abi>, Promise<Hex>>();
|
||||
* ```
|
||||
* @contract
|
||||
*/
|
||||
export function getBytecode<abi extends Abi>(
|
||||
contract: ThirdwebContract<abi>,
|
||||
): Promise<Hex> {
|
||||
export function getBytecode(contract: ThirdwebContract): Promise<Hex> {
|
||||
if (BYTECODE_CACHE.has(contract)) {
|
||||
return BYTECODE_CACHE.get(contract) as Promise<Hex>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Abi, formatAbi, parseAbi } from "abitype";
|
||||
import { download } from "../../storage/download.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import type { ThirdwebContract } from "../contract.js";
|
||||
import { type ThirdwebContract, getContract } from "../contract.js";
|
||||
|
||||
const ABI_RESOLUTION_CACHE = new WeakMap<ThirdwebContract<Abi>, Promise<Abi>>();
|
||||
|
||||
@@ -419,10 +419,10 @@ async function getAbisForPlugins(
|
||||
): Promise<Abi[]> {
|
||||
return Promise.all(
|
||||
options.plugins.map((pluginAddress) => {
|
||||
const newContract = {
|
||||
const newContract = getContract({
|
||||
...options.contract,
|
||||
address: pluginAddress,
|
||||
};
|
||||
});
|
||||
// if we have a method passed in that tells us how to resove the sub-api, use that
|
||||
if (options.resolveSubAbi) {
|
||||
return options.resolveSubAbi(newContract);
|
||||
|
||||
@@ -21,7 +21,7 @@ export type ContractOptions<
|
||||
*/
|
||||
export type ThirdwebContract<
|
||||
abi extends Abi = [],
|
||||
address extends string = string,
|
||||
address extends string = `0x${string}`,
|
||||
> = Readonly<ContractOptions<abi, address>>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,11 +50,13 @@ export async function zkDeployContractDeterministic(
|
||||
create2FactoryAddress,
|
||||
salt: options.salt,
|
||||
});
|
||||
const deployed = await isContractDeployed({
|
||||
address: predictedAddress,
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
});
|
||||
const deployed = await isContractDeployed(
|
||||
getContract({
|
||||
address: predictedAddress,
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
}),
|
||||
);
|
||||
if (!deployed) {
|
||||
// check if bytecodehash is known
|
||||
const knownCodesStorageContract = getContract({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed
|
||||
import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js";
|
||||
import { randomBytesHex } from "../../../utils/random.js";
|
||||
import type { ClientAndChainAndAccount } from "../../../utils/types.js";
|
||||
import type { ThirdwebContract } from "../../contract.js";
|
||||
import { type ThirdwebContract, getContract } from "../../contract.js";
|
||||
import { zkDeployContractDeterministic } from "./zkDeployDeterministic.js";
|
||||
|
||||
/**
|
||||
@@ -28,11 +28,13 @@ export async function zkDeployProxy(
|
||||
if (!implementationAddress) {
|
||||
throw new Error("initializeTransaction must have a 'to' field set");
|
||||
}
|
||||
const deployed = await isContractDeployed({
|
||||
address: implementationAddress,
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
});
|
||||
const deployed = await isContractDeployed(
|
||||
getContract({
|
||||
address: implementationAddress,
|
||||
chain: options.chain,
|
||||
client: options.client,
|
||||
}),
|
||||
);
|
||||
if (!deployed) {
|
||||
throw new Error(
|
||||
`Implementation contract at ${implementationAddress} is not deployed`,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
eth_getLogs,
|
||||
} from "../../rpc/actions/eth_getLogs.js";
|
||||
import { getRpcClient } from "../../rpc/rpc.js";
|
||||
import { getAddress } from "../../utils/address.js";
|
||||
import type { Prettify } from "../../utils/type-utils.js";
|
||||
import { type PreparedEvent, prepareEvent } from "../prepare-event.js";
|
||||
import { isAbiEvent } from "../utils.js";
|
||||
@@ -168,11 +169,11 @@ export async function getContractEvents<
|
||||
? // if we have events passed in then we use those
|
||||
events.map((e) => ({
|
||||
...restParams,
|
||||
address: contract?.address,
|
||||
address: getAddress(contract.address),
|
||||
topics: e.topics,
|
||||
}))
|
||||
: // otherwise we want "all" events (aka not pass any topics at all)
|
||||
[{ ...restParams, address: contract?.address }];
|
||||
[{ ...restParams, address: getAddress(contract.address) }];
|
||||
|
||||
const logs = await Promise.all(
|
||||
logsParams.map((ethLogParams) => eth_getLogs(rpcRequest, ethLogParams)),
|
||||
|
||||
@@ -72,6 +72,7 @@ export { frameTestnet } from "../chains/chain-definitions/frame-testnet.js";
|
||||
export { hokumTestnet } from "../chains/chain-definitions/hokum-testnet.js";
|
||||
export { godWokenTestnetV1 } from "../chains/chain-definitions/god-woken-testnet-v1.js";
|
||||
export { abstractTestnet } from "../chains/chain-definitions/abstract-testnet.js";
|
||||
export { abstract } from "../chains/chain-definitions/abstract.js";
|
||||
export { assetChainTestnet } from "../chains/chain-definitions/assetchain-testnet.js";
|
||||
export { celoAlfajoresTestnet } from "../chains/chain-definitions/celo-alfajores-testnet.js";
|
||||
export { fraxtalTestnet } from "../chains/chain-definitions/fraxtal-testnet.js";
|
||||
|
||||
@@ -114,3 +114,50 @@ export type {
|
||||
FarcasterProfile,
|
||||
LensProfile,
|
||||
} from "../social/types.js";
|
||||
|
||||
/**
|
||||
* Account
|
||||
*/
|
||||
export {
|
||||
Blobbie,
|
||||
AccountBlobbie,
|
||||
type BlobbieProps,
|
||||
} from "../react/native/ui/prebuilt/Account/blobbie.js";
|
||||
export {
|
||||
AccountProvider,
|
||||
type AccountProviderProps,
|
||||
} from "../react/core/account/provider.js";
|
||||
export {
|
||||
AccountBalance,
|
||||
type AccountBalanceProps,
|
||||
} from "../react/native/ui/prebuilt/Account/balance.js";
|
||||
export {
|
||||
AccountAddress,
|
||||
type AccountAddressProps,
|
||||
} from "../react/native/ui/prebuilt/Account/address.js";
|
||||
export {
|
||||
AccountName,
|
||||
type AccountNameProps,
|
||||
} from "../react/native/ui/prebuilt/Account/name.js";
|
||||
export {
|
||||
AccountAvatar,
|
||||
type AccountAvatarProps,
|
||||
} from "../react/native/ui/prebuilt/Account/avatar.js";
|
||||
|
||||
/**
|
||||
* Wallet
|
||||
*/
|
||||
export {
|
||||
WalletProvider,
|
||||
type WalletProviderProps,
|
||||
} from "../react/core/wallet/provider.js";
|
||||
export {
|
||||
WalletIcon,
|
||||
SocialIcon,
|
||||
type WalletIconProps,
|
||||
type SocialIconProps,
|
||||
} from "../react/native/ui/prebuilt/Wallet/icon.js";
|
||||
export {
|
||||
WalletName,
|
||||
type WalletNameProps,
|
||||
} from "../react/native/ui/prebuilt/Wallet/name.js";
|
||||
|
||||
@@ -223,8 +223,8 @@ export {
|
||||
export {
|
||||
AccountBalance,
|
||||
type AccountBalanceProps,
|
||||
type AccountBalanceInfo,
|
||||
} from "../react/web/ui/prebuilt/Account/balance.js";
|
||||
export type { AccountBalanceInfo } from "../react/core/utils/account.js";
|
||||
export {
|
||||
AccountName,
|
||||
type AccountNameProps,
|
||||
@@ -233,7 +233,7 @@ export { AccountBlobbie } from "../react/web/ui/prebuilt/Account/blobbie.js";
|
||||
export {
|
||||
AccountProvider,
|
||||
type AccountProviderProps,
|
||||
} from "../react/web/ui/prebuilt/Account/provider.js";
|
||||
} from "../react/core/account/provider.js";
|
||||
export {
|
||||
AccountAvatar,
|
||||
type AccountAvatarProps,
|
||||
@@ -278,9 +278,11 @@ export { getLastAuthProvider } from "../react/web/utils/storage.js";
|
||||
export {
|
||||
WalletProvider,
|
||||
type WalletProviderProps,
|
||||
} from "../react/web/ui/prebuilt/Wallet/provider.js";
|
||||
} from "../react/core/wallet/provider.js";
|
||||
export {
|
||||
WalletIcon,
|
||||
SocialIcon,
|
||||
type SocialIconProps,
|
||||
type WalletIconProps,
|
||||
} from "../react/web/ui/prebuilt/Wallet/icon.js";
|
||||
export {
|
||||
|
||||
@@ -47,6 +47,8 @@ export {
|
||||
*/
|
||||
type PrivateKeyToAccountOptions as PrivateKeyAccountOptions,
|
||||
} from "../wallets/private-key.js";
|
||||
// generate a random private key
|
||||
export { randomPrivateKey } from "ox/Secp256k1";
|
||||
|
||||
export type {
|
||||
WalletId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getContract } from "../../../contract/contract.js";
|
||||
import type { BaseTransactionOptions } from "../../../transaction/types.js";
|
||||
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
|
||||
import * as PredictAddress from "../__generated__/IAccountFactory/read/getAddress.js";
|
||||
@@ -15,10 +16,10 @@ export {
|
||||
* ```ts
|
||||
* import { isAccountDeployed } from 'thirdweb/extensions/erc4337';
|
||||
*
|
||||
* const transaction = addAdmin({
|
||||
* const isDeployed = await isAccountDeployed({
|
||||
* contract,
|
||||
* account,
|
||||
* adminAddress: '0x...'
|
||||
* adminSigner: '0x...'
|
||||
* });
|
||||
*
|
||||
* await isAccountDeployed({ contract, adminSigner });
|
||||
@@ -29,8 +30,10 @@ export async function isAccountDeployed(
|
||||
options: BaseTransactionOptions<PredictAddress.GetAddressParams>,
|
||||
): Promise<boolean> {
|
||||
const predictedAddress = await PredictAddress.getAddress(options);
|
||||
return isContractDeployed({
|
||||
...options.contract,
|
||||
address: predictedAddress,
|
||||
});
|
||||
return isContractDeployed(
|
||||
getContract({
|
||||
...options.contract,
|
||||
address: predictedAddress,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { encodePacked } from "viem/utils";
|
||||
import type { ThirdwebContract } from "../../../../contract/contract.js";
|
||||
import { getAddress } from "../../../../utils/address.js";
|
||||
import { keccak256 } from "../../../../utils/hashing/keccak256.js";
|
||||
|
||||
/**
|
||||
@@ -15,7 +16,7 @@ export const hashDelayedRevealPassword = async (
|
||||
return keccak256(
|
||||
encodePacked(
|
||||
["string", "uint256", "uint256", "address"],
|
||||
[password, chainId, batchTokenIndex, contractAddress],
|
||||
[password, chainId, batchTokenIndex, getAddress(contractAddress)],
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "~test/react-render.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { AccountAddress } from "./address.js";
|
||||
import { AccountAddress } from "../../web/ui/prebuilt/Account/address.js";
|
||||
import { AccountProvider } from "./provider.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("AccountProvider component", () => {
|
||||
+7
-5
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { Address } from "abitype";
|
||||
import type React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { isAddress } from "../../../utils/address.js";
|
||||
/**
|
||||
* Props for the <AccountProvider /> component
|
||||
* @component
|
||||
@@ -14,7 +13,7 @@ export type AccountProviderProps = {
|
||||
/**
|
||||
* The user's wallet address
|
||||
*/
|
||||
address: Address;
|
||||
address: string;
|
||||
/**
|
||||
* thirdweb Client
|
||||
*/
|
||||
@@ -35,7 +34,7 @@ const AccountProviderContext = /* @__PURE__ */ createContext<
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAvatar, AccountName, AccountAddress } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider>
|
||||
* <AccountProvider address="0x..." client={client}>
|
||||
* <AccountAvatar />
|
||||
* <AccountName />
|
||||
* <AccountAddress />
|
||||
@@ -54,6 +53,9 @@ export function AccountProvider(
|
||||
"AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
|
||||
);
|
||||
}
|
||||
if (!isAddress(props.address)) {
|
||||
throw new Error(`AccountProvider: Invalid address: ${props.address}`);
|
||||
}
|
||||
return (
|
||||
<AccountProviderContext.Provider value={props}>
|
||||
{props.children}
|
||||
@@ -11,7 +11,6 @@ export function useAdminWallet() {
|
||||
const activeWallet = useActiveWallet();
|
||||
const connectedWallets = useConnectedWallets();
|
||||
const adminAccount = activeWallet?.getAdminAccount?.();
|
||||
|
||||
if (!adminAccount) {
|
||||
// If the active wallet doesn't have an admin account, return the active wallet
|
||||
return activeWallet;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { Chain } from "../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
|
||||
import { convertCryptoToFiat } from "../../../pay/convert/cryptoToFiat.js";
|
||||
import type { SupportedFiatCurrency } from "../../../pay/convert/type.js";
|
||||
import { type Address, isAddress } from "../../../utils/address.js";
|
||||
import { formatNumber } from "../../../utils/formatNumber.js";
|
||||
import { shortenLargeNumber } from "../../../utils/shortenLargeNumber.js";
|
||||
import { getWalletBalance } from "../../../wallets/utils/getWalletBalance.js";
|
||||
|
||||
export const COLOR_OPTIONS = [
|
||||
["#fca5a5", "#b91c1c"],
|
||||
["#fdba74", "#c2410c"],
|
||||
["#fcd34d", "#b45309"],
|
||||
["#fde047", "#a16207"],
|
||||
["#a3e635", "#4d7c0f"],
|
||||
["#86efac", "#15803d"],
|
||||
["#67e8f9", "#0e7490"],
|
||||
["#7dd3fc", "#0369a1"],
|
||||
["#93c5fd", "#1d4ed8"],
|
||||
["#a5b4fc", "#4338ca"],
|
||||
["#c4b5fd", "#6d28d9"],
|
||||
["#d8b4fe", "#7e22ce"],
|
||||
["#f0abfc", "#a21caf"],
|
||||
["#f9a8d4", "#be185d"],
|
||||
["#fda4af", "#be123c"],
|
||||
];
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export type AccountBalanceInfo = {
|
||||
/**
|
||||
* Represents either token balance or fiat balance.
|
||||
*/
|
||||
balance: number;
|
||||
/**
|
||||
* Represents either token symbol or fiat symbol
|
||||
*/
|
||||
symbol: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal Exported for tests
|
||||
*/
|
||||
export async function loadAccountBalance(props: {
|
||||
chain?: Chain;
|
||||
client: ThirdwebClient;
|
||||
address: Address;
|
||||
tokenAddress?: Address;
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
}): Promise<AccountBalanceInfo> {
|
||||
const { chain, client, address, tokenAddress, showBalanceInFiat } = props;
|
||||
if (!chain) {
|
||||
throw new Error("chain is required");
|
||||
}
|
||||
|
||||
if (
|
||||
tokenAddress &&
|
||||
tokenAddress?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
) {
|
||||
throw new Error(`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`);
|
||||
}
|
||||
|
||||
if (!isAddress(address)) {
|
||||
throw new Error("Invalid wallet address. Expected an EVM address");
|
||||
}
|
||||
|
||||
if (tokenAddress && !isAddress(tokenAddress)) {
|
||||
throw new Error("Invalid tokenAddress. Expected an EVM contract address");
|
||||
}
|
||||
|
||||
const tokenBalanceData = await getWalletBalance({
|
||||
chain,
|
||||
client,
|
||||
address,
|
||||
tokenAddress,
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (!tokenBalanceData) {
|
||||
throw new Error(
|
||||
`Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chain.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (showBalanceInFiat) {
|
||||
const fiatData = await convertCryptoToFiat({
|
||||
fromAmount: Number(tokenBalanceData.displayValue),
|
||||
fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
|
||||
to: showBalanceInFiat,
|
||||
chain,
|
||||
client,
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (fiatData === undefined) {
|
||||
throw new Error(
|
||||
`Failed to resolve fiat value for ${tokenAddress ? `token: ${tokenAddress}` : "native token"} on chainId: ${chain.id}`,
|
||||
);
|
||||
}
|
||||
const result = {
|
||||
balance: fiatData?.result,
|
||||
symbol: getFiatSymbol(showBalanceInFiat),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
balance: Number(tokenBalanceData.displayValue),
|
||||
symbol: tokenBalanceData.symbol,
|
||||
};
|
||||
}
|
||||
|
||||
function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) {
|
||||
switch (showBalanceInFiat) {
|
||||
case "USD":
|
||||
return "$";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the display balance for both crypto and fiat, in the Details button and Modal
|
||||
* If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
|
||||
* @internal
|
||||
* Used internally for the Details button and the Details Modal
|
||||
*/
|
||||
export function formatAccountTokenBalance(
|
||||
props: AccountBalanceInfo & { decimals: number },
|
||||
): string {
|
||||
const formattedTokenBalance = formatNumber(props.balance, props.decimals);
|
||||
return `${formattedTokenBalance} ${props.symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used internally for the Details button and Details Modal
|
||||
* @internal
|
||||
*/
|
||||
export function formatAccountFiatBalance(
|
||||
props: AccountBalanceInfo & { decimals: number },
|
||||
) {
|
||||
const num = formatNumber(props.balance, props.decimals);
|
||||
// Need to keep them short to avoid UI overflow issues
|
||||
const formattedFiatBalance = shortenLargeNumber(num);
|
||||
return `${props.symbol}${formattedFiatBalance}`;
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js";
|
||||
import type { AuthOption } from "../../../wallets/types.js";
|
||||
import type { WalletId } from "../../../wallets/wallet-types.js";
|
||||
import { useWalletContext } from "../wallet/provider.js";
|
||||
|
||||
// TODO make the social icons usable in RN too
|
||||
const googleIconUri =
|
||||
@@ -106,3 +110,27 @@ export function getSocialIcon(provider: AuthOption | ({} & string)): string {
|
||||
return genericWalletIcon;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export 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;
|
||||
}) {
|
||||
return getWalletInfo(props.id, true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
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 "../wallet/provider.js";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import type React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import type { WalletId } from "../../../../../wallets/wallet-types.js";
|
||||
import type { WalletId } from "../../../wallets/wallet-types.js";
|
||||
|
||||
/**
|
||||
* Props for the WalletProvider component
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { Text, type TextProps } from "react-native";
|
||||
import { useAccountContext } from "../../../../core/account/provider.js";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export interface AccountAddressProps extends Omit<TextProps, "children"> {
|
||||
/**
|
||||
* The function used to transform (format) the wallet address
|
||||
* Specifically useful for shortening the wallet.
|
||||
*
|
||||
* This function should take in a string and output a string
|
||||
*/
|
||||
formatFn?: (str: string) => string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns a <span> containing the full wallet address of the account
|
||||
*
|
||||
* @example
|
||||
* ### Basic usage
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
|
||||
* <AccountAddress />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
* Result:
|
||||
* ```html
|
||||
* <span>0x12345674b599ce99958242b3D3741e7b01841DF3</span>
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ### Shorten the address
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAddress } from "thirdweb/react";
|
||||
* import { shortenAddress } from "thirdweb/utils";
|
||||
*
|
||||
* <AccountProvider address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
|
||||
* <AccountAddress formatFn={shortenAddress} />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
* Result:
|
||||
* ```html
|
||||
* <span>0x1234...1DF3</span>
|
||||
* ```
|
||||
*
|
||||
* @component
|
||||
* @wallet
|
||||
* @beta
|
||||
*/
|
||||
export function AccountAddress({
|
||||
formatFn,
|
||||
...restProps
|
||||
}: AccountAddressProps) {
|
||||
const { address } = useAccountContext();
|
||||
const value = formatFn ? formatFn(address) : address;
|
||||
return <Text {...restProps}>{value}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { Image, type ImageProps } from "react-native";
|
||||
import { resolveAvatar } from "../../../../../extensions/ens/resolve-avatar.js";
|
||||
import {
|
||||
type ResolveNameOptions,
|
||||
resolveName,
|
||||
} from "../../../../../extensions/ens/resolve-name.js";
|
||||
import { getSocialProfiles } from "../../../../../social/profiles.js";
|
||||
import type { SocialProfile } from "../../../../../social/types.js";
|
||||
import { parseAvatarRecord } from "../../../../../utils/ens/avatar.js";
|
||||
import { useAccountContext } from "../../../../core/account/provider.js";
|
||||
/**
|
||||
* Props for the AccountAvatar component
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export interface AccountAvatarProps
|
||||
extends Omit<ImageProps, "source">,
|
||||
Omit<ResolveNameOptions, "client" | "address"> {
|
||||
/**
|
||||
* Use this prop to prioritize the social profile that you want to display
|
||||
* This is useful for a wallet containing multiple social profiles.
|
||||
* This component inherits all attributes of a HTML's <img />, so you can interact with it just like a normal <img />
|
||||
*
|
||||
* @example
|
||||
* If you have ENS, Lens and Farcaster profiles linked to your wallet
|
||||
* you can prioritize showing the image for Lens by:
|
||||
* ```tsx
|
||||
* <AccountAvatar
|
||||
* socialType="lens" // Choose between: "farcaster" | "lens" | "ens"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
socialType?: SocialProfile["type"];
|
||||
|
||||
/**
|
||||
* This component will be shown while the avatar of the account is being fetched
|
||||
* If not passed, the component will return `null`.
|
||||
*
|
||||
* You can pass a loading sign or spinner to this prop.
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AccountAvatar loadingComponent={<Spinner />} />
|
||||
* ```
|
||||
*/
|
||||
loadingComponent?: React.ComponentType;
|
||||
/**
|
||||
* This component will be shown if the request for fetching the avatar is done
|
||||
* but could not retreive any result.
|
||||
* You can pass a dummy avatar/image to this prop.
|
||||
*
|
||||
* If not passed, the component will return `null`
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AccountAvatar fallbackComponent={<DummyImage />} />
|
||||
* ```
|
||||
*/
|
||||
fallbackComponent?: React.ComponentType;
|
||||
|
||||
/**
|
||||
* Optional query options for `useQuery`
|
||||
*/
|
||||
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
|
||||
}
|
||||
|
||||
/**
|
||||
* The component for showing the avatar of the account.
|
||||
* If fetches all the social profiles linked to your wallet, including: Farcaster, ENS, Lens (more to be added)
|
||||
* You can choose which social profile you want to display. Defaults to the first item in the list.
|
||||
*
|
||||
* @example
|
||||
* ### Basic usage
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAvatar } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountAvatar />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
* Result: An <img /> component, if the avatar is resolved successfully
|
||||
* ```html
|
||||
* <img alt="" src="resolved-url-for-the-avatar" />
|
||||
* ```
|
||||
*
|
||||
* ### Show a loading sign when the avatar is being resolved
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAvatar } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountAvatar
|
||||
* loadingComponent={<YourLoadingComponent />}
|
||||
* />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Fallback to something when the avatar fails to resolve
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAvatar } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountAvatar
|
||||
* fallbackComponent={<DummyImage />}
|
||||
* />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Select a social profile to display
|
||||
* If you wallet associates with more than one social profiles (Lens, Farcaster, ENS, etc.)
|
||||
* You can specify which service you want to prioritize using the `socialType` props
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAvatar } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountAvatar
|
||||
* // Choose between: "farcaster" | "lens" | "ens"
|
||||
* socialType={"ens"}
|
||||
* />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Custom ENS resolver chain
|
||||
* This component shares the same props with the ENS extension `resolveAvatar`
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountAvatar } from "thirdweb/react";
|
||||
* import { base } from "thirdweb/chains";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountAvatar
|
||||
* resolverAddress={"0x..."}
|
||||
* resolverChain={base}
|
||||
* />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### 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
|
||||
* <AccountAvatar
|
||||
* queryOptions={{
|
||||
* enabled: isEnabled,
|
||||
* retry: 3,
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
* @returns An <img /> if the avatar is resolved successfully
|
||||
* @component
|
||||
* @wallet
|
||||
* @beta
|
||||
*/
|
||||
export function AccountAvatar({
|
||||
socialType,
|
||||
resolverAddress,
|
||||
resolverChain,
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
queryOptions,
|
||||
...restProps
|
||||
}: AccountAvatarProps) {
|
||||
const { address, client } = useAccountContext();
|
||||
const avatarQuery = useQuery({
|
||||
queryKey: [
|
||||
"account-avatar",
|
||||
address,
|
||||
{ socialType },
|
||||
{ resolverAddress, resolverChain },
|
||||
],
|
||||
queryFn: async (): Promise<string> => {
|
||||
const [socialData, ensName] = await Promise.all([
|
||||
getSocialProfiles({ address, client }),
|
||||
resolveName({
|
||||
client,
|
||||
address: address || "",
|
||||
resolverAddress,
|
||||
resolverChain,
|
||||
}),
|
||||
]);
|
||||
|
||||
const uri = socialData?.filter(
|
||||
(p) => p.avatar && (socialType ? p.type === socialType : true),
|
||||
)[0]?.avatar;
|
||||
|
||||
const [resolvedSocialAvatar, resolvedENSAvatar] = await Promise.all([
|
||||
uri ? parseAvatarRecord({ client, uri }) : undefined,
|
||||
ensName
|
||||
? resolveAvatar({
|
||||
client,
|
||||
name: ensName,
|
||||
})
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
// If no social image + ens name found -> exit and show <Blobbie />
|
||||
if (!resolvedSocialAvatar && !resolvedENSAvatar) {
|
||||
throw new Error("Failed to resolve social + ens avatar");
|
||||
}
|
||||
|
||||
// else, prioritize the social image first
|
||||
if (resolvedSocialAvatar) {
|
||||
return resolvedSocialAvatar;
|
||||
}
|
||||
|
||||
if (resolvedENSAvatar) {
|
||||
return resolvedENSAvatar;
|
||||
}
|
||||
|
||||
throw new Error("Failed to resolve social + ens avatar");
|
||||
},
|
||||
retry: false,
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
if (avatarQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
|
||||
if (!avatarQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
return <Image source={{ uri: avatarQuery.data }} {...restProps} />;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { Text, type TextProps } from "react-native";
|
||||
import type { Chain } from "../../../../../chains/types.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
|
||||
import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
|
||||
import { getAddress } from "../../../../../utils/address.js";
|
||||
import { useAccountContext } from "../../../../core/account/provider.js";
|
||||
import {
|
||||
type AccountBalanceInfo,
|
||||
formatAccountFiatBalance,
|
||||
loadAccountBalance,
|
||||
} from "../../../../core/utils/account.js";
|
||||
import { formatAccountTokenBalance } from "../../../../core/utils/account.js";
|
||||
|
||||
/**
|
||||
* Props for the AccountBalance component
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export interface AccountBalanceProps extends Omit<TextProps, "children"> {
|
||||
/**
|
||||
* The network to fetch balance on
|
||||
* If not passed, the component will use the current chain that the wallet is connected to (`useActiveWalletChain()`)
|
||||
*/
|
||||
chain?: Chain;
|
||||
/**
|
||||
* By default this component will fetch the balance for the native token on a given chain
|
||||
* If you want to fetch balance for an ERC20 token, use the `tokenAddress` props
|
||||
*/
|
||||
tokenAddress?: string;
|
||||
/**
|
||||
* A function to format the balance's display value
|
||||
* use this function to transform the balance display value like round up the number
|
||||
* Particularly useful to avoid overflowing-UI issues
|
||||
*/
|
||||
formatFn?: (props: AccountBalanceInfo) => string;
|
||||
/**
|
||||
* This component will be shown while the balance of the account 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
|
||||
* <AccountBalance
|
||||
* chain={ethereum}
|
||||
* loadingComponent={<Spinner />}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
loadingComponent?: React.ComponentType;
|
||||
/**
|
||||
* This component will be shown if the balance 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
|
||||
* balance was not fetched succesfully
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AccountBalance
|
||||
* chain={nonExistentChain}
|
||||
* fallbackComponent={"Failed to load"}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
fallbackComponent?: React.ComponentType;
|
||||
/**
|
||||
* Optional `useQuery` params
|
||||
*/
|
||||
queryOptions?: Omit<
|
||||
UseQueryOptions<AccountBalanceInfo>,
|
||||
"queryFn" | "queryKey"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Show the token balance in a supported fiat currency (e.g "USD")
|
||||
*/
|
||||
showBalanceInFiat?: SupportedFiatCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component fetches and shows the balance of the wallet address on a given chain.
|
||||
* 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 { AccountProvider, AccountBalance } from "thirdweb/react";
|
||||
* import { ethereum } from "thirdweb/chains";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountBalance chain={ethereum} />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
* Result:
|
||||
* ```html
|
||||
* <span>1.091435 ETH</span>
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ### Format the balance (round up, shorten etc.)
|
||||
* The AccountBalance component accepts a `formatFn` which takes in an object of type `AccountBalanceInfo` and outputs a string
|
||||
* The function is used to modify the display value of the wallet balance (either in crypto or fiat)
|
||||
*
|
||||
* ```tsx
|
||||
* import type { AccountBalanceInfo } from "thirdweb/react";
|
||||
* import { formatNumber } from "thirdweb/utils";
|
||||
*
|
||||
* const format = (props: AccountInfoBalance):string => `${formatNumber(props.balance, 1)} ${props.symbol.toLowerCase()}`
|
||||
*
|
||||
* <AccountBalance formatFn={format} />
|
||||
* ```
|
||||
*
|
||||
* Result:
|
||||
* ```html
|
||||
* <span>1.1 eth</span> // the balance is rounded up to 1 decimal and the symbol is lowercased
|
||||
* ```
|
||||
*
|
||||
* ### Show a loading sign when the balance is being fetched
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountBalance } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountBalance
|
||||
* chain={ethereum}
|
||||
* loadingComponent={<Spinner />}
|
||||
* />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Fallback to something when the balance fails to resolve
|
||||
* ```tsx
|
||||
* <AccountProvider address="0x...">
|
||||
* <AccountBalance
|
||||
* chain={nonExistentChain}
|
||||
* fallbackComponent={"Failed to load"}
|
||||
* />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### 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
|
||||
* <AccountBalance
|
||||
* queryOptions={{
|
||||
* enabled: isEnabled,
|
||||
* retry: 4,
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @component
|
||||
* @wallet
|
||||
* @beta
|
||||
*/
|
||||
export function AccountBalance({
|
||||
chain,
|
||||
tokenAddress,
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
queryOptions,
|
||||
formatFn,
|
||||
showBalanceInFiat,
|
||||
...restProps
|
||||
}: AccountBalanceProps) {
|
||||
const { address, client } = useAccountContext();
|
||||
const walletChain = useActiveWalletChain();
|
||||
const chainToLoad = chain || walletChain;
|
||||
const balanceQuery = useQuery({
|
||||
queryKey: [
|
||||
"internal_account_balance",
|
||||
chainToLoad?.id || -1,
|
||||
address,
|
||||
{ tokenAddress },
|
||||
showBalanceInFiat,
|
||||
] as const,
|
||||
queryFn: async (): Promise<AccountBalanceInfo> =>
|
||||
loadAccountBalance({
|
||||
chain: chainToLoad,
|
||||
client,
|
||||
address: getAddress(address),
|
||||
tokenAddress: tokenAddress ? getAddress(tokenAddress) : undefined,
|
||||
showBalanceInFiat,
|
||||
}),
|
||||
retry: false,
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
if (balanceQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
|
||||
if (balanceQuery.data === undefined) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
// Prioritize using the formatFn from users
|
||||
if (formatFn) {
|
||||
return <Text {...restProps}>{formatFn(balanceQuery.data)}</Text>;
|
||||
}
|
||||
|
||||
if (showBalanceInFiat) {
|
||||
return (
|
||||
<Text {...restProps}>
|
||||
{formatAccountFiatBalance({ ...balanceQuery.data, decimals: 2 })}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text {...restProps}>
|
||||
{formatAccountTokenBalance({
|
||||
...balanceQuery.data,
|
||||
decimals: balanceQuery.data.balance < 1 ? 3 : 2,
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { hexToNumber } from "@noble/curves/abstract/utils";
|
||||
import { useMemo } from "react";
|
||||
import { StyleSheet, View, type ViewStyle } from "react-native";
|
||||
import { Defs, LinearGradient, Rect, Stop, Svg } from "react-native-svg";
|
||||
import { useAccountContext } from "../../../../core/account/provider.js";
|
||||
import { COLOR_OPTIONS } from "../../../../core/utils/account.js";
|
||||
/**
|
||||
* Props for the Blobbie component
|
||||
* @component
|
||||
*/
|
||||
export type BlobbieProps = {
|
||||
address: string;
|
||||
size: number;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper for the Blobbie component
|
||||
* @param props BlobbieProps
|
||||
* @beta
|
||||
* @wallet
|
||||
*/
|
||||
export function AccountBlobbie(props: Omit<BlobbieProps, "address">) {
|
||||
const { address } = useAccountContext();
|
||||
return <Blobbie {...props} address={address} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* A unique gradient avatar based on the provided address.
|
||||
* @param props The component props.
|
||||
* @param props.address The address to generate the gradient with.
|
||||
* @param props.style The style for the component
|
||||
* @param props.size The size of each side of the square avatar (in pixels)
|
||||
* @component
|
||||
* @wallet
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { Blobbie } from "thirdweb/react";
|
||||
*
|
||||
* <Blobbie address="0x...." style={{ width: 40, height: 40 }} />
|
||||
* ```
|
||||
*/
|
||||
export function Blobbie(props: BlobbieProps) {
|
||||
const colors = useMemo(
|
||||
() =>
|
||||
COLOR_OPTIONS[
|
||||
Number(hexToNumber(props.address.slice(2, 4))) % COLOR_OPTIONS.length
|
||||
] as [string, string],
|
||||
[props.address],
|
||||
);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
const baseStyle = props.style || {};
|
||||
if (props.size) {
|
||||
return {
|
||||
...baseStyle,
|
||||
width: props.size,
|
||||
height: props.size,
|
||||
};
|
||||
}
|
||||
return baseStyle;
|
||||
}, [props.style, props.size]);
|
||||
|
||||
const gradientUniqueId = `grad${colors[0]}+${colors[1]}`.replace(
|
||||
/[^a-zA-Z0-9 ]/g,
|
||||
"",
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
<Svg height="100%" width="100%" style={StyleSheet.absoluteFillObject}>
|
||||
<Defs>
|
||||
<LinearGradient
|
||||
id={gradientUniqueId}
|
||||
x1="0%"
|
||||
y1="100%"
|
||||
x2="100%"
|
||||
y2="0%"
|
||||
>
|
||||
<Stop offset="0" stopColor={colors[0]} />
|
||||
<Stop offset="1" stopColor={colors[1]} />
|
||||
</LinearGradient>
|
||||
</Defs>
|
||||
<Rect width="100%" height="100%" fill={`url(#${gradientUniqueId})`} />
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import { Text, type TextProps } from "react-native";
|
||||
import {
|
||||
type ResolveNameOptions,
|
||||
resolveName,
|
||||
} from "../../../../../extensions/ens/resolve-name.js";
|
||||
import { getSocialProfiles } from "../../../../../social/profiles.js";
|
||||
import type { SocialProfile } from "../../../../../social/types.js";
|
||||
import { useAccountContext } from "../../../../core/account/provider.js";
|
||||
|
||||
/**
|
||||
* Props for the AccountName component
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export interface AccountNameProps
|
||||
extends Omit<TextProps, "children">,
|
||||
Omit<ResolveNameOptions, "client" | "address"> {
|
||||
/**
|
||||
* A function used to transform (format) the name of the account.
|
||||
* it should take in a string and output a string.
|
||||
*
|
||||
* This function is particularly useful
|
||||
*/
|
||||
formatFn?: (str: string) => string;
|
||||
/**
|
||||
* Use this prop to prioritize the social profile that you want to display
|
||||
* This is useful for a wallet containing multiple social profiles
|
||||
*/
|
||||
socialType?: SocialProfile["type"];
|
||||
/**
|
||||
* This component will be shown while the name of the account is being fetched
|
||||
* If not passed, the component will return `null`.
|
||||
*
|
||||
* You can pass a loading sign or spinner to this prop.
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AccountName loadingComponent={<Spinner />} />
|
||||
* ```
|
||||
*/
|
||||
loadingComponent?: React.ComponentType;
|
||||
/**
|
||||
* This component will be shown if the request for fetching the name is done but could not retreive any result.
|
||||
* You can pass the wallet address as the fallback option if that's the case.
|
||||
*
|
||||
* If not passed, the component will return `null`
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AccountName fallbackComponent={"0x1234....3f3f"} />
|
||||
* ```
|
||||
*/
|
||||
fallbackComponent?: React.ComponentType;
|
||||
/**
|
||||
* Optional `useQuery` params
|
||||
*/
|
||||
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is used to display the name of the account.
|
||||
* A "name" in this context is the username, or account of the social profiles that the wallet may have.
|
||||
* In case a name is not found or failed to resolve, you can always fallback to displaying the wallet address instead by using the `fallbackComponent` prop.
|
||||
*
|
||||
* This component inherits all attribute of a native HTML <span> element, so you can style it just like how you would style a <span>.
|
||||
*
|
||||
* @param props
|
||||
* @returns A `<span>` containing the name of the account
|
||||
* ```html
|
||||
* <span>{name}</span>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ### Basic usage
|
||||
* ```tsx
|
||||
* import { AccountProvider, AccountName } from "thirdweb/react";
|
||||
*
|
||||
* <AccountProvider address="0x1234...3f3f" client={client}>
|
||||
* <AccountName />
|
||||
* </AccountProvider>
|
||||
* ```
|
||||
*
|
||||
* ### Show wallet address while social name is being loaded
|
||||
* ```tsx
|
||||
* <AccountName
|
||||
* loadingComponent={<AccountAddress />}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ### Fallback to showing wallet address if fail to resolve social name
|
||||
* ```tsx
|
||||
* <AccountName
|
||||
* fallbackComponent={<AccountAddress />}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Transform the account name using `formatFn` prop
|
||||
* ```tsx
|
||||
* import { isAddress, shortenAddress } from "thirdweb/utils";
|
||||
* import { AccountProvider, AccountName } from "thirdweb/react";
|
||||
*
|
||||
* // Let's say we want the name to be capitalized without using CSS
|
||||
* const formatName = (name: string) => name.toUpperCase();
|
||||
*
|
||||
* return <AccountName formatFn={formatName} />
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ### 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
|
||||
* <AccountName
|
||||
* queryOptions={{
|
||||
* enabled: isEnabled,
|
||||
* retry: 3
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @component
|
||||
* @wallet
|
||||
* @beta
|
||||
*/
|
||||
export function AccountName({
|
||||
resolverAddress,
|
||||
resolverChain,
|
||||
socialType,
|
||||
formatFn,
|
||||
queryOptions,
|
||||
loadingComponent,
|
||||
fallbackComponent,
|
||||
...restProps
|
||||
}: AccountNameProps) {
|
||||
const { address, client } = useAccountContext();
|
||||
const nameQuery = useQuery({
|
||||
queryKey: [
|
||||
"account-name",
|
||||
address,
|
||||
{ socialType },
|
||||
{ resolverAddress, resolverChain },
|
||||
],
|
||||
queryFn: async () => {
|
||||
const [socialData, ensName] = await Promise.all([
|
||||
getSocialProfiles({ address, client }),
|
||||
resolveName({
|
||||
client,
|
||||
address,
|
||||
resolverAddress,
|
||||
resolverChain,
|
||||
}),
|
||||
]);
|
||||
|
||||
const name =
|
||||
socialData?.filter(
|
||||
(p) => p.name && (socialType ? p.type === socialType : true),
|
||||
)[0]?.name || ensName;
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Failed to resolve account name");
|
||||
}
|
||||
return formatFn ? formatFn(name) : name;
|
||||
},
|
||||
retry: false,
|
||||
...queryOptions,
|
||||
});
|
||||
|
||||
if (nameQuery.isLoading) {
|
||||
return loadingComponent || null;
|
||||
}
|
||||
|
||||
if (!nameQuery.data) {
|
||||
return fallbackComponent || null;
|
||||
}
|
||||
|
||||
return <Text {...restProps}>{nameQuery.data}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
import { Image, type ImageProps } from "react-native";
|
||||
import { SvgXml, type XmlProps } from "react-native-svg";
|
||||
import type { AuthOption } from "../../../../../wallets/types.js";
|
||||
import { useWalletIcon } from "../../../../core/utils/walletIcon.js";
|
||||
import { getAuthProviderImage } from "../../components/WalletImage.js";
|
||||
|
||||
export interface WalletIconProps extends Omit<ImageProps, "uri"> {
|
||||
/**
|
||||
* 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?: React.ComponentType;
|
||||
/**
|
||||
* This component will be shown if the icon fails to be retrieved
|
||||
* 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 successfully
|
||||
* @example
|
||||
* ```tsx
|
||||
* <WalletIcon fallbackComponent={<span>Failed to load</span>}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
fallbackComponent?: React.ComponentType;
|
||||
/**
|
||||
* 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 <Image source={{ uri: imageQuery.data }} {...restProps} />;
|
||||
}
|
||||
|
||||
export interface SocialIconProps extends Omit<XmlProps, "xml"> {
|
||||
provider: AuthOption | (string & {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Social auth provider icon
|
||||
* @returns an <img /> component with the src set to the svg
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { SocialIcon } from "thirdweb/react";
|
||||
*
|
||||
* <SocialIcon provider="google" />
|
||||
* ```
|
||||
*
|
||||
* Result: An <img /> component with the src of the icon
|
||||
* ```html
|
||||
* <img src="google-icon-svg" />
|
||||
* ```
|
||||
*
|
||||
* @component
|
||||
* @wallet
|
||||
* @beta
|
||||
*/
|
||||
export function SocialIcon({ provider, ...restProps }: SocialIconProps) {
|
||||
const src = getAuthProviderImage(provider);
|
||||
return <SvgXml xml={src} {...restProps} />;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
import { Text, type TextProps } from "react-native";
|
||||
import { useWalletName } from "../../../../core/utils/walletname.js";
|
||||
/**
|
||||
* Props for the WalletName component
|
||||
* @component
|
||||
* @wallet
|
||||
*/
|
||||
export interface WalletNameProps extends Omit<TextProps, "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?: React.ComponentType;
|
||||
/**
|
||||
* 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?: React.ComponentType;
|
||||
/**
|
||||
* 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 <Text {...restProps}>{nameQuery.data}</Text>;
|
||||
}
|
||||
@@ -1,32 +1,14 @@
|
||||
"use client";
|
||||
import { hexToNumber } from "@noble/curves/abstract/utils";
|
||||
import { useId, useMemo } from "react";
|
||||
import type { Address } from "viem";
|
||||
|
||||
const COLOR_OPTIONS = [
|
||||
["#fca5a5", "#b91c1c"],
|
||||
["#fdba74", "#c2410c"],
|
||||
["#fcd34d", "#b45309"],
|
||||
["#fde047", "#a16207"],
|
||||
["#a3e635", "#4d7c0f"],
|
||||
["#86efac", "#15803d"],
|
||||
["#67e8f9", "#0e7490"],
|
||||
["#7dd3fc", "#0369a1"],
|
||||
["#93c5fd", "#1d4ed8"],
|
||||
["#a5b4fc", "#4338ca"],
|
||||
["#c4b5fd", "#6d28d9"],
|
||||
["#d8b4fe", "#7e22ce"],
|
||||
["#f0abfc", "#a21caf"],
|
||||
["#f9a8d4", "#be185d"],
|
||||
["#fda4af", "#be123c"],
|
||||
];
|
||||
import { COLOR_OPTIONS } from "../../../core/utils/account.js";
|
||||
|
||||
/**
|
||||
* Props for the Blobbie component
|
||||
* @component
|
||||
*/
|
||||
export type BlobbieProps = {
|
||||
address: Address;
|
||||
address: string;
|
||||
style?: Omit<React.CSSProperties, "backgroundImage">;
|
||||
className?: string;
|
||||
size?: number;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getDefaultWallets } from "../../../../wallets/defaultWallets.js";
|
||||
import { AccountProvider } from "../../../core/account/provider.js";
|
||||
import { iconSize } from "../../../core/design-system/index.js";
|
||||
import { useSiweAuth } from "../../../core/hooks/auth/useSiweAuth.js";
|
||||
import type { ConnectButtonProps } from "../../../core/hooks/connection/ConnectButtonProps.js";
|
||||
@@ -23,7 +24,6 @@ import { Spinner } from "../components/Spinner.js";
|
||||
import { Container } from "../components/basic.js";
|
||||
import { Button } from "../components/buttons.js";
|
||||
import { fadeInAnimation } from "../design-system/animations.js";
|
||||
import { AccountProvider } from "../prebuilt/Account/provider.js";
|
||||
import { ConnectedWalletDetails } from "./Details.js";
|
||||
import ConnectModal from "./Modal/ConnectModal.js";
|
||||
import { LockIcon } from "./icons/LockIcon.js";
|
||||
|
||||
@@ -14,8 +14,8 @@ import { base } from "../../../../chains/chain-definitions/base.js";
|
||||
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
|
||||
import { useActiveAccount } from "../../../../react/core/hooks/wallets/useActiveAccount.js";
|
||||
import { useActiveWalletChain } from "../../../../react/core/hooks/wallets/useActiveWalletChain.js";
|
||||
import { AccountProvider } from "../../../core/account/provider.js";
|
||||
import { ThirdwebProvider } from "../../providers/thirdweb-provider.js";
|
||||
import { AccountProvider } from "../prebuilt/Account/provider.js";
|
||||
import {
|
||||
ConnectedToSmartWallet,
|
||||
ConnectedWalletDetails,
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
EcosystemWalletId,
|
||||
WalletId,
|
||||
} from "../../../../wallets/wallet-types.js";
|
||||
import { AccountProvider } from "../../../core/account/provider.js";
|
||||
import {
|
||||
CustomThemeProvider,
|
||||
parseTheme,
|
||||
@@ -67,6 +68,11 @@ import { useAdminWallet } from "../../../core/hooks/wallets/useAdminWallet.js";
|
||||
import { useDisconnect } from "../../../core/hooks/wallets/useDisconnect.js";
|
||||
import { useSwitchActiveWalletChain } from "../../../core/hooks/wallets/useSwitchActiveWalletChain.js";
|
||||
import { SetRootElementContext } from "../../../core/providers/RootElementContext.js";
|
||||
import {
|
||||
type AccountBalanceInfo,
|
||||
formatAccountFiatBalance,
|
||||
formatAccountTokenBalance,
|
||||
} from "../../../core/utils/account.js";
|
||||
import type {
|
||||
SupportedNFTs,
|
||||
SupportedTokens,
|
||||
@@ -91,15 +97,9 @@ import { fadeInAnimation } from "../design-system/animations.js";
|
||||
import { StyledButton } from "../design-system/elements.js";
|
||||
import { AccountAddress } from "../prebuilt/Account/address.js";
|
||||
import { AccountAvatar } from "../prebuilt/Account/avatar.js";
|
||||
import {
|
||||
AccountBalance,
|
||||
type AccountBalanceInfo,
|
||||
formatAccountFiatBalance,
|
||||
formatAccountTokenBalance,
|
||||
} from "../prebuilt/Account/balance.js";
|
||||
import { AccountBalance } from "../prebuilt/Account/balance.js";
|
||||
import { AccountBlobbie } from "../prebuilt/Account/blobbie.js";
|
||||
import { AccountName } from "../prebuilt/Account/name.js";
|
||||
import { AccountProvider } from "../prebuilt/Account/provider.js";
|
||||
import { ChainIcon } from "../prebuilt/Chain/icon.js";
|
||||
import { ChainName } from "../prebuilt/Chain/name.js";
|
||||
import { ChainProvider } from "../prebuilt/Chain/provider.js";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isEcosystemWallet } from "../../../../../wallets/ecosystem/is-ecosystem
|
||||
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
|
||||
import type { EcosystemWalletId } from "../../../../../wallets/wallet-types.js";
|
||||
import { iconSize } from "../../../../core/design-system/index.js";
|
||||
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
|
||||
import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js";
|
||||
import { useAdminWallet } from "../../../../core/hooks/wallets/useAdminWallet.js";
|
||||
import EcosystemWalletConnectUI from "../../../wallets/ecosystem/EcosystemWalletConnectUI.js";
|
||||
@@ -28,20 +29,23 @@ export function LinkProfileScreen(props: {
|
||||
client: ThirdwebClient;
|
||||
walletConnect: { projectId?: string } | undefined;
|
||||
}) {
|
||||
const activeWallet = useAdminWallet();
|
||||
const adminWallet = useAdminWallet();
|
||||
const activeWallet = useActiveWallet();
|
||||
const chain = useActiveWalletChain();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
if (!activeWallet) {
|
||||
const wallet = adminWallet || activeWallet;
|
||||
|
||||
if (!wallet) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (activeWallet.id === "inApp") {
|
||||
if (wallet.id === "inApp") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<InAppWalletConnectUI
|
||||
walletConnect={props.walletConnect}
|
||||
wallet={activeWallet as Wallet<"inApp">}
|
||||
wallet={wallet as Wallet<"inApp">}
|
||||
done={() => {
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
@@ -63,11 +67,11 @@ export function LinkProfileScreen(props: {
|
||||
);
|
||||
}
|
||||
|
||||
if (isEcosystemWallet(activeWallet)) {
|
||||
if (isEcosystemWallet(wallet)) {
|
||||
return (
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<EcosystemWalletConnectUI
|
||||
wallet={activeWallet as Wallet<EcosystemWalletId>}
|
||||
wallet={wallet as Wallet<EcosystemWalletId>}
|
||||
done={() => {
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ describe("ManageWalletScreen", () => {
|
||||
expect(screen.getByText(en.manageWallet.title)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the linked profiles button if allowLinkingProfiles is true", () => {
|
||||
it.skip("should render the linked profiles button if allowLinkingProfiles is true", () => {
|
||||
render(
|
||||
<ManageWalletScreen
|
||||
{...mockProps}
|
||||
@@ -38,7 +38,7 @@ describe("ManageWalletScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render the linked profiles button if allowLinkingProfiles is false", () => {
|
||||
it.skip("should not render the linked profiles button if allowLinkingProfiles is false", () => {
|
||||
render(
|
||||
<ManageWalletScreen
|
||||
{...mockProps}
|
||||
@@ -50,7 +50,7 @@ describe("ManageWalletScreen", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should default to showing linked profiles button", () => {
|
||||
it.skip("should default to showing linked profiles button", () => {
|
||||
render(<ManageWalletScreen {...mockProps} />);
|
||||
expect(
|
||||
screen.getByText(en.manageWallet.linkedProfiles),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user