Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba3b30b60 | ||
|
|
5ad442cf0c | ||
|
|
5756c74a71 | ||
|
|
a1cff7a579 | ||
|
|
eafaefec20 | ||
|
|
b81d7a1854 | ||
|
|
21cc45adc2 | ||
|
|
a67d342779 | ||
|
|
60edce49c0 | ||
|
|
20c0dca958 | ||
|
|
4b30b847e2 | ||
|
|
b2cc17fd1e | ||
|
|
ace1dc5b09 | ||
|
|
9f79277344 | ||
|
|
6e0fd205ff | ||
|
|
69b1ee9176 | ||
|
|
0a19142a24 | ||
|
|
feb4fd0908 | ||
|
|
1d8695eb42 | ||
|
|
858dc53a25 | ||
|
|
8997fac1b0 | ||
|
|
403e4224e4 | ||
|
|
de039ef462 | ||
|
|
35fd3bcafa | ||
|
|
80a22ff45e |
@@ -12,7 +12,7 @@ packages/eslint-config-thirdweb/ @thirdweb-dev/core-platform @edwardysun
|
||||
packages/tw-tsconfig/ @thirdweb-dev/core-platform @edwardysun
|
||||
|
||||
# apps
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform @jakubkrehel
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform
|
||||
|
||||
# .github folder + .changeset + turbo config is owned by jonas for now
|
||||
scripts/ @jnsdls @joaquim-verges
|
||||
|
||||
@@ -27,5 +27,9 @@
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./packages/*/" },
|
||||
{ "pattern": "./apps/*/" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.15.0",
|
||||
"checkly": "^4.18.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AnalyticsQueryParams,
|
||||
InAppWalletStats,
|
||||
RpcMethodStats,
|
||||
TransactionStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
@@ -77,7 +78,10 @@ export async function getUserOpUsage(
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch user ops usage");
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch user ops usage: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -85,6 +89,30 @@ export async function getUserOpUsage(
|
||||
return json.data as UserOpStats[];
|
||||
}
|
||||
|
||||
export async function getClientTransactions(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<TransactionStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v1/transactions/client?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch client transactions stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as TransactionStats[];
|
||||
}
|
||||
|
||||
export async function getRpcMethodUsage(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<RpcMethodStats[]> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -21,11 +20,7 @@ export type Project = {
|
||||
};
|
||||
|
||||
export async function getProjects(teamSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return [];
|
||||
@@ -46,11 +41,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
|
||||
@@ -90,6 +90,9 @@ export function SingleNetworkSelector(props: {
|
||||
popoverContentClassName?: string;
|
||||
// if specified - only these chains will be shown
|
||||
chainIds?: number[];
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -142,14 +145,17 @@ export function SingleNetworkSelector(props: {
|
||||
/>
|
||||
{chain.name}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
const isLoadingChains = allChains.length === 0;
|
||||
@@ -168,6 +174,8 @@ export function SingleNetworkSelector(props: {
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={isLoadingChains}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ interface SelectWithSearchProps
|
||||
) => boolean;
|
||||
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
|
||||
popoverContentClassName?: string;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
@@ -107,7 +109,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -118,23 +120,24 @@ export const SelectWithSearch = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm",
|
||||
"truncate text-muted-foreground text-sm",
|
||||
selectedOption && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={cn("z-[10001] p-0", popoverContentClassName)}
|
||||
align="center"
|
||||
align={props.align || "center"}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
|
||||
export function CopyTextButton(props: {
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
tooltip: string;
|
||||
tooltip: string | undefined;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
variant?:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
|
||||
const links: {
|
||||
name: string;
|
||||
segment: string;
|
||||
}[] = [
|
||||
{ name: "Transaction Simulator", segment: "transaction-simulator" },
|
||||
{ name: "Wei Converter", segment: "wei-converter" },
|
||||
{ name: "Hex Converter", segment: "hex-converter" },
|
||||
{ name: "Unix Time Converter", segment: "unixtime-converter" },
|
||||
{ name: "Keccak-256 Converter", segment: "keccak256-converter" },
|
||||
];
|
||||
|
||||
export function ToolsTabs() {
|
||||
const layoutSegment = useSelectedLayoutSegment() || "";
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={links.map((tab) => ({
|
||||
name: tab.name,
|
||||
href: `/tools/${tab.segment}`,
|
||||
isActive: layoutSegment === tab.segment,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { Metadata } from "next";
|
||||
import { ToolsTabs } from "./components/tabs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "thirdweb Blockchain Tools",
|
||||
@@ -11,13 +11,31 @@ export default function ToolLayout({
|
||||
children,
|
||||
}: { children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="flex h-full flex-col gap-8">
|
||||
<main className="container flex-1 pb-20">
|
||||
<div className="h-14" />
|
||||
<ToolsTabs />
|
||||
<div className="h-8" />
|
||||
<div>{children}</div>
|
||||
</main>
|
||||
</section>
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
label: "Transaction Simulator",
|
||||
href: "/tools/transaction-simulator",
|
||||
},
|
||||
{
|
||||
label: "Wei Converter",
|
||||
href: "/tools/wei-converter",
|
||||
},
|
||||
{
|
||||
label: "Hex Converter",
|
||||
href: "/tools/hex-converter",
|
||||
},
|
||||
{
|
||||
label: "Unix Time Converter",
|
||||
href: "/tools/unixtime-converter",
|
||||
},
|
||||
{
|
||||
label: "Keccak-256 Converter",
|
||||
href: "/tools/keccak256-converter",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,118 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
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";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { ShareButton } from "../../components/share";
|
||||
import {
|
||||
formatDistanceToNow,
|
||||
formatISO9075,
|
||||
formatRFC3339,
|
||||
formatRFC7231,
|
||||
fromUnixTime,
|
||||
} from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export const UnixTimeConverter = () => {
|
||||
const getCurrentUnixMilliseconds = () => Math.floor(new Date().getTime());
|
||||
const [unix, setUnix] = useState<number>(getCurrentUnixMilliseconds());
|
||||
export function UnixTimeConverter() {
|
||||
const [unixTime, setUnixTime] = useState(() => {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
});
|
||||
|
||||
// `unix` may be in milliseconds or seconds.
|
||||
// Treat any value < 100000000000 as seconds, otherwise milliseconds.
|
||||
const isSeconds = unix < 100000000000;
|
||||
const date = new Date(isSeconds ? unix * 1000 : unix);
|
||||
const dateLocal = date.toLocaleString(undefined, {
|
||||
timeZoneName: "short",
|
||||
});
|
||||
const dateUTC = date.toLocaleString(undefined, {
|
||||
timeZoneName: "short",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
const { date, error } = useMemo(() => {
|
||||
if (Number.isNaN(unixTime) || !Number.isInteger(unixTime)) {
|
||||
return {
|
||||
error: "Invalid Unix Timestamp",
|
||||
date: null,
|
||||
};
|
||||
}
|
||||
|
||||
const date = fromUnixTime(unixTime);
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
return {
|
||||
date: null,
|
||||
error: "Invalid Unix Timestamp",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
date,
|
||||
error: null,
|
||||
};
|
||||
}, [unixTime]);
|
||||
|
||||
return (
|
||||
<div className="space-y-24">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label htmlFor="unix-input" className="min-w-64 text-xl">
|
||||
Unix Time
|
||||
</Label>
|
||||
<Input
|
||||
id="unix-input"
|
||||
type="number"
|
||||
value={unix}
|
||||
onChange={(e) => {
|
||||
if (!Number.isNaN(e.target.valueAsNumber)) {
|
||||
setUnix(e.target.valueAsNumber);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a Unix time in milliseconds or seconds"
|
||||
className="p-6 text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label className="min-w-64 text-xl">Time (local)</Label>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={dateLocal}
|
||||
textToShow={dateLocal}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm italic">
|
||||
← {formatDistanceToNow(date, { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label className="min-w-64 text-xl">Time (UTC)</Label>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={dateUTC}
|
||||
textToShow={dateUTC}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<ShareButton
|
||||
cta="Share on X"
|
||||
href="https://twitter.com/intent/tweet?text=Easy-to-use%20Unix%20time%20converter%20by%20thirdweb%20%F0%9F%98%8D&url=https%3A%2F%2Fthirdweb.com%2Ftools%2Funixtime-converter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader className="mb-6 border-b">
|
||||
<CardTitle className="font-bold text-2xl">
|
||||
Unix Time Converter
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h1 className="font-bold text-2xl">
|
||||
Convert Unix time in milliseconds or seconds
|
||||
</h1>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="unixTime">Enter Unix Timestamp</Label>
|
||||
<Input
|
||||
id="unixTime"
|
||||
type="number"
|
||||
value={unixTime}
|
||||
onChange={(e) => {
|
||||
setUnixTime(Number(e.target.value));
|
||||
}}
|
||||
placeholder="e.g., 1609459200"
|
||||
className="mt-1"
|
||||
/>
|
||||
{error && (
|
||||
<p className="mt-2 text-destructive-text text-sm">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Timestamps are often represented in Unix time for several key reasons:
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Simplicity</h2>
|
||||
<p>
|
||||
Counts seconds since January 1, 1970, making it easy to understand and
|
||||
manipulate.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Consistency</h2>
|
||||
<p>
|
||||
Provides a uniform time representation across systems, unaffected by
|
||||
time zones or calendar changes.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Standardization</h2>
|
||||
<p>
|
||||
Widely supported in computing, ensuring compatibility across
|
||||
platforms.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Calculation</h2>
|
||||
<p>
|
||||
Allows straightforward arithmetic operations for time differences.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Efficiency</h2>
|
||||
<p>
|
||||
Stores time as a single integer, conserving memory and computational
|
||||
resources.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Interoperability</h2>
|
||||
<p>Facilitates seamless data exchange between systems and languages.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label>Relative Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date
|
||||
? formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Local Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatLocal(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>UTC Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatUTC(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>ISO 8601</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatISO8601(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>ISO 9075</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatISO9075(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>RFC 7231</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatRFC7231(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>RFC 3339</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatRFC3339(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>SQL Timestamp</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatSQL(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function formatLocal(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatUTC(date: Date): string {
|
||||
return date.toUTCString();
|
||||
}
|
||||
|
||||
function isValidDate(date: Date): boolean {
|
||||
return !Number.isNaN(date.getTime());
|
||||
}
|
||||
|
||||
function formatISO8601(date: Date): string {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
function formatSQL(date: Date): string {
|
||||
return date.toISOString().slice(0, 19).replace("T", " ");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ClientOnly } from "../../../../components/ClientOnly/ClientOnly";
|
||||
import { UnixTimeConverter } from "./components/UnixTimeConverter";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -7,5 +8,9 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return <UnixTimeConverter />;
|
||||
return (
|
||||
<ClientOnly ssr={null}>
|
||||
<UnixTimeConverter />
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function TotalSponsoredChartCardUI({
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
isCurrency
|
||||
title={title || "Total Sponsored"}
|
||||
title={title || "Gas Sponsored"}
|
||||
chartConfig={chartConfig}
|
||||
data={timeSeriesData}
|
||||
activeChart={
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
|
||||
import type { TransactionStats } from "types/analytics";
|
||||
import { EmptyAccountAbstractionChartContent } from "../../../../../components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
|
||||
import { BarChart } from "../../../components/Analytics/BarChart";
|
||||
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
|
||||
|
||||
export async function TransactionsChartCardUI({
|
||||
data,
|
||||
aggregatedData,
|
||||
searchParams,
|
||||
className,
|
||||
onlyMainnet,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
data: TransactionStats[];
|
||||
aggregatedData: TransactionStats[];
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
className?: string;
|
||||
onlyMainnet?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const uniqueChainIds = [
|
||||
...new Set(data.map((item) => item.chainId).filter(Boolean)),
|
||||
];
|
||||
const chains = await Promise.all(
|
||||
uniqueChainIds.map((chainId) =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(Number(chainId))).catch(() => undefined),
|
||||
),
|
||||
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
|
||||
|
||||
// Process data to combine by date and chain type
|
||||
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
|
||||
for (const item of data) {
|
||||
const chain = chains.find((c) => c.chainId === Number(item.chainId));
|
||||
|
||||
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
|
||||
if (chain?.testnet) {
|
||||
existing.testnet += item.count;
|
||||
} else {
|
||||
existing.mainnet += item.count;
|
||||
}
|
||||
dateMap.set(item.date, existing);
|
||||
}
|
||||
|
||||
// Convert to array and sort by date
|
||||
const timeSeriesData = Array.from(dateMap.entries())
|
||||
.map(([date, values]) => ({
|
||||
date,
|
||||
mainnet: values.mainnet,
|
||||
testnet: values.testnet,
|
||||
total: values.mainnet + values.testnet,
|
||||
}))
|
||||
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
|
||||
const processedAggregatedData = {
|
||||
mainnet: aggregatedData
|
||||
.filter(
|
||||
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.count, 0),
|
||||
testnet: aggregatedData
|
||||
.filter(
|
||||
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.count, 0),
|
||||
total: aggregatedData.reduce((acc, curr) => acc + curr.count, 0),
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
mainnet: {
|
||||
label: "Mainnet Chains",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
testnet: {
|
||||
label: "Testnet Chains",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
total: {
|
||||
label: "All Chains",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
};
|
||||
|
||||
if (onlyMainnet) {
|
||||
const filteredData = timeSeriesData.filter((d) => d.mainnet > 0);
|
||||
return (
|
||||
<div className={cn("rounded-lg border p-4 lg:p-6", className)}>
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
{title || "Transactions"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground"> {description}</p>
|
||||
<BarChart
|
||||
chartConfig={chartConfig}
|
||||
data={filteredData}
|
||||
activeKey="mainnet"
|
||||
emptyChartContent={<EmptyAccountAbstractionChartContent />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
title={title || "Transactions"}
|
||||
chartConfig={chartConfig}
|
||||
data={timeSeriesData}
|
||||
activeChart={
|
||||
(searchParams?.client_transactions as keyof typeof chartConfig) ??
|
||||
"mainnet"
|
||||
}
|
||||
queryKey="client_transactions"
|
||||
existingQueryParams={searchParams}
|
||||
aggregateFn={(_data, key) => processedAggregatedData[key]}
|
||||
className={className}
|
||||
// Get the trend from the last two COMPLETE periods
|
||||
trendFn={(data, key) =>
|
||||
data.filter((d) => (d[key] as number) > 0).length >= 3
|
||||
? ((data[data.length - 2]?.[key] as number) ?? 0) /
|
||||
((data[data.length - 3]?.[key] as number) ?? 0) -
|
||||
1
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getWalletConnections } from "@/api/analytics";
|
||||
import { type Project, getProjects } from "@/api/projects";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { Changelog } from "components/dashboard/Changelog";
|
||||
import { subDays } from "date-fns";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TeamProjectsPage } from "./~/projects/TeamProjectsPage";
|
||||
import {
|
||||
type ProjectWithAnalytics,
|
||||
TeamProjectsPage,
|
||||
} from "./~/projects/TeamProjectsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string }>;
|
||||
@@ -15,12 +20,13 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const projects = await getProjects(params.team_slug);
|
||||
const projectsWithTotalWallets = await getProjectsWithAnalytics(projects);
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
|
||||
<div className="flex grow flex-col">
|
||||
<h1 className="mb-4 font-semibold text-2xl tracking-tight">Projects</h1>
|
||||
<TeamProjectsPage projects={projects} team={team} />
|
||||
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[320px]">
|
||||
<h2 className="mb-4 font-semibold text-2xl tracking-tight">
|
||||
@@ -31,3 +37,38 @@ export default async function Page(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function getProjectsWithAnalytics(
|
||||
projects: Project[],
|
||||
): Promise<Array<ProjectWithAnalytics>> {
|
||||
return Promise.all(
|
||||
projects.map(async (p) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = subDays(today, 30);
|
||||
|
||||
const data = await getWalletConnections({
|
||||
clientId: p.publishableKey,
|
||||
period: "all",
|
||||
from: thirtyDaysAgo,
|
||||
to: today,
|
||||
});
|
||||
|
||||
let uniqueWalletsConnected = 0;
|
||||
for (const d of data) {
|
||||
uniqueWalletsConnected += d.uniqueWalletsConnected;
|
||||
}
|
||||
|
||||
return {
|
||||
...p,
|
||||
monthlyActiveUsers: uniqueWalletsConnected,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
...p,
|
||||
monthlyActiveUsers: 0,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
getClientTransactions,
|
||||
getInAppWalletUsage,
|
||||
getUserOpUsage,
|
||||
getWalletConnections,
|
||||
@@ -31,9 +32,10 @@ import { getValidAccount } from "app/account/settings/getAccount";
|
||||
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
|
||||
import { Suspense } from "react";
|
||||
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
|
||||
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
|
||||
|
||||
// revalidate every 5 minutes
|
||||
export const revalidate = 300;
|
||||
export const maxDuration = 300;
|
||||
|
||||
type SearchParams = {
|
||||
usersChart?: string;
|
||||
@@ -100,6 +102,8 @@ async function OverviewPageContent(props: {
|
||||
inAppWalletUsage,
|
||||
userOpUsageTimeSeries,
|
||||
userOpUsage,
|
||||
clientTransactionsTimeSeries,
|
||||
clientTransactions,
|
||||
] = await Promise.all([
|
||||
// Aggregated wallet connections
|
||||
getWalletConnections({
|
||||
@@ -135,6 +139,19 @@ async function OverviewPageContent(props: {
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
// Client transactions
|
||||
getClientTransactions({
|
||||
accountId: account.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}),
|
||||
getClientTransactions({
|
||||
accountId: account.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
]);
|
||||
|
||||
const isEmpty =
|
||||
@@ -180,6 +197,14 @@ async function OverviewPageContent(props: {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{clientTransactions.length > 0 && (
|
||||
<TransactionsChartCardUI
|
||||
searchParams={searchParams}
|
||||
data={clientTransactionsTimeSeries}
|
||||
aggregatedData={clientTransactions}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
/>
|
||||
)}
|
||||
{userOpUsage.length > 0 ? (
|
||||
<TotalSponsoredChartCardUI
|
||||
searchParams={searchParams}
|
||||
@@ -189,7 +214,7 @@ async function OverviewPageContent(props: {
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateCard
|
||||
metric="Sponsored Transactions"
|
||||
metric="Gas Sponsored"
|
||||
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -35,7 +35,10 @@ import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { EngineBackendWalletOptions } from "lib/engine";
|
||||
import { useActiveChainAsDashboardChain } from "lib/v5-adapter";
|
||||
import {
|
||||
useActiveChainAsDashboardChain,
|
||||
useV5DashboardChain,
|
||||
} from "lib/v5-adapter";
|
||||
import {
|
||||
DownloadIcon,
|
||||
PencilIcon,
|
||||
@@ -59,6 +62,7 @@ interface BackendWalletsTableProps {
|
||||
isPending: boolean;
|
||||
isFetched: boolean;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
interface BackendWalletDashboard extends BackendWallet {
|
||||
@@ -72,19 +76,21 @@ interface BackendWalletBalanceCellProps {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
});
|
||||
const chain = useActiveChainAsDashboardChain();
|
||||
const chain = useV5DashboardChain(chainId);
|
||||
if (!chain || !backendWalletBalance) {
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +106,7 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
</Text>
|
||||
);
|
||||
|
||||
const explorer = chain?.explorers?.[0];
|
||||
const explorer = chain.blockExplorers?.[0];
|
||||
if (!explorer) {
|
||||
return balanceComponent;
|
||||
}
|
||||
@@ -123,6 +129,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
isPending,
|
||||
isFetched,
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const editDisclosure = useDisclosure();
|
||||
const receiveDisclosure = useDisclosure();
|
||||
@@ -163,13 +170,14 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
instanceUrl={instanceUrl}
|
||||
address={address}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
id: "balance",
|
||||
}),
|
||||
];
|
||||
}, [instanceUrl, authToken]);
|
||||
}, [instanceUrl, authToken, chainId]);
|
||||
|
||||
const [selectedBackendWallet, setSelectedBackendWallet] =
|
||||
useState<BackendWallet>();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineBackendWallets,
|
||||
useEngineWalletConfig,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { BackendWalletsTable } from "./backend-wallets-table";
|
||||
import { CreateBackendWalletButton } from "./create-backend-wallet-button";
|
||||
import { ImportBackendWalletButton } from "./import-backend-wallet-button";
|
||||
@@ -43,6 +45,10 @@ function BackendWalletsSection(props: {
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instance, teamSlug, authToken } = props;
|
||||
const activeWalletChain = useActiveWalletChain();
|
||||
const [_chainId, setChainId] = useState<number>();
|
||||
const chainId = _chainId || activeWalletChain?.id || 1;
|
||||
|
||||
const backendWallets = useEngineBackendWallets({
|
||||
instanceUrl: instance.url,
|
||||
authToken,
|
||||
@@ -105,9 +111,14 @@ function BackendWalletsSection(props: {
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Show balance for</span>
|
||||
{/* TODO - Replace with simple network selector - there's no need for user to switch chain */}
|
||||
<div className="flex flex-row">
|
||||
<NetworkSelectorButton />
|
||||
<SingleNetworkSelector
|
||||
chainId={chainId}
|
||||
onChange={setChainId}
|
||||
className="min-w-40 max-w-52 lg:max-w-60"
|
||||
popoverContentClassName="!w-[80vw] md:!w-[500px]"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,6 +131,7 @@ function BackendWalletsSection(props: {
|
||||
isPending={backendWallets.isPending}
|
||||
isFetched={backendWallets.isFetched}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Team } from "@/api/team";
|
||||
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
|
||||
import { CopyButton } from "@/components/ui/CopyButton";
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -11,6 +12,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,51 +25,90 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
EllipsisVerticalIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type SortById = "name" | "createdAt";
|
||||
type SortById = "name" | "createdAt" | "monthlyActiveUsers";
|
||||
|
||||
export type ProjectWithAnalytics = Project & {
|
||||
monthlyActiveUsers: number;
|
||||
};
|
||||
|
||||
export function TeamProjectsPage(props: {
|
||||
projects: Project[];
|
||||
projects: ProjectWithAnalytics[];
|
||||
team: Team;
|
||||
}) {
|
||||
const { projects } = props;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortBy, setSortBy] = useState<SortById>("createdAt");
|
||||
const [sortBy, setSortBy] = useState<SortById>("monthlyActiveUsers");
|
||||
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
|
||||
useState(false);
|
||||
const router = useDashboardRouter();
|
||||
|
||||
let projectsToShow = !searchTerm
|
||||
? projects
|
||||
: projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.publishableKey
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
const sortedProjects = useMemo(() => {
|
||||
let _projectsToShow = !searchTerm
|
||||
? projects
|
||||
: projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.publishableKey
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
if (sortBy === "name") {
|
||||
projectsToShow = projectsToShow.sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
} else if (sortBy === "createdAt") {
|
||||
projectsToShow = projectsToShow.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
}
|
||||
if (sortBy === "name") {
|
||||
_projectsToShow = _projectsToShow.sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
} else if (sortBy === "createdAt") {
|
||||
_projectsToShow = _projectsToShow.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
} else if (sortBy === "monthlyActiveUsers") {
|
||||
_projectsToShow = _projectsToShow.sort(
|
||||
(a, b) => b.monthlyActiveUsers - a.monthlyActiveUsers,
|
||||
);
|
||||
}
|
||||
|
||||
return _projectsToShow;
|
||||
}, [searchTerm, sortBy, projects]);
|
||||
|
||||
const pageSize = 8;
|
||||
const [page, setPage] = useState(1);
|
||||
const paginatedProjects = sortedProjects.slice(
|
||||
(page - 1) * pageSize,
|
||||
page * pageSize,
|
||||
);
|
||||
|
||||
const showPagination = sortedProjects.length > pageSize;
|
||||
const totalPages = Math.ceil(sortedProjects.length / pageSize);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
{/* Filters + Add New */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
<SearchInput value={searchTerm} onValueChange={setSearchTerm} />
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
onValueChange={(v) => {
|
||||
setSearchTerm(v);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<SelectBy value={sortBy} onChange={setSortBy} />
|
||||
<SelectBy
|
||||
value={sortBy}
|
||||
onChange={(v) => {
|
||||
setSortBy(v);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<AddNewButton
|
||||
createProject={() => setIsCreateProjectDialogOpen(true)}
|
||||
teamMembersSettingsPath={`/team/${props.team.slug}/~/settings/members`}
|
||||
@@ -74,23 +119,33 @@ export function TeamProjectsPage(props: {
|
||||
<div className="h-6" />
|
||||
|
||||
{/* Projects */}
|
||||
{projectsToShow.length === 0 ? (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects created</p>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setIsCreateProjectDialogOpen(true)}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Create a Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{paginatedProjects.length === 0 ? (
|
||||
<>
|
||||
{searchTerm !== "" ? (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects found</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects created</p>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setIsCreateProjectDialogOpen(true)}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Create a Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{projectsToShow.map((project) => {
|
||||
{paginatedProjects.map((project) => {
|
||||
return (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
@@ -102,6 +157,16 @@ export function TeamProjectsPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPagination && (
|
||||
<div className="py-6">
|
||||
<PaginationButtons
|
||||
activePage={page}
|
||||
onPageClick={setPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={isCreateProjectDialogOpen}
|
||||
onOpenChange={setIsCreateProjectDialogOpen}
|
||||
@@ -118,7 +183,7 @@ export function TeamProjectsPage(props: {
|
||||
}
|
||||
|
||||
function ProjectCard(props: {
|
||||
project: Project;
|
||||
project: ProjectWithAnalytics;
|
||||
team_slug: string;
|
||||
}) {
|
||||
const { project, team_slug } = props;
|
||||
@@ -130,34 +195,51 @@ function ProjectCard(props: {
|
||||
{/* TODO - set image */}
|
||||
<ProjectAvatar className="size-10 rounded-full" src="" />
|
||||
|
||||
<div className="flex-grow flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Link
|
||||
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
|
||||
// remove /connect when we have overview page
|
||||
href={`/team/${team_slug}/${project.slug}`}
|
||||
>
|
||||
<h2 className="font-medium text-base">{project.name}</h2>
|
||||
</Link>
|
||||
<CopyButton
|
||||
text={project.publishableKey}
|
||||
iconClassName="z-10 size-3"
|
||||
className="!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-1">
|
||||
<Link
|
||||
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
|
||||
// remove /connect when we have overview page
|
||||
href={`/team/${team_slug}/${project.slug}`}
|
||||
>
|
||||
<h2 className="font-medium text-base">{project.name}</h2>
|
||||
</Link>
|
||||
|
||||
<p className="flex items-center text-muted-foreground text-sm">
|
||||
{truncate(project.publishableKey, 32)}
|
||||
<p className="flex items-center gap-1 text-muted-foreground text-sm">
|
||||
<span>{project.monthlyActiveUsers}</span>
|
||||
Monthly Active Users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="z-10 h-auto w-auto p-2" variant="ghost">
|
||||
<EllipsisVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[180px] p-1">
|
||||
<CopyTextButton
|
||||
textToCopy={project.publishableKey}
|
||||
textToShow="Copy Client ID"
|
||||
copyIconPosition="right"
|
||||
tooltip={undefined}
|
||||
variant="ghost"
|
||||
className="flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start gap-3"
|
||||
asChild
|
||||
>
|
||||
<Link href={`/team/${team_slug}/${project.slug}/settings`}>
|
||||
Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function truncate(str: string, stringLimit: number) {
|
||||
return str.length > stringLimit ? `${str.slice(0, stringLimit)}...` : str;
|
||||
}
|
||||
|
||||
function SearchInput(props: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
@@ -209,10 +291,11 @@ function SelectBy(props: {
|
||||
value: SortById;
|
||||
onChange: (value: SortById) => void;
|
||||
}) {
|
||||
const values: SortById[] = ["name", "createdAt"];
|
||||
const values: SortById[] = ["name", "createdAt", "monthlyActiveUsers"];
|
||||
const valueToLabel: Record<SortById, string> = {
|
||||
name: "Name",
|
||||
createdAt: "Creation Date",
|
||||
monthlyActiveUsers: "Monthly Active Users",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -223,7 +306,12 @@ function SelectBy(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-[200px] bg-card capitalize">
|
||||
Sort by {valueToLabel[props.value]}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="!hidden lg:!inline text-muted-foreground">
|
||||
Sort by
|
||||
</span>
|
||||
{valueToLabel[props.value]}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{values.map((value) => (
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { defineChain, getContract } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { TransactionsChartCardUI } from "../../../(team)/_components/TransactionsCard";
|
||||
import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server";
|
||||
import { fetchDashboardContractMetadata } from "../../../../../../@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import type { TransactionStats } from "../../../../../../types/analytics";
|
||||
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
|
||||
|
||||
export function TransactionsChartsUI({
|
||||
data,
|
||||
aggregatedData,
|
||||
searchParams,
|
||||
}: {
|
||||
data: TransactionStats[];
|
||||
aggregatedData: TransactionStats[];
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<TransactionsChartCardUI
|
||||
searchParams={searchParams}
|
||||
data={data}
|
||||
aggregatedData={aggregatedData}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
/>
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
<ChainDistributionCard data={aggregatedData} />
|
||||
<ContractDistributionCard data={aggregatedData} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.chainId] = (acc[curr.chainId] || 0) + curr.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(key));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
return {
|
||||
label: chainMeta?.slug || chain.id.toString(),
|
||||
value,
|
||||
link: `/${chain.id}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const aggregateFn = () => new Set(data.map((d) => `${d.chainId}`)).size;
|
||||
|
||||
return (
|
||||
<PieChartCard title="Chains" data={reducedData} aggregateFn={aggregateFn} />
|
||||
);
|
||||
}
|
||||
|
||||
async function ContractDistributionCard({
|
||||
data,
|
||||
}: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
acc[`${curr.chainId}:${curr.contractAddress}`] =
|
||||
(acc[`${curr.chainId}:${curr.contractAddress}`] || 0) +
|
||||
curr.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
const [chainId, contractAddress] = key.split(":");
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(chainId));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
const contractData = await fetchDashboardContractMetadata(
|
||||
getContract({
|
||||
chain,
|
||||
address: contractAddress as string, // we filter above
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
).catch(() => undefined);
|
||||
return {
|
||||
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
|
||||
link: `/${chainId}/${contractAddress}`,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const aggregateFn = () =>
|
||||
new Set(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
.map((d) => `${d.chainId}:${d.contractAddress}`),
|
||||
).size;
|
||||
|
||||
return (
|
||||
<PieChartCard
|
||||
title="Contracts"
|
||||
data={reducedData}
|
||||
aggregateFn={aggregateFn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { LoadingChartState } from "components/analytics/empty-chart-state";
|
||||
import { Suspense } from "react";
|
||||
import type { AnalyticsQueryParams } from "types/analytics";
|
||||
import { getClientTransactions } from "../../../../../../@/api/analytics";
|
||||
import { TransactionsChartsUI } from "./TransactionCharts";
|
||||
|
||||
export function TransactionsCharts(
|
||||
props: AnalyticsQueryParams & {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
},
|
||||
) {
|
||||
return (
|
||||
// TODO: Add better LoadingChartState
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-[400px]">
|
||||
<LoadingChartState />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TransactionsChartCardAsync {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function TransactionsChartCardAsync(
|
||||
props: AnalyticsQueryParams & {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
},
|
||||
) {
|
||||
const [data, aggregatedData] = await Promise.all([
|
||||
getClientTransactions(props),
|
||||
getClientTransactions({
|
||||
...props,
|
||||
period: "all",
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!aggregatedData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransactionsChartsUI
|
||||
data={data}
|
||||
aggregatedData={aggregatedData}
|
||||
searchParams={props.searchParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChar
|
||||
import { EmptyState } from "../../components/Analytics/EmptyState";
|
||||
import { PieChartCard } from "../../components/Analytics/PieChartCard";
|
||||
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
|
||||
import { TransactionsCharts } from "./components/Transactions";
|
||||
|
||||
interface PageParams {
|
||||
team_slug: string;
|
||||
@@ -49,6 +50,8 @@ interface PageProps {
|
||||
searchParams: Promise<PageSearchParams>;
|
||||
}
|
||||
|
||||
export const maxDuration = 300;
|
||||
|
||||
export default async function ProjectOverviewPage(props: PageProps) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
@@ -174,12 +177,6 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/connect/quickstart"
|
||||
/>
|
||||
)}
|
||||
<RpcMethodBarChartCard
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
{walletConnections.status === "fulfilled" &&
|
||||
walletConnections.value.length > 0 ? (
|
||||
@@ -200,6 +197,13 @@ async function ProjectAnalytics(props: {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<TransactionsCharts
|
||||
searchParams={searchParams}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
{userOpUsageTimeSeries.status === "fulfilled" &&
|
||||
userOpUsage.status === "fulfilled" &&
|
||||
userOpUsage.value.length > 0 ? (
|
||||
@@ -216,6 +220,12 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
|
||||
/>
|
||||
)}
|
||||
<RpcMethodBarChartCard
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -417,7 +427,7 @@ async function TotalSponsoredCard({
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
isCurrency
|
||||
title="Total Sponsored"
|
||||
title="Gas Sponsored"
|
||||
chartConfig={chartConfig}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
data={timeSeriesData}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getTeamBySlug } from "@/api/team";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TWAutoConnect } from "../../components/autoconnect";
|
||||
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
|
||||
|
||||
export default async function RootTeamLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -19,6 +20,7 @@ export default async function RootTeamLayout(props: {
|
||||
<div className="flex grow flex-col">{props.children}</div>
|
||||
<TWAutoConnect />
|
||||
<AppFooter />
|
||||
<SaveLastVisitedTeamPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { PieChart } from "./PieChart";
|
||||
import { Stat } from "./Stat";
|
||||
|
||||
@@ -11,6 +12,7 @@ type ChartData = {
|
||||
value: number;
|
||||
label: string;
|
||||
fill?: string;
|
||||
link?: string;
|
||||
};
|
||||
export function PieChartCard({
|
||||
title,
|
||||
@@ -22,11 +24,11 @@ export function PieChartCard({
|
||||
aggregateFn?: (data: ChartData[]) => number;
|
||||
}) {
|
||||
const processedData = (() => {
|
||||
if (data.length <= 9) return data;
|
||||
|
||||
// Sort by value descending
|
||||
const sorted = [...data].sort((a, b) => b.value - a.value);
|
||||
|
||||
if (sorted.length <= 9) return sorted;
|
||||
|
||||
// Take top 9
|
||||
const top10 = sorted.slice(0, 9).map((item) => ({
|
||||
...item,
|
||||
@@ -62,13 +64,27 @@ export function PieChartCard({
|
||||
<CardFooter className="no-scrollbar flex max-w-full justify-center p-6 pt-0 max-md:overflow-x-auto">
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2.5">
|
||||
{processedData.map(
|
||||
({ label, fill }: { label: string; fill?: string }) => (
|
||||
({
|
||||
label,
|
||||
fill,
|
||||
link,
|
||||
}: { label: string; fill?: string; link?: string }) => (
|
||||
<div key={fill} className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-2 rounded-full"
|
||||
style={{ background: fill }}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
{link ? (
|
||||
<Link
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="text-muted-foreground text-xs hover:text-link-foreground"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { setCookie } from "../../../../lib/cookie";
|
||||
import { LAST_VISITED_TEAM_PAGE_PATH } from "./consts";
|
||||
|
||||
export function SaveLastVisitedTeamPage() {
|
||||
const pathname = usePathname();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
setCookie(LAST_VISITED_TEAM_PAGE_PATH, pathname);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Only keep constants here - this file is imported in middleware too
|
||||
|
||||
export const LAST_VISITED_TEAM_PAGE_PATH = "last-visited-team-page-path";
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -24,7 +26,6 @@ import {
|
||||
useAllContractList,
|
||||
} from "@3rdweb-sdk/react/hooks/useRegistry";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -82,17 +83,22 @@ const importFormSchema = z.object({
|
||||
message: "Invalid contract address",
|
||||
},
|
||||
),
|
||||
chainId: z.coerce.number(),
|
||||
});
|
||||
|
||||
function ImportForm() {
|
||||
const router = useDashboardRouter();
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const chainSlug = useChainSlug(chainId || 1);
|
||||
const activeChainId = useActiveWalletChain()?.id;
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(importFormSchema),
|
||||
values: {
|
||||
contractAddress: "",
|
||||
chainId: activeChainId || 1,
|
||||
},
|
||||
});
|
||||
const chainSlug = useChainSlug(form.watch("chainId"));
|
||||
const addToDashboard = useAddContractMutation();
|
||||
const address = useActiveAccount()?.address;
|
||||
const registry = useAllContractList(address);
|
||||
@@ -107,9 +113,7 @@ function ImportForm() {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
if (!chainId) {
|
||||
throw new Error("No chain ID");
|
||||
}
|
||||
const { chainId } = data;
|
||||
let contractAddress: string;
|
||||
|
||||
try {
|
||||
@@ -199,7 +203,11 @@ function ImportForm() {
|
||||
<div className="h-3" />
|
||||
<div>
|
||||
<Label className="mb-3 inline-block">Network</Label>
|
||||
<NetworkSelectorButton />
|
||||
<SingleNetworkSelector
|
||||
chainId={form.watch("chainId")}
|
||||
onChange={(v) => form.setValue("chainId", v)}
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Input,
|
||||
} from "@chakra-ui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { AbiFunction, AbiParameter } from "abitype";
|
||||
import { type AbiFunction, type AbiParameter, formatAbiItem } from "abitype";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
|
||||
@@ -28,13 +28,12 @@ import {
|
||||
type ThirdwebContract,
|
||||
prepareContractCall,
|
||||
readContract,
|
||||
resolveMethod,
|
||||
simulateTransaction,
|
||||
toSerializableTransaction,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { parseAbiParams, stringify } from "thirdweb/utils";
|
||||
import { parseAbiParams, stringify, toFunctionSelector } from "thirdweb/utils";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -135,7 +134,12 @@ interface InteractiveAbiFunctionProps {
|
||||
twAccount: Account | undefined;
|
||||
}
|
||||
|
||||
function useAsyncRead(contract: ThirdwebContract, functionName: string) {
|
||||
function useAsyncRead(contract: ThirdwebContract, abiFunction: AbiFunction) {
|
||||
const formattedAbi = formatAbiItem({
|
||||
...abiFunction,
|
||||
type: "function",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
} as any);
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
args,
|
||||
@@ -144,7 +148,8 @@ function useAsyncRead(contract: ThirdwebContract, functionName: string) {
|
||||
const params = parseAbiParams(types, args);
|
||||
return readContract({
|
||||
contract,
|
||||
method: resolveMethod(functionName),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing
|
||||
method: formattedAbi as any,
|
||||
params,
|
||||
});
|
||||
},
|
||||
@@ -156,21 +161,32 @@ function useSimulateTransaction() {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
contract,
|
||||
functionName,
|
||||
abiFunction,
|
||||
params,
|
||||
value,
|
||||
}: {
|
||||
contract: ThirdwebContract;
|
||||
functionName: string;
|
||||
abiFunction: AbiFunction;
|
||||
params: unknown[];
|
||||
value?: bigint;
|
||||
}) => {
|
||||
if (!from) {
|
||||
return toast.error("No account connected");
|
||||
}
|
||||
const formattedAbi = formatAbiItem({
|
||||
...abiFunction,
|
||||
type: "function",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
} as any);
|
||||
console.log(
|
||||
"formattedAbi",
|
||||
formattedAbi,
|
||||
toFunctionSelector(abiFunction),
|
||||
);
|
||||
const transaction = prepareContractCall({
|
||||
contract,
|
||||
method: resolveMethod(functionName),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing
|
||||
method: formattedAbi as any,
|
||||
params,
|
||||
value,
|
||||
});
|
||||
@@ -179,10 +195,16 @@ function useSimulateTransaction() {
|
||||
simulateTransaction({
|
||||
from,
|
||||
transaction,
|
||||
}).catch((e) => {
|
||||
console.error("Error simulating transaction", e);
|
||||
throw e;
|
||||
}),
|
||||
toSerializableTransaction({
|
||||
from,
|
||||
transaction,
|
||||
}).catch((e) => {
|
||||
console.error("Error serializing transaction", e);
|
||||
throw e;
|
||||
}),
|
||||
]);
|
||||
return `--- ✅ Simulation succeeded ---
|
||||
@@ -257,7 +279,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
data: readData,
|
||||
isPending: readLoading,
|
||||
error: readError,
|
||||
} = useAsyncRead(contract, abiFunction.name);
|
||||
} = useAsyncRead(contract, abiFunction);
|
||||
|
||||
const txSimulation = useSimulateTransaction();
|
||||
|
||||
@@ -311,12 +333,18 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
if (!abiFunction.name) {
|
||||
return toast.error("Cannot detect function name");
|
||||
}
|
||||
const formattedAbi = formatAbiItem({
|
||||
...abiFunction,
|
||||
type: "function",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
} as any);
|
||||
const types = abiFunction.inputs.map((o) => o.type);
|
||||
const formatted = formatContractCall(d.params);
|
||||
const params = parseAbiParams(types, formatted);
|
||||
const transaction = prepareContractCall({
|
||||
contract,
|
||||
method: resolveMethod(abiFunction.name),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing
|
||||
method: formattedAbi as any,
|
||||
params,
|
||||
value: d.value ? toWei(d.value) : undefined,
|
||||
});
|
||||
@@ -334,7 +362,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
txSimulation.mutate({
|
||||
contract,
|
||||
params,
|
||||
functionName: abiFunction.name,
|
||||
abiFunction,
|
||||
value: d.value ? toWei(d.value) : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function Changelog() {
|
||||
|
||||
async function getChangelog() {
|
||||
const res = await fetch(
|
||||
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=10",
|
||||
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=7",
|
||||
);
|
||||
const json = await res.json();
|
||||
return json.posts as ChangelogItem[];
|
||||
|
||||
@@ -24,6 +24,13 @@ export async function fetchAnalytics(
|
||||
decodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
// client id DEBUG OVERRIDE
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("clientId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("accountId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// "clientId",
|
||||
// "...",
|
||||
// );
|
||||
|
||||
return fetch(ANALYTICS_SERVICE_URL, {
|
||||
...init,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
export function getCookie(name: string) {
|
||||
try {
|
||||
const value = document.cookie;
|
||||
const cookies = value.split(";");
|
||||
|
||||
for (const c of cookies) {
|
||||
const [_name, _val] = c.trim().split("=");
|
||||
if (_name === name && _val) {
|
||||
return decodeURIComponent(_val);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setCookie(name: string, value: string, days = 365) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `${name}=${value};path=/;expires=${date.toUTCString()}`;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { isValidENSName } from "thirdweb/utils";
|
||||
import { LAST_VISITED_TEAM_PAGE_PATH } from "./app/team/components/last-visited-page/consts";
|
||||
import { defineDashboardChain } from "./lib/defineDashboardChain";
|
||||
|
||||
// ignore assets, api - only intercept page routes
|
||||
@@ -54,7 +55,6 @@ export async function middleware(request: NextRequest) {
|
||||
: null;
|
||||
|
||||
// utm collection
|
||||
// NOTE: this is not working for pages with rewrites in next.config.js - (framer pages)
|
||||
// if user is already signed in - don't bother capturing utm params
|
||||
if (!authCookie) {
|
||||
const searchParamsEntries = request.nextUrl.searchParams.entries();
|
||||
@@ -96,13 +96,15 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// remove '/' in front and then split by '/'
|
||||
|
||||
// if it's the homepage and we have an auth cookie, redirect to the dashboard
|
||||
if (paths.length === 1 && paths[0] === "" && authCookie) {
|
||||
const lastVisitedTeamPagePath = request.cookies.get(
|
||||
LAST_VISITED_TEAM_PAGE_PATH,
|
||||
)?.value;
|
||||
|
||||
return redirect(
|
||||
request,
|
||||
"/team",
|
||||
lastVisitedTeamPagePath || "/team",
|
||||
cookiesToSet
|
||||
? {
|
||||
cookiesToSet,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Store, useStore } from "@/lib/reactive";
|
||||
import { useEffect } from "react";
|
||||
import { getCookie, setCookie } from "../lib/cookie";
|
||||
|
||||
const loadedStoreKeys = new Set<string>();
|
||||
|
||||
@@ -51,27 +52,3 @@ export function SyncStoreToCookies<T>(props: {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
try {
|
||||
const value = document.cookie;
|
||||
const cookies = value.split(";");
|
||||
|
||||
for (const c of cookies) {
|
||||
const [_name, _val] = c.trim().split("=");
|
||||
if (_name === name && _val) {
|
||||
return decodeURIComponent(_val);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, days = 365) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `${name}=${value};path=/;expires=${date.toUTCString()}`;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ export interface UserOpStats {
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
export interface TransactionStats {
|
||||
date: string;
|
||||
chainId: number;
|
||||
contractAddress?: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RpcMethodStats {
|
||||
date: string;
|
||||
evmMethod: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { zkSyncSepolia } from "thirdweb/chains";
|
||||
@@ -15,6 +14,12 @@ import { shortenHex } from "thirdweb/utils";
|
||||
import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
import { WALLETS } from "../../lib/constants";
|
||||
|
||||
// const chain = abstractTestnet;
|
||||
// const editionDropContract = getContract({
|
||||
// client: THIRDWEB_CLIENT,
|
||||
// address: "0x8A24a7Df38fA5fCCcFD1259e90Fb6996fDdfcADa",
|
||||
// chain,
|
||||
// });
|
||||
const chain = zkSyncSepolia;
|
||||
const editionDropContract = getContract({
|
||||
client: THIRDWEB_CLIENT,
|
||||
@@ -83,6 +88,7 @@ export function SponsoredTxZksyncPreview() {
|
||||
metadata: nft?.metadata,
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error("error", error);
|
||||
alert(`Error: ${error.message}`);
|
||||
}}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
@@ -39,7 +38,9 @@ export function HooksPreview() {
|
||||
</>
|
||||
) : (
|
||||
<Button variant="default" onClick={connect}>
|
||||
Connect with Metamask
|
||||
{connectMutation.isConnecting
|
||||
? "Connecting..."
|
||||
: "Connect with Metamask"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@next/next/recommended",
|
||||
"plugin:storybook/recommended"
|
||||
"plugin:mdx/recommended",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"svg-jsx/camel-case-dash": "error"
|
||||
}
|
||||
"svg-jsx/camel-case-dash": "error",
|
||||
"tailwindcss/classnames-order": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.js", "*.tsx", "*.jsx"],
|
||||
"extends": ["biome"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"next": true,
|
||||
"project": ["src/**"],
|
||||
"ignoreBinaries": ["only-allow", "biome"],
|
||||
"ignore": ["src/components/ui/**"],
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/chains",
|
||||
"@thirdweb-dev/wallets",
|
||||
"thirdweb",
|
||||
"@types/mdx"
|
||||
],
|
||||
"entry": [
|
||||
"next.config.{js,ts,cjs,mjs}",
|
||||
"{instrumentation,middleware}.{js,ts}",
|
||||
"app/global-error.{js,jsx,ts,tsx}",
|
||||
"app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx,mdx}",
|
||||
"app/**/route.{js,jsx,ts,tsx}",
|
||||
"app/{manifest,sitemap,robots}.{js,ts}",
|
||||
"app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
|
||||
"app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
|
||||
"pages/**/*.{js,jsx,ts,tsx}",
|
||||
"src/{instrumentation,middleware}.{js,ts}",
|
||||
"src/app/global-error.{js,jsx,ts,tsx}",
|
||||
"src/app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx,mdx}",
|
||||
"src/app/**/route.{js,jsx,ts,tsx}",
|
||||
"src/app/{manifest,sitemap,robots}.{js,ts}",
|
||||
"src/app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
|
||||
"src/app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
|
||||
"src/pages/**/*.{js,jsx,ts,tsx}",
|
||||
"scripts/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -10,10 +10,12 @@
|
||||
"build": "next build",
|
||||
"postbuild": "pnpm run extract-search-data && pnpm next-sitemap",
|
||||
"start": "next start",
|
||||
"lint": "biome check ./src && eslint ./src",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"create-index": "pnpm tsx scripts/createEmptySearchIndex.ts",
|
||||
"extract-search-data": "pnpm tsx scripts/extractSearchData.ts"
|
||||
"extract-search-data": "pnpm tsx scripts/extractSearchData.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dirtycajunrice/klee": "^1.0.6",
|
||||
@@ -30,7 +32,6 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"lucide-react": "0.468.0",
|
||||
@@ -43,7 +44,6 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-html-parser": "2.0.2",
|
||||
"remark-gfm": "3.0.1",
|
||||
"semver": "^7.6.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
@@ -53,6 +53,7 @@
|
||||
"typedoc-better-json": "0.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.3",
|
||||
"@types/flexsearch": "^0.7.6",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.10.2",
|
||||
@@ -65,10 +66,10 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"eslint-plugin-mdx": "^3.1.5",
|
||||
"eslint-plugin-svg-jsx": "^1.2.4",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"knip": "5.41.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"tailwindcss": "3.4.17",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
import { unrealEngineRedirects } from "./src/app/unreal-engine/redirects.mjs";
|
||||
const unrealEngineRedirects = {
|
||||
"/unreal/:path*": "/unreal-engine/:path*",
|
||||
"/unreal-engine/cpp/wallet-handle": "/unreal-engine/cpp/wallet-handles",
|
||||
"/unreal-engine/blueprints/private-key-wallet":
|
||||
"unreal-engine/blueprints/in-app-wallet",
|
||||
};
|
||||
|
||||
const reactRedirects = {
|
||||
"/react": "/react/v4",
|
||||
@@ -981,6 +986,20 @@ const v5RestructuredRedirects = {
|
||||
"/typescript/v5/react-native/:path*": "/react-native/v5/:path*",
|
||||
};
|
||||
|
||||
const v4ToV5Redirects = {
|
||||
"/typescript/v4": "/typescript/v5",
|
||||
"/typescript/v4/:path*": "/typescript/v5",
|
||||
"/react/v4": "/react/v5",
|
||||
"/react/v4/:path*": "/react/v5",
|
||||
"/react-native/v0": "/react-native/v5",
|
||||
"/react-native/v0/:path*": "/react-native/v5",
|
||||
"/wallet-sdk/:path*": "/connect",
|
||||
"/storage-sdk/v2": "/typescript/v5/storage",
|
||||
"/storage-sdk/v2/:path*": "/typescript/v5/storage",
|
||||
"/unity/v4": "/unity/v5",
|
||||
"/unity/v4/:path*": "/unity/v5",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('next').NextConfig['redirects']}
|
||||
*/
|
||||
@@ -999,18 +1018,7 @@ export const redirects = async () => {
|
||||
...createRedirects(otherRedirects),
|
||||
...createRedirects(v5RestructuredRedirects),
|
||||
...createRedirects(unrealEngineRedirects),
|
||||
// references docs
|
||||
latestReference("react", "v4"),
|
||||
latestReference("react-native", "v0"),
|
||||
latestReference("typescript", "v5"),
|
||||
latestReference("wallets", "v2"),
|
||||
latestReference("storage", "v2"),
|
||||
// sdk docs
|
||||
latestSDK("react", "v4"),
|
||||
latestSDK("react-native", "v0"),
|
||||
latestSDK("typescript", "v5"),
|
||||
latestSDK("wallet-sdk", "v2"),
|
||||
latestSDK("storage-sdk", "v2"),
|
||||
...createRedirects(v4ToV5Redirects),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1026,31 +1034,3 @@ function createRedirects(linkMap, permanent = true) {
|
||||
}
|
||||
return redirects;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} pkg
|
||||
* @param {string} latestVersion
|
||||
* @returns
|
||||
*/
|
||||
function latestReference(pkg, latestVersion) {
|
||||
return {
|
||||
source: `/references/${pkg}/latest/:path*`,
|
||||
destination: `/references/${pkg}/${latestVersion}/:path*`,
|
||||
permanent: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} pkg
|
||||
* @param {string} latestVersion
|
||||
* @returns
|
||||
*/
|
||||
function latestSDK(pkg, latestVersion) {
|
||||
return {
|
||||
source: `/${pkg}/latest/:path*`,
|
||||
destination: `/${pkg}/${latestVersion}/:path*`,
|
||||
permanent: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 104 KiB |
@@ -19,5 +19,3 @@ export const ignoreHeadings = new Set(
|
||||
"example",
|
||||
].map((t) => t.toLowerCase()),
|
||||
);
|
||||
|
||||
export const minimumParagraphlength = 20;
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import FlexSearch from "flexsearch";
|
||||
import type { PageData, PageTitleIndex, SectionIndex } from "../types";
|
||||
|
||||
export type Indexes = {
|
||||
type Indexes = {
|
||||
sectionIndex: SectionIndex;
|
||||
websiteData: PageData[];
|
||||
pageTitleIndex: PageTitleIndex;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PostsOrPages } from "@tryghost/content-api";
|
||||
import "./[slug]/styles.css";
|
||||
|
||||
export const GHOST_THIRDWEB_BLOG_KEY = "49c62b5137df1c17ab6b9e46e3";
|
||||
const GHOST_THIRDWEB_BLOG_KEY = "49c62b5137df1c17ab6b9e46e3";
|
||||
|
||||
export async function fetchChangeLogs() {
|
||||
const queryParamsString = Object.entries({
|
||||
|
||||
@@ -72,6 +72,7 @@ async function PageContent() {
|
||||
<div>
|
||||
<Heading
|
||||
level={2}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="changelog-title"
|
||||
id={slugger.slug(post.title || "")}
|
||||
anchorClassName="[&>a]:hidden m-0 border-b pb-5 mb-5"
|
||||
|
||||
@@ -16,16 +16,6 @@ npx thirdweb install <projectpath> [options]
|
||||
|
||||
This command will automatically detect the type of project you are working on and install or update the appropriate thirdweb dependencies.
|
||||
|
||||
**Dependencies installed:**
|
||||
|
||||
- **JS/TS**
|
||||
- **Smart contract projects**: [`@thirdweb/contracts`](/typescript/latest)
|
||||
- **App projects:**
|
||||
- **React**: [`@thirdweb/react`](/react/latest), [`@thirdweb/sdk`](/typescript/latest) and [`ethers` v5](https://docs.ethers.org/v5/)
|
||||
- **React Native**: [`@thirdweb/react-native`](/react-native/latest), [`@thirdweb/react-native-compat`](/react-native/latest) and [`ethers` v5](https://docs.ethers.org/v5/)
|
||||
- **Other**: [`@thirdweb/sdk`](/typescript/latest) and [`ethers` v5](https://docs.ethers.org/v5/)
|
||||
|
||||
If any thirdweb package _(or Ethers)_ is already installed, they are updated to the latest compatible version.
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ string address = await sdk.wallet.Connect(connection);
|
||||
</Step>
|
||||
<Step title="Executing Transactions with Smart Accounts">
|
||||
|
||||
Once setup, you can use the Connect [TypeScript](/typescript/latest), [React](/react/latest) and [Unity SDKs](/unity) to deploy contracts, perform transactions, and manipulate smart accounts like any other wallet.
|
||||
Once setup, you can use the Connect [TypeScript](/typescript/v5), [React](/react/v5) and [Unity SDKs](/unity) to deploy contracts, perform transactions, and manipulate smart accounts like any other wallet.
|
||||
|
||||
<Tabs defaultValue="react">
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Using Account Abstraction in React
|
||||
|
||||
By using the [wallet SDK](/references/wallets/latest) alongside the [React SDK](/react/latest), you can use smart accounts in your front-end applications easily.
|
||||
By using the [TypeScript SDK](/typescript/v5), you can use smart accounts in your front-end applications easily.
|
||||
|
||||
<Steps>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
|
||||
|
||||
# Using Account abstraction in Typescript
|
||||
|
||||
By using the [wallet SDK](/references/wallets/latest) alongside the [TypeScript SDK](/typescript/latest), you can use smart accounts in your applications easily.
|
||||
By using the [TypeScript SDK](/typescript/v5), you can use smart accounts in your applications easily.
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -80,15 +80,18 @@ console.log("Personal account address:", personalAccount.address);
|
||||
</Step>
|
||||
<Step title="Creating the Smart account">
|
||||
|
||||
Now, let's create a smart account using the SmartWallet class from the `@thirdweb-dev/wallets` package.
|
||||
To do this, we need to pass a `SmartWalletConfig` object to the constructor. This object contains the following properties:
|
||||
Now, let's create a smart account using `smartWallet` from `thirdweb/wallets` package.
|
||||
To do this, we need to pass an object of type [SmartWalletOptions](/references/typescript/v5/SmartWalletOptions) to the function.
|
||||
|
||||
Some of the important properties in this object are:
|
||||
|
||||
- `chain`: the chain that the smart account will be deployed on.
|
||||
- `sponsorGas`: whether the smart account should have sponsored transactions or not.
|
||||
|
||||
Once we have created the config and instantiated the `SmartWallet` class, we can connect the personal wallet to the smart account using the `connect` method.
|
||||
|
||||
Once we have created the smart wallet interface, we can connect the personal wallet to the smart account using the `connect` method.
|
||||
```typescript
|
||||
import { smartWallet } from "thirdweb/wallets";
|
||||
|
||||
// Configure the smart wallet
|
||||
const wallet = smartWallet({
|
||||
chain: sepolia,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { createMetadata } from "@doc";
|
||||
import { V4SDKbanner } from "@/components/others/V4SDKBanner";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@doc";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
import { V4SDKbanner } from "@/components/others/V4SDKBanner";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@doc";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
import { V4SDKbanner } from "@/components/others/V4SDKBanner";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
|
||||
@@ -31,25 +31,27 @@ Engine makes it effortless for any developer to airdrop NFTs at scale. You spons
|
||||
|
||||
## Frontend: Add Connect Wallet and Claim buttons
|
||||
|
||||
Use [`<ConnectWallet>`](/react/latest/components/ConnectWallet) to prompt the user for their wallet. The **Claim** button calls `POST /api/claim`.
|
||||
Use [`ConnectButton`](/references/typescript/v5/ConnectButton) component to prompt the user for their wallet. The **Claim** button calls `POST /api/claim`.
|
||||
|
||||
```tsx
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
function ClaimPage() {
|
||||
const userWalletAddress = useAddress();
|
||||
const account = useActiveAccount();
|
||||
const address = account?.address;
|
||||
|
||||
const onClick = async () => {
|
||||
await fetch("/api/claim", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ userWalletAddress }),
|
||||
body: JSON.stringify({ address }),
|
||||
});
|
||||
alert(`🎉 A reward has been sent to your wallet: ${userWalletAddress}`);
|
||||
alert(`🎉 A reward has been sent to your wallet: ${address}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h2>Thank you for being a superfan! ❤️</h2>
|
||||
<ConnectWallet />
|
||||
{userWalletAddress && <button onClick={onClick}>Claim my reward</button>}
|
||||
{address && <button onClick={onClick}>Claim my reward</button>}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ The buyer receives the NFT without requiring wallet signatures or gas funds.
|
||||
|
||||
## Frontend: Add Connect Wallet and credit card form
|
||||
|
||||
Use [`<ConnectWallet>`](/react/latest/components/ConnectWallet) to prompt the buyer for their wallet address. The buyer provides their credit card details and selects **Pay now** to send payment details directly to Stripe.
|
||||
Use [`<ConnectButton>`](/references/typescript/v5/ConnectButton) to prompt the buyer for their wallet address. The buyer provides their credit card details and selects **Pay now** to send payment details directly to Stripe.
|
||||
|
||||
```tsx
|
||||
function Home() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain="<chain_id>" clientId="<thirdweb_client_id>">
|
||||
<ThirdwebProvider>
|
||||
<PurchasePage />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
@@ -93,6 +93,15 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* If no data-theme is added in body, its in dark theme */
|
||||
body:not([data-theme="light"]) .light-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-theme="light"] .dark-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code span {
|
||||
color: var(--code-dark-color);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ Composability refers to the ability for different protocols and smart contracts
|
||||
It allows for the creation of new products and services without having to build everything from scratch.
|
||||
|
||||
Thirdweb is completely composable. Each product can be used independently or in combination with other products.
|
||||
For example, you can use the [SDKs](/typescript/latest) or [Dashboard](https://thirdweb.com/team) with **any** contract. Similarly, you can use the [Solidity SDK](/contracts/build/overview)
|
||||
without using the [Dashboard](https://thirdweb.com/team) or [TypeScript/JavaScript SDK](/typescript/latest).
|
||||
For example, you can use the [SDKs](/connect) or [Dashboard](https://thirdweb.com/team) with **any** contract. Similarly, you can use the [Solidity SDK](/contracts/build/overview)
|
||||
without using the [Dashboard](https://thirdweb.com/team) or [TypeScript/JavaScript SDK](/typescript/v5).
|
||||
|
||||
So, if you already have your own workflow which works for you, you can pick and choose which products you want to use.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import posthog from "posthog-js";
|
||||
import { Button } from "../../components/ui/button";
|
||||
|
||||
export function LandingPageCTAs() {
|
||||
return (
|
||||
<div className="flex flex-col flex-wrap gap-4 md:flex-row">
|
||||
<Button
|
||||
asChild
|
||||
variant="default"
|
||||
className="py-3 text-lg md:py-2 lg:min-w-[150px] lg:px-5"
|
||||
onClick={() => {
|
||||
posthog.capture("unified-sdk-cta.click");
|
||||
}}
|
||||
>
|
||||
<Link href="/typescript/v5">Get started with thirdweb SDK</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="py-3 text-lg md:py-2 lg:min-w-[150px] lg:px-7"
|
||||
asChild
|
||||
onClick={() => {
|
||||
posthog.capture("learn-more-cta.click");
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href="https://blog.thirdweb.com/changelog/introducing-the-unified-thirdweb-sdk/"
|
||||
target="_blank"
|
||||
>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,10 +26,6 @@ const monoFont = Fira_Code({
|
||||
export const metadata = createMetadata({
|
||||
title: "thirdweb docs",
|
||||
description: "thirdweb developer portal",
|
||||
image: {
|
||||
title: "Build apps and games on any EVM chain",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -66,7 +66,7 @@ export const nebulaAPI422Response = `\
|
||||
]
|
||||
}`;
|
||||
|
||||
export const nebulaExecuteConfigType = `\
|
||||
const nebulaExecuteConfigType = `\
|
||||
{
|
||||
mode: "engine";
|
||||
engine_url: string;
|
||||
@@ -82,7 +82,7 @@ export const nebulaExecuteConfigType = `\
|
||||
signer_wallet_address: string;
|
||||
}`;
|
||||
|
||||
export const nebulaContextFilterType = `\
|
||||
const nebulaContextFilterType = `\
|
||||
{
|
||||
chainIds: string[] | null;
|
||||
contractAddresses: string[] | null;
|
||||
|
||||
|
After Width: | Height: | Size: 330 KiB |
@@ -1,7 +1,8 @@
|
||||
import { Heading } from "@/components/Document";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import DocsHero from "./_images/docs-hero.png";
|
||||
import DocsHeroDark from "./_images/docs-hero-dark.png";
|
||||
import DocsHeroLight from "./_images/docs-hero-light.png";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -34,7 +35,8 @@ function Hero() {
|
||||
|
||||
{/* right */}
|
||||
<div className="hidden justify-center lg:flex">
|
||||
<Image src={DocsHero} alt="" className="w-full" />
|
||||
<Image src={DocsHeroDark} alt="" className="dark-only w-full" />
|
||||
<Image src={DocsHeroLight} alt="" className="light-only w-full" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,21 +0,0 @@
|
||||
import { createMetadata } from "@/components/Document";
|
||||
import { DocLayout } from "@/components/Layouts/DocLayout";
|
||||
import { sidebar } from "./sidebar";
|
||||
|
||||
export default async function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<DocLayout sideBar={sidebar} editPageButton={true} noIndex>
|
||||
{props.children}
|
||||
</DocLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Payments",
|
||||
icon: "payment",
|
||||
},
|
||||
title: "thirdweb Payments",
|
||||
description:
|
||||
"thirdweb payments delivers the easiest NFT payments experience for you and your buyers",
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "API References | thirdweb Payments",
|
||||
description: "thirdweb Payments API Reference",
|
||||
});
|
||||
|
||||
# Usage
|
||||
|
||||
The Payments API enables all the functionalities needed to create checkouts.
|
||||
|
||||
[View the full API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc)
|
||||
|
||||
You must use an API Secret Key to make authenticated calls. These calls should be
|
||||
made from your backend. [Create an API Key by creating a project from your thirdweb dashboard.](https://thirdweb.com/team)
|
||||
|
||||
Provide your secret key as a header: `x-secret-key: your_api_secret_key`
|
||||
|
||||
---
|
||||
|
||||
## API Categories
|
||||
|
||||
### Enable Contracts for Payments
|
||||
|
||||
Enable, edit, and disable contracts for Payments.
|
||||
|
||||
[View API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Contracts)
|
||||
|
||||
### Checkout Links
|
||||
|
||||
Create, edit, and delete Checkout Links.
|
||||
|
||||
[View API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Checkout-Links)
|
||||
|
||||
### One-time Checkout Links (Advanced)
|
||||
|
||||
Create One-time Checkout Links.
|
||||
|
||||
[View API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/One-time-Checkout-Links)
|
||||
|
||||
### Embedded Elements
|
||||
|
||||
Create SDK Client Secret used for Embedded Elements.
|
||||
|
||||
[View API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Checkout-Embedded-Elements)
|
||||
|
||||
### Transaction Status
|
||||
|
||||
Get the status of a transaction by transaction ID.
|
||||
|
||||
[View API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Transaction-Status)
|
||||
|
||||
### Get Estimated Price
|
||||
|
||||
Get the estimated price of a checkout, including fees and gas.
|
||||
|
||||
[View API reference.](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Estimated-Price)
|
||||
|
||||
### Transaction Holds
|
||||
|
||||
Capture or cancel a transaction hold.
|
||||
|
||||
[View API reference.](<https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Transaction-Holds-(Off-chain-Auctions)>)
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 268 KiB |
@@ -1,39 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Create a Checkout Link | thirdweb Payments",
|
||||
description:
|
||||
"Checkout Links are public, reusable URLs that allow buyers to complete a purchase with thirdweb's prebuilt checkout experience.",
|
||||
});
|
||||
|
||||
# Create a Checkout Link
|
||||
|
||||
Checkout Links are public, reusable URLs that allow buyers to complete a purchase with thirdweb's prebuilt checkout experience.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you've [enabled your contract for Payments](/payments/nft-checkout/enable-contract) first.
|
||||
|
||||
## Use cases
|
||||
|
||||
- Integrate a functional, prebuilt checkout experience quickly.
|
||||
- Open your NFT's public sale to all buyers.
|
||||
- Charge your buyers the same price.
|
||||
- Request your buyer's details (email, wallet address) during checkout.
|
||||
|
||||
## Create a checkout link
|
||||
|
||||
### Dashboard
|
||||
|
||||
1. Navigate into your contract from the [Contracts page](https://thirdweb.com/team/~/~/contracts).
|
||||
2. Navigate to the "Payments" page from the sidebar.
|
||||
3. Click the "Create New Checkout" button
|
||||
4. Customize the look and feel of your checkout experience, and hit "Create"
|
||||
|
||||
### API
|
||||
|
||||
[See the API Reference for creating a checkout link](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Checkout-Links/paths/~1api~12022-08-12~1shareable-checkout-link/post)
|
||||
|
||||
## Branding
|
||||
|
||||
Creating a Checkout Link is the fastest way to get started with thirdweb Checkouts. If you are looking for complete customization over branding and UX, check out the guide on [Embedded Elements](elements).
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 144 KiB |
@@ -1,174 +0,0 @@
|
||||
import { Steps, Step, DocImage } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
import checkoutCardDemoImage from "./assets/checkout-with-card-demo.png";
|
||||
import checkoutCardCustomizationImage from "./assets/checkout-with-card-customization-demo.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "CheckoutWithCard | thirdweb Embedded Elements",
|
||||
description:
|
||||
"The CheckoutWithCard element embeds a form on your app that accepts credit/debit card, Apple Pay, and Google Pay.",
|
||||
});
|
||||
|
||||
# CheckoutWithCard
|
||||
|
||||
The **CheckoutWithCard** element embeds a form on your app that accepts credit/debit card, Apple Pay, and Google Pay.
|
||||
|
||||
This component also handles:
|
||||
|
||||
- Apple Pay and Google Pay
|
||||
- Bot and anti-fraud detection
|
||||
- 3D Secure (if necessary)
|
||||
- Buyer KYC (if necessary)
|
||||
|
||||
<DocImage src={checkoutCardDemoImage} alt="CheckoutWithCard Embed Demo" />
|
||||
|
||||
## React Integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the React SDK">
|
||||
Follow our SDK [install guide](/react/latest/getting-started) here.
|
||||
{/* <!-- prettier-ignore --> */}
|
||||
```bash
|
||||
npm install @thirdweb-dev/react
|
||||
```
|
||||
{/* <!-- prettier-ignore --> */}
|
||||
```bash
|
||||
yarn add @thirdweb-dev/react
|
||||
```
|
||||
</Step>
|
||||
<Step title="Embed your component">
|
||||
On your frontend, render the `CheckoutWithCard` component with your configs.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
import { CheckoutWithCard } from "@thirdweb-dev/react";
|
||||
|
||||
<CheckoutWithCard
|
||||
clientId="YOUR_CLIENT_ID"
|
||||
configs={{
|
||||
// Registered contract ID
|
||||
contractId: "YOUR_CONTRACT_ID",
|
||||
// Buyer wallet address
|
||||
walletAddress: "0x...",
|
||||
// Mint method (for custom contracts only)
|
||||
mintMethod: {
|
||||
name: "claimTo",
|
||||
args: {
|
||||
_to: "$WALLET",
|
||||
_quantity: "$QUANTITY",
|
||||
_tokenId: 0,
|
||||
},
|
||||
payment: {
|
||||
value: "0.1 * $QUANTITY",
|
||||
currency: "ETH",
|
||||
},
|
||||
},
|
||||
}}
|
||||
onPaymentSuccess={(result) => {
|
||||
console.log("Payment successful:", result);
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### `CheckoutWithCard` props
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| clientId **\*** | string | thirdweb client ID (Obtained from an API key which you can generate on the [Dashboard](https://thirdweb.com/team)) |
|
||||
| configs **\*** | object | A list of configs to create your card checkout element. Fields are the same as the ones found in the [Create Checkout Elements Client Secret](https://docs.withpaper.com/reference/create-checkout-elements-client-secret) API. |
|
||||
| onPaymentSuccess **\*** | `({ transactionId: string; }) => void` | This method is called after the payment has been submitted for processing. This payment may still be rejected by the cardholder's bank. |
|
||||
| onError | `(PaperSDKError) => void` | This method is called when an error is encountered. |
|
||||
| onPriceUpdate | `({ quantity: number; unitPrice: PriceDetail; networkFees: PriceDetail; serviceFees: PriceDetail; total: PriceDetail; }) => void` | This method is called when the price is updated or loaded for the first time. This summary is helpful to show a granular price breakdown. Where PriceDetail is `{ display: string; valueInSubunits: number; currency: string; }` |
|
||||
| locale | enum Valid values: `en`, `fr`, `es`, `it`, `de`, `ja`, `ko`, `zh` | The language to show text in. Defaults to `en`. |
|
||||
| options | object | Customize component styling. See [Customization](#customization). |
|
||||
|
||||
## Javascript Integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the Javascript SDK with your preferred package manager">
|
||||
Follow our SDK [install guide](/react/latest/getting-started) here.
|
||||
{/* <!-- prettier-ignore --> */}
|
||||
```bash
|
||||
npm install @thirdweb-dev/payments
|
||||
```
|
||||
{/* <!-- prettier-ignore --> */}
|
||||
```bash
|
||||
yarn add @thirdweb-dev/payments
|
||||
```
|
||||
</Step>
|
||||
<Step title="Add your iframe">
|
||||
Call `createCheckoutWithCardElement` to insert the iframe on your page. Pass the `configs` to this component.
|
||||
> If you don't provide `elementOrId`, this call returns an iframe element for you to insert into your page.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Example code
|
||||
|
||||
```typescript
|
||||
import { createCheckoutWithCardElement } from "@thirdweb-dev/payments";
|
||||
|
||||
// Assume a container exists:
|
||||
//
|
||||
// <div id="paper-checkout-container" width="380px" />
|
||||
//
|
||||
createCheckoutWithCardElement({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
configs: {
|
||||
contractId: "YOUR_CONTRACT_ID",
|
||||
walletAddress: "0x...",
|
||||
}
|
||||
elementOrId: "paper-checkout-container",
|
||||
appName: "My Web3 App",
|
||||
options,
|
||||
onError(error) {
|
||||
console.error("Payment error:", error);
|
||||
},
|
||||
onPaymentSuccess({ id }) {
|
||||
console.log("Payment successful.");
|
||||
},
|
||||
});
|
||||
|
||||
// Alternatively, insert the iframe programmatically:
|
||||
//
|
||||
// const iframe = createCheckoutWithCardElement(...)
|
||||
// document.getElementById('paper-checkout-container').appendChild(iframe);
|
||||
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
The optional `options` argument allows you to customize the component's styling. All customization fields are optional.
|
||||
|
||||
#### `options` object
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| colorPrimary | string (In hex, e.g. #cf3781) | The primary brand color for buttons and links. |
|
||||
| colorBackground | string (In hex, e.g. #cf3781) | The background color of the page. |
|
||||
| colorText | string (In hex, e.g. #cf3781) | The color for text on the page and UI elements. |
|
||||
| borderRadius | number (In px, e.g. 0 for sharp corners, 12 for rounded corners, 24 for pill shape) | The roundness of buttons and input elements. |
|
||||
| inputBorderColor | string (In hex, e.g. #cf3781) | The border color of the input field. |
|
||||
| inputBackgroundColor | string (In hex, e.g. #cf3781) | The background color of the input field. |
|
||||
|
||||
#### Example
|
||||
|
||||
Here's an example component with the following props:
|
||||
|
||||
```javascript
|
||||
{
|
||||
colorBackground: '#fefae0',
|
||||
colorPrimary: '#606c38',
|
||||
colorText: '#283618',
|
||||
borderRadius: 6,
|
||||
inputBackgroundColor: '#faedcd',
|
||||
inputBorderColor: '#d4a373',
|
||||
}
|
||||
```
|
||||
|
||||
<DocImage
|
||||
src={checkoutCardCustomizationImage}
|
||||
alt="CheckoutWithCard Embed Demo"
|
||||
/>
|
||||
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 176 KiB |
@@ -1,135 +0,0 @@
|
||||
import { DocImage } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
import checkoutWithEthImage from "./assets/checkout-with-eth-demo.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "CheckoutWithEth | thirdweb Embedded Elements",
|
||||
description:
|
||||
"CheckoutWithEth embeds a component on your page that accepts ETH payments on Ethereum.",
|
||||
});
|
||||
|
||||
# CheckoutWithEth
|
||||
|
||||
**CheckoutWithEth** embeds a component on your page that accepts ETH payments on Ethereum.
|
||||
|
||||
This component also handles:
|
||||
|
||||
- Connecting the payment wallet
|
||||
- Prompting the payment wallet to switch chain, if necessary
|
||||
- Informing the payment wallet they have insufficient funds
|
||||
|
||||
<DocImage src={checkoutWithEthImage} alt="CheckoutWithEth Embed Demo" />
|
||||
|
||||
## React Integration
|
||||
|
||||
1. Install the React SDK with your preferred package manager.
|
||||
- `npm install @paperxyz/react-client-sdk-checkout-with-eth`
|
||||
- `yarn add @paperxyz/react-client-sdk-checkout-with-eth`
|
||||
2. Copy your API key from the [Developer Dashboard: Developers](https://withpaper.com/dashboard/developers) page.
|
||||
3. When a buyer wants to make a purchase, create a `configs` object (see props table below). You will provide some buyer information and configure behavior via this API.
|
||||
4. Instantiate a `PaperSDKProvider` provider to store Paper-specific properties.
|
||||
5. Within the provider, instantiate the `CheckoutWithEth` component to render this component. Pass the SDK Client Secret to this component.
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
import { CheckoutWithEth } from "@paperxyz/react-client-sdk-checkout-with-eth";
|
||||
|
||||
<CheckoutWithEth
|
||||
configs={{
|
||||
contractId: "CONTRACT_ID",
|
||||
walletAddress: "0x...",
|
||||
}}
|
||||
onPaymentSuccess={(result) => {
|
||||
console.log("Payment successful.");
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### `CheckoutWithEth` props
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| configs **\*** | string | A list of configs to create your eth checkout element. Fields are the same as the ones found in the [Create Checkout Elements Client Secret](ref:create-checkout-elements-client-secret) API. |
|
||||
| onPaymentSuccess **\*** | `(props : { onChainTxReceipt: TransactionReceipt; transactionId: string; }) => void` | This method is called after the payment has succeeded onchain. |
|
||||
| onPriceUpdate | `({ cryptoToFiatConversionRate: number; quantity: number; unitPrice: PriceDetail; networkFees: PriceDetail; serviceFees: PriceDetail; total: PriceDetail; }) => void` | This method is called when the price is updated or loaded for the first time. This summary is helpful to show a granular price breakdown. \* The `valueInSubunits` type here differs from the subunits for CheckoutWithCard (which is a number) |
|
||||
| locale | enum (Valid values: `en`, `fr`, `es`, `it`, `de`, `ja`, `ko`, `zh`) | The language to show text in. Defaults to `en`. |
|
||||
| receivingWalletType | enum (Valid values: `WalletConnect`, `MetaMask`, `Coinbase Wallet`, `Phantom`, `Preset`) | Defaults to `Preset`. The wallet type of the user wallet receiving the NFT to render the appropriate wallet icon in the component. |
|
||||
| suppressErrorToast | boolean | Defaults to `false`. If `false`, there will be a toast (within the iFrame) that pops up informing the user of what went wrong. This can include canceling transactions, pending transactions, etc. |
|
||||
| showConnectWalletOptions | boolean | Defaults to `true`. If `true`, a connect wallet screen will be displayed allowing users to connect their wallet. |
|
||||
| payingWalletSigner | [`ethers.Signer`](https://docs.ethers.io/v5/api/signer/) | If provided, the component will request funds from this signer. |
|
||||
| onWalletConnected | `(props: { userAddress: string, chainId: number }) => void` | This method is called when a payment wallet is connected. |
|
||||
| onPageChange | `(currentPage: CheckoutWithEthPage) => void` | This method is called when the buyer transitions between pages. |
|
||||
| onError | `(error: PaperSDKError) => void` | This method is called when an error is encountered. |
|
||||
| setUpUserPayingWalletSigner | `(args: { chainId: number }) => void` | This method is called before the `payingWalletSigner` is asked to pay with the `chainId` that the `payingWalletSigner` is supposed to be on. |
|
||||
|
||||
## Javascript Integration
|
||||
|
||||
1. Install the Javascript SDK with your preferred package manager.
|
||||
- `npm install @paperxyz/js-client-sdk`
|
||||
- `yarn add @paperxyz/js-client-sdk`
|
||||
2. Copy your API key from the [Developer Dashboard: Developers](https://withpaper.com/dashboard/developers) page.
|
||||
3. When a buyer wants to make a purchase, create a `configs` object (see props table below). You will provide some buyer information and configure behavior via this API.
|
||||
4. Call `createCheckoutWithEthElement` to insert the iframe on your page. Pass the SDK Client Secret to this component.
|
||||
1. If you don't provide `elementOrId`, this call returns an iframe element for you to insert into your page.
|
||||
|
||||
### Code example
|
||||
|
||||
```javascript
|
||||
import type { createCheckoutWithEthElement } from "@paperxyz/js-client-sdk-checkout-with-eth";
|
||||
|
||||
// Assume a container exists:
|
||||
//
|
||||
// <div id="paper-checkout-container" w="380px" />
|
||||
//
|
||||
const iframe = await createCheckoutWithEthElement({
|
||||
configs: {
|
||||
contractId: 'CONTRACT_ID',
|
||||
walletAddress: "0x...",
|
||||
}
|
||||
elementOrId: "paper-checkout-container",
|
||||
payingWalletSigner,
|
||||
setUpUserPayingWalletSigner,
|
||||
receivingWalletType,
|
||||
onError(error) {
|
||||
console.log("Payment error:", error);
|
||||
},
|
||||
onSuccess({ transactionId }) {
|
||||
console.log("Payment successful.");
|
||||
},
|
||||
});
|
||||
|
||||
// Alternatively, insert the iframe programmatically:
|
||||
//
|
||||
// const iframe = createCheckoutWithEthElement(...)
|
||||
// document.getElementById('paper-checkout-container').appendChild(iframe);
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| configs **\*** | string | A list of configs to create your eth checkout element. Fields are the same as the ones found in the [Create Checkout Elements Client Secret](ref:create-checkout-elements-client-secret) API. |
|
||||
| payingWalletSigner **\*** | [ethers.Signer](https://docs.ethers.io/v5/api/signer/) | The connected wallet who is going to be paying the `ETH` required for the checkout |
|
||||
| locale | enum (Valid values: `en`, `fr`, `es`, `it`, `de`, `ja`, `ko`, `zh`) | The language to show text in. Defaults to `en`. |
|
||||
| options | object | Customize component styling. See [Customization](#customization). |
|
||||
| setUpUserPayingWalletSigner | `(args: { chainId: number; }) => void \| Promise<void>` | If provided, the callback will be called before asking buyers to pay with the `chainId` that they are expected to be on. Developers must make sure that users are on the right chain (Goerli for testnets, Eth for mainnets) or risk buyers sending funds into the void. |
|
||||
| receivingWalletType | enum (Valid values: `WalletConnect`, `MetaMask`, `Coinbase Wallet`, `Phantom`, `Preset`) | Defaults to `Preset`. The wallet type of the user wallet receiving the NFT to render the appropriate wallet icon in the component. |
|
||||
| onError | `(error: PaperSDKError) => void` | This method is called when anything wrong happens during the checkout process. |
|
||||
| onPaymentSuccess | `(props: { onChainTxReceipt: TransactionReceipt; transactionId: string; }) => void` | This method is called after the payment has succeeded onchain. |
|
||||
| suppressErrorToast | boolean | Defaults to `true`. If false, any error thrown will be displayed in a toast. |
|
||||
| onLoad | `() => void` | This method is called when the iframe loads. |
|
||||
| elementOrId | `string \| HTMLElement` | If provided, the iframe will be appended this element. You can pass in the DOM element or the `id` associated with the element. A minimum width of 380px is recommended. |
|
||||
|
||||
## Customization
|
||||
|
||||
The optional `options` argument allows you to customize the component's styling. All customization fields are optional.
|
||||
|
||||
#### `options` object
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| colorPrimary | string (In hex, e.g. #cf3781) | The primary brand color for buttons and links. |
|
||||
| colorBackground | string (In hex, e.g. #cf3781) | The background color of the page. |
|
||||
| colorText | string (In hex, e.g. #cf3781) | The color for text on the page and UI elements. |
|
||||
| borderRadius | number (In px, e.g. 0 for sharp corners, 12 for rounded corners, 24 for pill shape) | The roundness of buttons and input elements. |
|
||||
@@ -1,146 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Custom Contracts | thirdweb NFT Checkout",
|
||||
description:
|
||||
"Using your own Custom Contract on Polygon, Ethereum, or another EVM blockchain with thirdweb NFT Checkout",
|
||||
});
|
||||
|
||||
# Custom Contracts
|
||||
|
||||
Using your own Custom Contract on Polygon, Ethereum, or another EVM blockchain? The following example shows you how to use it with thirdweb.
|
||||
|
||||
## Example
|
||||
|
||||
Assume that this is your contract's claim function:
|
||||
|
||||
```javascript
|
||||
function claimTo(
|
||||
address _to,
|
||||
uint256 _quantity,
|
||||
uint256 _tokenId,
|
||||
string CUSTOM_ARG_1, // Any additional arguments.
|
||||
bool CUSTOM_ARG_2
|
||||
)
|
||||
external
|
||||
payable
|
||||
mintCompliant(_tokenId, _quantity)
|
||||
priceCompliant(_tokenId, _quantity)
|
||||
tokenLive(_tokenId)
|
||||
{
|
||||
// YOUR MINTING LOGIC HERE
|
||||
}
|
||||
```
|
||||
|
||||
You will then need a **mintMethod** in order for thirdweb to call your claim function. For the example above, your mintMethod should look like this:
|
||||
|
||||
```json
|
||||
mintMethod: {
|
||||
name: "claimTo", // Name of the function to call within the smart contract.
|
||||
args: {
|
||||
_to: "$WALLET",
|
||||
_quantity: "$QUANTITY",
|
||||
_tokenId: 0,
|
||||
CUSTOM_ARG_1: "xyz",
|
||||
CUSTOM_ARG_2: false
|
||||
},
|
||||
payment: {
|
||||
value: "0.1 * $QUANTITY", // Assuming that your NFT costs 0.1 ETH each.
|
||||
currency: "ETH"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will ensure that when thirdweb calls the smart contract during any checkout flow, under the hood, we would be calling your smart contract like so:
|
||||
|
||||
```javascript
|
||||
claimTo("0x...", 1, 0, "xyz", false {
|
||||
value: ethers.utils.parseEther("0.1"),
|
||||
});
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
Your smart contract method that mints the NFT must satisfy these requirements:
|
||||
|
||||
1. The method must accept the buyer's wallet address as an argument.
|
||||
2. The method must allow thirdweb's minter wallets to mint an unlimited amount as the `msg.sender`. Any restrictions on the minting wallet address or quantity is not allowed.
|
||||
3. (If priced in USDC) The method must explicitly [request USDC token](/payments/nft-checkout/erc20-pricing) from the `msg.sender`.
|
||||
|
||||
## MintMethod
|
||||
|
||||
The **mintMethod** is required when creating [Shareable Checkout Links](/payments/nft-checkout/checkout-link), [One-Time Checkout Links](/payments/nft-checkout/one-time-checkout-link), or [Checkout Elements](/payments/nft-checkout/elements). Think of it as an ABI for thirdweb to know how to call your claim function.
|
||||
|
||||
The generic format of the mintMethod is:
|
||||
|
||||
```json
|
||||
"mintMethod": {
|
||||
"name": "claimTo",
|
||||
"args": {
|
||||
"_to": "$WALLET",
|
||||
"_quantity": "$QUANTITY"
|
||||
},
|
||||
"payment": {
|
||||
"value": "0.1 * $QUANTITY",
|
||||
"currency": "ETH"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template variables
|
||||
|
||||
> 🚧 Don't know the values?
|
||||
>
|
||||
> thirdweb supports two template variables -`$WALLET` and `$QUANTITY` that you may use if you don't know the buyer's wallet address and quantity. Note that for **all other** fields such as `value` and `currency`, you would need to hardcode the actual values.
|
||||
|
||||
Template variables will be replaced when calling your contract:
|
||||
|
||||
| Variable | Replaced with |
|
||||
| :---------- | :------------------------------ |
|
||||
| `$WALLET` | The buyer's wallet address. |
|
||||
| `$QUANTITY` | The quantity of tokens to mint. |
|
||||
|
||||
## Payment
|
||||
|
||||
The `payment` field should provide the **price per NFT in human-readable form** (not wei).
|
||||
|
||||
Example: `"payment": { "value": "0.1 * $QUANTITY", "currency": "ETH" }`
|
||||
|
||||
### Free NFTs
|
||||
|
||||
Set the price to zero.
|
||||
|
||||
Example: `"payment": { value: "0 * $QUANTITY", currency: "MATIC" }`
|
||||
|
||||
### NFTs priced in USDC
|
||||
|
||||
Ensure your mint method **explicitly requests USDC tokens from msg.sender**. See [USDC Pricing](/payments/nft-checkout/erc20-pricing) for more details.
|
||||
|
||||
### Delegate a different ERC-20 payment address
|
||||
|
||||
> 📘 This is required when interacting with the [Seaport contract](https://docs.opensea.io/reference/seaport-overview).
|
||||
|
||||
By default thirdweb's float wallet approves your NFT contract to request ERC-20 tokens.
|
||||
To specify a different contract address that will request ERC-20 tokens from thirdweb's float wallet, set that address in the `"spender"` field:
|
||||
|
||||
```json
|
||||
"payment": {
|
||||
"value": "100 * $QUANTITY",
|
||||
"currency": "USDC",
|
||||
"spender": "<PAYMENT CONTRACT ADDRESS>"
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### I have multiple methods with the same name. Why isn't my mintMethod isn't working?
|
||||
|
||||
Please provide the full signature as your mintMethod. thirdweb will not try to "guess" the correct method.
|
||||
|
||||
> **Example**
|
||||
>
|
||||
> Your contract has methods `mint(address to)` and `mint(address to, string tokenId)` and your checkout will call the former.
|
||||
>
|
||||
> Provide your mintMethod as `mint(address)`.
|
||||
|
||||
You **do not** need to modify your contract.
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "thirdweb Embedded Elements",
|
||||
description:
|
||||
"Checkout Embedded Elements are modular components that can be embedded directly in your app. This allows you to fully customize the look and feel of the checkout experience within your application.",
|
||||
});
|
||||
|
||||
# Embedded Elements
|
||||
|
||||
Checkout Embedded Elements are modular components that can be embedded directly in your app. This allows you to fully customize the look and feel of the checkout experience within your application.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Make sure you've [enabled your contract for Payments](enable-contract)
|
||||
- You have the user's wallet address
|
||||
- If you do not already a wallet set up for your users, use [Connect](/connect/connect) to create a wallet first.
|
||||
- Your Content Security Policy is set allowing thirdweb's domain.
|
||||
- Make sure you enable the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) directives on your server, please allow the following directives:
|
||||
- `connect-src https://payments.thirdweb.com;`
|
||||
- `frame-src https://payments.thirdweb.com;`
|
||||
- `script-src https://payments.thirdweb.com;`
|
||||
|
||||
## Integration
|
||||
|
||||
#### Accepting fiat payment options (credit/debit card, Apple Pay, Google Pay)
|
||||
|
||||
Embed [CheckoutWithCard](/payments/nft-checkout/checkout-with-card) in your app.
|
||||
|
||||
#### Accepting crypto payment options (ETH)
|
||||
|
||||
Embed [CheckoutWithEth](/payments/nft-checkout/checkout-with-eth) in your app.
|
||||
|
Before Width: | Height: | Size: 268 KiB |
@@ -1,35 +0,0 @@
|
||||
import { DocImage } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
import enablePaymentsImage from "./assets/enable-payments.jpeg";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Enable Contract for Payments | thirdweb Payments",
|
||||
description:
|
||||
"Enabling your contract for Payments is required to unlock Checkouts functionality. This can be done on the thirdweb dashboard or with the Payments API.",
|
||||
});
|
||||
|
||||
# Enable Contract for Payments
|
||||
|
||||
Enabling your contract for Payments is required to unlock Checkouts functionality. This can be done on the thirdweb dashboard or with the Payments API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, ensure your contract is set up properly for Checkouts.
|
||||
|
||||
- Using a thirdweb contract? [Ensure your contract is configured properly](pre-built-contracts).
|
||||
- Using your own NFT contract? [Ensure your custom contract is compatible](custom-contracts).
|
||||
- Selling a token listed on a secondary marketplace? [Visit the marketplaces guide](marketplaces).
|
||||
|
||||
## Enable a contract for Payments
|
||||
|
||||
#### Dashboard
|
||||
|
||||
1. Navigate into your contract by visiting [Contracts page](https://thirdweb.com/team/~/~/contracts).
|
||||
2. Navigate to the "Payments" page from the sidebar.
|
||||
3. Click the "Enable Payments" button
|
||||
|
||||
<DocImage src={enablePaymentsImage} alt="Checkouts Process Diagram" />
|
||||
|
||||
#### API
|
||||
|
||||
[See the API Reference for enabling contracts for Payments](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/Contracts/paths/~1api~12022-08-12~1register-contract/post)
|
||||
@@ -1,56 +0,0 @@
|
||||
import { DocImage } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "ERC-20 Pricing | thirdweb NFT Checkout",
|
||||
description:
|
||||
"thirdweb supports selling NFTs that accept the USDC token for enterprise customers",
|
||||
});
|
||||
|
||||
# ERC-20 Pricing
|
||||
|
||||
> thirdweb supports selling NFTs that accept the USDC token **for enterprise customers only**.
|
||||
>
|
||||
> 👉 Read our guide on [How to Price your NFTs in USDC](https://blog.withpaper.com/how-to-price-your-nfts-in-usdc/).
|
||||
|
||||
## Integration
|
||||
|
||||
### For thirdweb contracts
|
||||
|
||||
1. Navigate to the thirdweb dashboard for your contract
|
||||
2. Set the claim condition to price the NFT in USDC.
|
||||
|
||||
<DocImage src={"https://files.readme.io/f1bd58f-image.png"} height={150} />
|
||||
|
||||
### For custom contracts
|
||||
|
||||
1. Add the following snippet to your contract (modify as needed):
|
||||
|
||||
```solidity
|
||||
// Replace MY_USDC_ADDRESS with the address based on the blockchain below.
|
||||
IERC20 public usdc = IERC20("0xe6b8a5CF854791412c1f6EFC7CAf629f5Df1c747");
|
||||
uint256 priceInUsdc = 50 * 10 ** 6
|
||||
function mintTo(address recipient, uint256 quantity) public {
|
||||
usdc.transferFrom(msg.sender, address(this), quantity * priceInUsdc);
|
||||
}
|
||||
```
|
||||
|
||||
2. Configure your checkout to accept USDC payment:
|
||||
1. **Shareable Checkout Links:** When creating the checkout set the **Price per NFT** currency to **USDC**.
|
||||
2. **One-Time Checkout Links:** When [creating a One-Time Checkout Link](/payments/nft-checkout/one-time-checkout-link) set `mintMethod` with `"payment": { "value": "50 * $QUANTITY", currency: "USDC" }`.
|
||||
3. **Checkout Elements:** When [creating a Client Secret](/payments/nft-checkout/elements) set `mintMethod` with `"payment": { "value": "50 * $QUANTITY", currency: "USDC" }`.
|
||||
|
||||
## Accepting the correct USDC token
|
||||
|
||||
Make sure your contract requests the correct USDC token address from thirdweb. These are the supported USDC token addresses.
|
||||
|
||||
- USDC on Polygon: [0x3c499c542cef5e3811e1192ce70d8cc03d5c3359](https://polygonscan.com/address/0x3c499c542cef5e3811e1192ce70d8cc03d5c3359)
|
||||
- USDC on Goerli: [0x07865c6E87B9F70255377e024ace6630C1Eaa37F](https://goerli.etherscan.io/address/0x07865c6E87B9F70255377e024ace6630C1Eaa37F)
|
||||
- USDC on Ethereum: [0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48](https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48)
|
||||
- USDC on Optimism: [0x7f5c764cbc14f9669b88837ca1490cca17c31607](https://optimistic.etherscan.io/address/0x7f5c764cbc14f9669b88837ca1490cca17c31607)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Do you support other ERC-20 tokens?
|
||||
|
||||
thirdweb supports popular ERC-20 (or ERC20) tokens like USDC and WETH. If your NFT is priced in a different token, [contact us](mailto:[email protected]).
|
||||
@@ -1,151 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "FAQ | thirdweb Payments",
|
||||
description: "Frequently asked questions about thirdweb Payments.",
|
||||
});
|
||||
|
||||
# General
|
||||
|
||||
### How much does thirdweb Checkouts cost?
|
||||
|
||||
There is **no cost to sellers**! Buyers pay a 1% service fee on top of payment processing fees.
|
||||
|
||||
### How long does implementation take?
|
||||
|
||||
A full-featured checkout link **takes minutes to set up**:
|
||||
|
||||
1. [Register your contract](/payments/nft-checkout/enable-contract).
|
||||
2. Create a [🔗 Checkout Link](/payments/nft-checkout/checkout-link).
|
||||
3. Send the link to your customers. Done ✨!
|
||||
|
||||
When you're ready to customize further, you can tinker with styling, SDKs, webhooks, dynamic pricing, and more.
|
||||
|
||||
### How scalable is thirdweb Checkouts? How much load can it handle?
|
||||
|
||||
Our product is enterprise-ready with a 99.9% uptime. We are battle-tested with enterprises like Balmain, New York Fashion Week, Unilever, deadmau5 and Tilting Point. We can enqueue transactions up to 200 transactions/minute by default, with higher limits upon request.
|
||||
|
||||
### What platforms are supported?
|
||||
|
||||
Checkout links can be used anywhere webpages are supported.
|
||||
|
||||
The Checkout Embedded Elements SDK is supported on Web and Mobile Web.
|
||||
|
||||
## Smart contract support
|
||||
|
||||
### Which blockchains and currencies are supported?
|
||||
|
||||
See the [overview](/payments) page for a list of supported chains and currencies. We can support any EVM chain upon request. Please contact [email protected] if the chain or currency you want to sell in is not on our list.
|
||||
|
||||
### Are marketplace contracts supported?
|
||||
|
||||
Yes! See [Marketplace Sales](/payments/nft-checkout/marketplaces) for more details.
|
||||
|
||||
### Can I sell an NFT that is already minted?
|
||||
|
||||
Yes! You'll need to "wrap" your NFT with a marketplace contract like [thirdweb marketplace](/contracts/explore/pre-built-contracts/marketplace) or build your own. This "direct listing" approach works like this:
|
||||
|
||||
- The seller has an NFT already minted and wants to sell it.
|
||||
- The seller approves the marketplace contract to take this NFT from their wallet once a buyer offers a specified amount. The NFT is not moved until a sale is completed.
|
||||
- A buyer agrees to pay the specified amount. They pay via your Checkout flow.
|
||||
- After the buyer's payment is successful, thirdweb calls the "buy" method with the expected amount in crypto to the marketplace contract. This method transfers the NFT to the buyer's wallet.
|
||||
|
||||
## Payments
|
||||
|
||||
### Where does thirdweb send the funds after a customer has paid?
|
||||
|
||||
thirdweb directly calls the smart contract and pays for the mint using the native currency specified in the contract function. If you are looking for FIAT payout, it is available for the enterprise customers. Please [contact sales](mailto:[email protected]) .
|
||||
|
||||
### What regions does thirdweb support credit card payments?
|
||||
|
||||
thirdweb uses multiple payment processors and supports credit card payments in most countries (190+) and all 50 US states. Select countries that the US cannot conduct business with are not supported (Cuba, Iran, North Korea, Syria, and the Crimea, Donetsk, and Luhansk Regions). 10+ currencies and languages are supported.
|
||||
|
||||
### What fiat payment options are accepted?
|
||||
|
||||
thirdweb accepts: credit/debit cards (Visa, Mastercard, American Express, Discover, UnionPay, JCB, Diners Club), Apple Pay, Google Pay.
|
||||
|
||||
### What is the price limit for checkouts?
|
||||
|
||||
thirdweb limits checkouts to be priced at $2,000 USD and reserves the right to change this limit based on your checkout's risk profile. For enterprise customers, we can support up to $15,000.
|
||||
|
||||
If you need a higher price limit, please fill out [this form](https://thirdweb.com/contact-us).
|
||||
|
||||
### When does thirdweb require identity verification (KYC)?
|
||||
|
||||
thirdweb combines buyer, merchant, payment method, device, and behavioral signals from multiple vendors to compute the riskiness of a purchase. A small portion of buyers (\< 3% and varies by merchant) will be prompted to submit a photo ID and take a webcam selfie. This step is automated and takes under 1 minute.
|
||||
|
||||
Reminder: thirdweb takes on **all** chargeback liability from the developer!
|
||||
|
||||
## Testing
|
||||
|
||||
### Is there a test credit card?
|
||||
|
||||
Yes, please use the following card numbers. The expiry date, CVV, and postal code do not matter.
|
||||
|
||||
| Card Number | Description |
|
||||
| :--------------- | :----------------------------------- |
|
||||
| 4242424242424242 | A "low risk" card. |
|
||||
| 4000000000009235 | A "high risk" card that prompts KYC. |
|
||||
|
||||
### Where can I get testnet funds?
|
||||
|
||||
Here are some faucets to receive testnet funds:
|
||||
|
||||
| Chain | Currency | Faucet link |
|
||||
| :----------------- | :------- | :---------------------------------- |
|
||||
| Sepolia (Ethereum) | ETH | `https://sepoliafaucet.com` |
|
||||
| Goerli (Optimism) | ETH | `https://faucet.paradigm.xyz` |
|
||||
|
||||
## Customization
|
||||
|
||||
### Can I customize the checkout to match my branding?
|
||||
|
||||
Yes, customize colors and UI elements to fit your theme for [Checkout Links](checkout-link). Or embed [Checkout Elements](elements) in your app with granular control of colors.
|
||||
|
||||
### How do allow my users to redeem utility with their NFT?
|
||||
|
||||
NFTs are a great way to gate content! Once the NFT is purchased, you can provide a link to redeem their utility on the post-purchase page and email.
|
||||
|
||||
Listen to [webhooks](webhooks) to update your database or send a custom email when the NFT has been transferred.
|
||||
|
||||
### Are checkouts translated into different languages?
|
||||
|
||||
Yes, the checkout is localized to the buyer's browser language, or you can force a specific language. See [Translations](translations) for more details.
|
||||
|
||||
### How does checkout support referral codes or discounts?
|
||||
|
||||
You can use our dynamic one-time checkout links to generate payment links with discounts or referral codes for customers. For a full guide to how to implement this, refer [here](one-time-checkout-link).
|
||||
|
||||
## Wallets
|
||||
|
||||
### What if my users don't have a wallet?
|
||||
|
||||
Buyers who don't have a wallet can create one with their email or social login. This uses thirdweb's non-custodial [In-App Wallet](/connect/embedded-wallet/overview) product.
|
||||
|
||||
### How does my user transfer the NFT from the In-App Wallet?
|
||||
|
||||
Buyers can visit [My Wallets](https://ews.thirdweb.com/wallet) to view and transfer NFTs in their In-App Wallets. We cover all gas fees related to transferring and offer a way for users to export their private key.
|
||||
|
||||
## Other
|
||||
|
||||
### Does Checkouts support credit card payments for functions outside of minting like burning and editing NFTs?
|
||||
|
||||
This feature is available upon request. Please contact us at [[email protected]](mailto:[email protected]).
|
||||
|
||||
### What is the customer support level provided to customers?
|
||||
|
||||
For developers, please [visit our support site](https://thirdweb.com/support). If you are looking for guaranteed support time and SLA, please contact [[email protected]](mailto:[email protected]).
|
||||
|
||||
For customers, we respond to inquiries on our support center and to [[email protected]](mailto:[email protected]) within 24 business hours.
|
||||
|
||||
### How does thirdweb handle compliance?
|
||||
|
||||
We conduct KYC (know your customer) verification with driver's license and liveliness check on suspicious customers (\<3% on average and varies by merchant). We also handle dark wallet checks to ensure that flagged wallets and stolen funds are not being used in any of our transactions.
|
||||
|
||||
We take on all liabilities & chargeback risks as the Merchant of Record.
|
||||
|
||||
thirdweb is currently in the process of receiving our SOC II certification.
|
||||
|
||||
### What are the authorization rates?
|
||||
|
||||
We have industry leading authorization rates at 92%. We are able to maintain a high authorization by only operating with NFTs, and having a proprietary fraud engine that keeps our fraud rate at \<0.5%.
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Steps, Step } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Getting Started with NFT Checkout | thirdweb Payments",
|
||||
description:
|
||||
"Get an end-to-end NFT checkout experience up and running in a few steps!",
|
||||
});
|
||||
|
||||
# Getting Started
|
||||
|
||||
Get an end-to-end NFT checkout experience up and running in a few steps!
|
||||
|
||||
<Steps>
|
||||
<Step title="Prerequisite: Deploy your smart contract">
|
||||
|
||||
You can create or deploy a smart contract on [thirdweb dashboard](https://thirdweb.com/team/~/~/contracts).
|
||||
|
||||
</Step>
|
||||
<Step title="Enable your contract for payments">
|
||||
|
||||
Visit the Payments dashboard or use the API to enable your contract for Payments. Start with the first step of our integration guide, [Enable Contract for Payments](enable-contract).
|
||||
|
||||
</Step>
|
||||
<Step title="Create your Checkout Link">
|
||||
|
||||
Generate a complete, pre-built checkout experience by [creating a Checkout Link](checkout-link).
|
||||
|
||||
Want a more branded checkout embedded in your app? Visit the guide on [Embedded Checkout Elements](elements) instead.
|
||||
|
||||
</Step>
|
||||
<Step title="Launch!">
|
||||
Make sure to review the [Go Live Checklist](go-live-checklist) prior to launch to ensure your setup is launch-ready.
|
||||
|
||||
**Important note: you must provide personal documents (KYC) and business documents (KYB) at least 48 hours prior to launching on mainnet.**
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Steps, Step } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Go Live Checklist | thirdweb Payments",
|
||||
description:
|
||||
"Before you go live on mainnet, be sure to review this checklist to ensure your production checkout launch goes smoothly.",
|
||||
});
|
||||
|
||||
# Go Live Checklist
|
||||
|
||||
Before you go live on mainnet, be sure to review this checklist to ensure your production checkout launch goes smoothly.
|
||||
|
||||
<Steps>
|
||||
<Step title="Test your expected flow works on testnet">
|
||||
Testing on mainnet is costly! Please test your contract and checkout implementation on testnet.
|
||||
|
||||
- For Polygon projects, please test on Amoy.
|
||||
- For Ethereum projects, please test on Sepolia.
|
||||
- Visit the [overview page](/payments) to see a list of supported testnets.
|
||||
|
||||
You should be able to test all critical aspects of your smart contract on testnet, including allowlists, signature-based mints, your custom mint method, etc.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Provide personal and business information">
|
||||
|
||||
In order to accept fiat payments (credit cards, debit cards) on mainnet chains, we are required by law to verify your personal (KYC) and business (KYB) information. This information is collected purely for compliance purposes and is not used or shared for any other reason. **You must provide this information at least 48 hours prior to launch.**
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Set up production webhook endpoints">
|
||||
|
||||
Testnet and mainnet transactions call a different set of webhook endpoints. Make sure to add your production endpoint(s) in the Webhooks section of the dashboard before going live.
|
||||
|
||||
</Step>
|
||||
<Step title="Ensure your NFT is priced under $2,000 USD">
|
||||
|
||||
We currently limit checkouts to be priced under $2,000 USD by default. We may be able to change this at our discretion based on your checkout's risk profile.
|
||||
|
||||
To request a higher price limit, please contact [email protected].
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Inform us of high volume launches">
|
||||
|
||||
If you are expecting very high sales volume, please [contact us](https://thirdweb.com/contact-us) in advance so we can make sure the engineering team is aware and ready to support your launch.
|
||||
|
||||
</Step>
|
||||
<Step title="Change ERC-20 addresses (if applicable)">
|
||||
|
||||
Remember that ERC-20 token addresses differ between testnet and mainnet. Make sure to update your contract to point at the mainnet address for the requested token.
|
||||
|
||||
</Step>
|
||||
<Step title="Verify your collection on OpenSea">
|
||||
|
||||
If your contract is on an L2 chain like Polygon, OpenSea may hide your collection by default from buyers' profiles. To avoid this, please contact OpenSea to verify your collection before your launch.
|
||||
|
||||
Buyers are still able to complete their purchase regardless of OpenSea verification, but may need to manually unhide their purchased token from their OpenSea profile if it is hidden by default.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -1,70 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Marketplaces | thirdweb NFT Checkout",
|
||||
description:
|
||||
"thirdweb supports marketplace contracts where a user can list an NFT and receive funds when a buyer purchases it.",
|
||||
});
|
||||
|
||||
# Marketplaces
|
||||
|
||||
thirdweb supports marketplace contracts where a user can list an NFT and receive funds when a buyer purchases it.
|
||||
|
||||
Integration is identical to a checkout using a `CUSTOM_CONTRACT` contract type. Follow the guide on integrating [custom contracts](custom-contracts), and ensure the `mintMethod` identifies the method on your smart contract that purchases the NFT (a "buy" method).
|
||||
|
||||
Your contract method should do the following:
|
||||
|
||||
- Accepts the wallet address where the NFT should be delivered.
|
||||
- Accepts payment in the chain's native coin or [USDC / ERC-20 tokens](erc20-pricing) from the caller (`msg.sender`) of the method.
|
||||
- Transfers the NFT from the seller to the recipient's wallet address. Remember: `msg.sender` is thirdweb's minter wallet and not the buyer's wallet address. Your function needs to ensure the recipient is specified in the argument list.
|
||||
|
||||
## Third-party marketplaces
|
||||
|
||||
Checkouts also supports purchases of tokens listed on third-party Marketplaces such as OpenSea, LooksRare, or X2Y2.
|
||||
|
||||
Purchases on third-party marketplaces are facilitated by the [Reservoir](https://docs.reservoir.tools/docs) contract. You can create a Checkout for an item listed on any [supported Reservoir marketplace](https://docs.reservoir.tools/docs/supported-marketplaces).
|
||||
|
||||
## Integration
|
||||
|
||||
1. Register a new contract
|
||||
1. Set your **Contract Type** to **Reservoir**.
|
||||
2. Leave **Contract Address** blank.
|
||||
2. Create a checkout link with the ID of the registered contract. We recommend using one-time checkout links as the underlying token listed on third-party marketplaces may sell out.
|
||||
|
||||
### Code sample
|
||||
|
||||
```typescript
|
||||
const body = {
|
||||
contractId: "REGISTERED_CONTRACT_ID", // this contract should be registered with Reservoir as the contract type
|
||||
title: "My Checkout",
|
||||
contractArgs: {
|
||||
// TIP! You can add more than 1 token to the nfts array and we'll accept all of those listings!
|
||||
nfts: [
|
||||
{ token: `COLLECTION_CONTRACT_ADDRESS_1:TOKEN_ID_1` },
|
||||
{ token: `COLLECTION_CONTRACT_ADDRESS_2:TOKEN_ID_2` }
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
const resp = await fetch("https://payments.thirdweb.com/api/2022-08-12/checkout-link-intent", {
|
||||
method: "POST",
|
||||
headers: [
|
||||
"Authorization": "Bearer MY_THIRDWEB_API_SECRET_KEY",
|
||||
"Content-Type": "application/json",
|
||||
]
|
||||
body: JSON.stringify(body);
|
||||
});
|
||||
const { checkoutLinkIntentUrl } = await resp.body();
|
||||
|
||||
// Navigate users to this URL to purchase your NFT.
|
||||
|
||||
```
|
||||
|
||||
Please update the following variables:
|
||||
|
||||
- `REGISTERED_CONTRACT_ID`: The Contract ID from Step 1.
|
||||
- `COLLECTION_CONTRACT_ADDRESS`: The contract address of the NFT collection.
|
||||
- `TOKEN_ID`: The token ID of the NFT from the listing. This listing must exist on one of Reservoir's supported marketplaces.
|
||||
- `MY_THIRDWEB_API_SECRET_KEY` found on your API Keys dashboard page.
|
||||
|
||||
For a more embedded and branded checkout experience, you may also sell marketplace NFTs with [Checkout Elements](elements).
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "One-Time Checkout Link | thirdweb NFT Checkout",
|
||||
description:
|
||||
"A one-time Checkout Link is a prebuilt checkout experience that is customized per buyer by providing a fixed price, email, wallet address, quantity, and more.",
|
||||
});
|
||||
|
||||
# One-Time Checkout Link
|
||||
|
||||
A one-time Checkout Link is a prebuilt checkout experience that is customized per buyer by providing a fixed price, email, wallet address, quantity, and more.
|
||||
|
||||
Each unique link allows at most one purchase and expires after the specified duration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you've [enabled your contract for Payments](enable-contract).
|
||||
|
||||
## Use cases
|
||||
|
||||
- Gate purchases via an off-chain allowlist. After verifying the buyer's identity on your app, navigate them to the checkout link.
|
||||
- Implement dynamic pricing by passing in the price for each checkout. This feature allows you to provide coupon codes, bulk discounts, and loyalty discounts.
|
||||
- Restrict the quantity of NFTs your buyers can purchase.
|
||||
- Directly email the checkout URL directly to the buyer's inbox.
|
||||
|
||||
## Create a one-time checkout link
|
||||
|
||||
One-time checkout links can only be created via API.
|
||||
|
||||
### API
|
||||
|
||||
[See the API Reference for creating a one-time checkout link](https://redocly.github.io/redoc/?url=https://payments.thirdweb.com/api/doc#tag/One-time-Checkout-Links/paths/~1api~12022-08-12~1checkout-link-intent/post)
|
||||
@@ -1,76 +0,0 @@
|
||||
import { DocImage } from "@doc";
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Pre-built Contracts | thirdweb NFT Checkout",
|
||||
description: "Pre-built Contracts available on thirdweb NFT Checkout",
|
||||
});
|
||||
|
||||
# Pre-built Contracts
|
||||
|
||||
## Integration
|
||||
|
||||
For some thirdweb contracts, set `contractArgs` when creating [Shareable Checkout Links](/payments/nft-checkout/checkout-link), [One-Time Checkout Links](/payments/nft-checkout/one-time-checkout-link), or [Checkout Elements](/payments/nft-checkout/elements).
|
||||
|
||||
See guide: [Create an ERC721A NFT Drop with Signature-Based Minting](https://blog.thirdweb.com/guides/signature-drop/)
|
||||
|
||||
### NFT Drop
|
||||
|
||||
This is an ERC-721A contract where the NFT metadata is unique but the claim configuration is identical for all buyers. No `contractArgs` should be set.
|
||||
|
||||
### Edition Drop
|
||||
|
||||
This is an ERC-1155 contract where the NFT metadata and claim configuration is identical for all buyers. Set `contractArgs` with the token ID to mint:
|
||||
|
||||
```typescript
|
||||
contractArgs = { tokenId: "0" };
|
||||
```
|
||||
|
||||
### Marketplace
|
||||
|
||||
This is a contract that allows other users to purchase already-minted NFTs. Set `contractArgs` with an array of the marketplace listing IDs of each of the direct listing:
|
||||
|
||||
```typescript
|
||||
contractArgs = {
|
||||
listings: [
|
||||
{ listingId: "0" },
|
||||
{ listingId: "1" },
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Configure the Claim Condition
|
||||
|
||||
Your thirdweb contract must have at least one active claim condition, meaning the **When will this phase start?** date is in the past.
|
||||
|
||||
<DocImage
|
||||
src="https://files.readme.io/994d683-image.png"
|
||||
width={564}
|
||||
height={798}
|
||||
/>
|
||||
|
||||
Helpful tips for each field:
|
||||
|
||||
| Field | Notes |
|
||||
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **When will this phase start?** | thirdweb can only mint NFTs after this date. |
|
||||
| **How many NFTs will you drop in this phase?** | Remember to create NFTs on the **NFTs** tab for NFT Drop contracts. |
|
||||
| **How much do you want to charge to claim each NFT?** | For Goerli, this price must be ≤ 0.0001 ETH. <br /><br />On production, there is a $2,000 price limit. Please fill out [this Typeform](https://fw3786mcxwl.typeform.com/to/B0xIFoiu) to request an increase. |
|
||||
| **What currency do you want to use?** | Supported currencies on thirdweb: - Polygon: MATIC, USDC, WETH <br />- Goerli: ETH <br />- Ethereum: ETH, USDC |
|
||||
| **Who can claim NFTs during this phase?** | If you have an allowlist, please add thirdweb's minter wallets. <br />Otherwise leave this blank. |
|
||||
| **How many NFTs can be claimed per transaction?** | This value must be Unlimited. Otherwise thirdweb's minter wallets will not be able to mint more than this amount. |
|
||||
| **How many seconds do wallets have to wait in-between claiming?** | This value must be 0. Otherwise thirdweb's minter wallets will fail when many mints occur at once. |
|
||||
|
||||
## Debug common blockchain error responses
|
||||
|
||||
| Error Message | Description | Solution |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `!Qty` | The buyer is attempting to purchase more than allowed per wallet. | **:warning: Your Claim Condition must allow thirdweb Wallets to mint an unlimited amount.** <br /><br />The **How many NFTs can be claimed per transaction?** setting must be set to Unlimited. Alternatively, allow thirdweb's minter wallets to mint the full supply in a snapshot. |
|
||||
| `!MaxSupply` | The buyer is attempting to purchase more than the available supply, or the drop is sold out. | Allow more NFTs to be sold, or prevent buyers from navigating to the checkout page if sold out. |
|
||||
| `cant claim yet` | There is no claim phase, or the claim phase has not started. | Wait until the claim phase has started, or set one claim phase's start date to a past date. |
|
||||
| `!PriceOrCurrency` | thirdweb sent the incorrect amount or currency to the contract. | thirdweb may be auto-detecting the price incorrectly. |
|
||||
|
||||
_Source: Drop.sol from thirdweb contracts_
|
||||
|
||||
If your transactions are failing for these reasons, please update the active **Claim Condition** on your thirdweb contract.
|
||||
@@ -1,43 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Translations | thirdweb NFT Checkout",
|
||||
description: "List of supported languages in thirdweb NFT Checkout",
|
||||
});
|
||||
|
||||
# Translations
|
||||
|
||||
Checkouts supports the following languages:
|
||||
|
||||
- English
|
||||
- French
|
||||
- Italian
|
||||
- Spanish
|
||||
- German
|
||||
- Japanese
|
||||
- Korean
|
||||
- Chinese
|
||||
|
||||
By default, text is shown in the buyer's preferred browser language and defaults to English. Non-English languages are **only supported in the Embedded Elements implementation**.
|
||||
|
||||
## Force a specific language
|
||||
|
||||
To always show text in one language, set the `locale` property to the two-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) when creating the Checkout Element in your frontend code.
|
||||
|
||||
### React example
|
||||
|
||||
```typescript
|
||||
<CheckoutWithCard
|
||||
// ...
|
||||
locale="fr"
|
||||
/>
|
||||
```
|
||||
|
||||
### Javascript SDK example
|
||||
|
||||
```typescript
|
||||
createCheckoutWithCardElement({
|
||||
// ...
|
||||
locale: "fr",
|
||||
});
|
||||
```
|
||||
@@ -1,214 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Webhooks | thirdweb NFT Checkout",
|
||||
description:
|
||||
"Configure webhook URLs to have thirdweb notify your backend when successful or failed events occur.",
|
||||
});
|
||||
|
||||
# Webhooks
|
||||
|
||||
Configure webhook URLs to have thirdweb notify your backend when successful or failed events occur.
|
||||
|
||||
## Use cases
|
||||
|
||||
- Update your database when a buyer purchases an NFT.
|
||||
- Send an email to a buyer after their purchase succeeds.
|
||||
- Inform your team in Slack/Discord when a payment or purchase failed.
|
||||
|
||||
## Events
|
||||
|
||||
The following webhook events are supported.
|
||||
|
||||
| Event | Description |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `transfer:succeeded` | The NFT has been delivered to the buyer's wallet. |
|
||||
| `transfer:failed` | The NFT was unable to be delivered after multiple retries. A refund will automatically be processed. |
|
||||
| `payment:succeeded` | A buyer's payment has been successfully completed. |
|
||||
| `payment:failed` | A buyer's payment attempt has been rejected. Extra data fields may be available with information from our payment processor on the failure reason. |
|
||||
| `payment:refunded` | A buyer's payment has been refunded because the mint failed multiple attempts. Extra data fields may be available with the reason for the refund. |
|
||||
| `payment:hold_created` | This is only emitted if `capturePaymentLater` is set. A buyer's payment method has a pre-authorization hold created for the given amount. They have not been charged yet. You can capture this hold to complete their purchase, or cancel it. |
|
||||
|
||||
## Request format
|
||||
|
||||
thirdweb will call your backend with an HTTPS `POST` request:
|
||||
|
||||
### Headers
|
||||
|
||||
```text
|
||||
Content-Type: application/json
|
||||
x-thirdweb-signature: <SIGNATURE_FROM_THIRDWEB>
|
||||
```
|
||||
|
||||
### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "transfer:succeeded",
|
||||
"result": {
|
||||
"id": "5bbbada7-e864-4dac-ae4b-0ee4967f55d8",
|
||||
"checkoutId": "70e08b7f-c528-46af-8b17-76b0e0ade641",
|
||||
"walletAddress": "0x2086Fcd5b0B8F4aFAc376873E861DE00c67D7B83",
|
||||
"walletType": "Preset",
|
||||
"email": "[email protected]",
|
||||
"quantity": 1,
|
||||
"paymentMethod": "BUY_WITH_CARD",
|
||||
"networkFeeUsd": 0.02,
|
||||
"serviceFeeUsd": 1.79,
|
||||
"totalPriceUsd": 45.99,
|
||||
"createdAt": "2022-08-22T19:15:09.755375+00:00",
|
||||
"paymentCompletedAt": "2022-08-22T19:16:01.673+00:00",
|
||||
"transferCompletedAt": "2022-08-22T19:16:18.024+00:00",
|
||||
"claimedTokens": {
|
||||
"collectionAddress": "0x965550329b91b7c703a527347b613E175f38872d",
|
||||
"collectionTitle": "My First NFT",
|
||||
"tokens": [
|
||||
{
|
||||
"transferHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
|
||||
"transferExplorerUrl": "https://polygonscan.com/tx/0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
|
||||
"tokenId": "262",
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "My First Checkout",
|
||||
"transactionHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
|
||||
"valueInCurrency": "0.05",
|
||||
"currency": "ETH",
|
||||
"metadata": {
|
||||
"myAppUserId": "23a9fj2930gya0"
|
||||
},
|
||||
"mintMethod": { ... },
|
||||
"eligibilityMethod": { ... },
|
||||
"contractArgs": { ... },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json JSON (Marketplace)
|
||||
{
|
||||
"event": "transfer:succeeded",
|
||||
"result": {
|
||||
"id": "5bbbada7-e864-4dac-ae4b-0ee4967f55d8",
|
||||
"checkoutId": "70e08b7f-c528-46af-8b17-76b0e0ade641",
|
||||
"walletAddress": "0x2086Fcd5b0B8F4aFAc376873E861DE00c67D7B83",
|
||||
"walletType": "Preset",
|
||||
"email": "[email protected]",
|
||||
"quantity": 1,
|
||||
"paymentMethod": "BUY_WITH_CARD",
|
||||
"networkFeeUsd": 0.02,
|
||||
"serviceFeeUsd": 1.79,
|
||||
"totalPriceUsd": 45.99,
|
||||
"createdAt": "2022-08-22T19:15:09.755375+00:00",
|
||||
"paymentCompletedAt": "2022-08-22T19:16:01.673+00:00",
|
||||
"transferCompletedAt": "2022-08-22T19:16:18.024+00:00",
|
||||
"claimedTokens": {
|
||||
"tokens": [
|
||||
{
|
||||
"transferHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
|
||||
"transferExplorerUrl": "https://polygonscan.com/tx/0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
|
||||
"tokenId": "262",
|
||||
"quantity": 1,
|
||||
"from": "0xce6913CA121276E550b82844A08aCB4dfDc09178",
|
||||
"collectionAddress": "0x965550329b91b7c703a527347b613E175f38872d",
|
||||
"collectionTitle": "My First NFT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "My First Checkout",
|
||||
"transactionHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
|
||||
"valueInCurrency": "0.05",
|
||||
"currency": "ETH",
|
||||
"metadata": {
|
||||
"myAppUserId": "23a9fj2930gya0"
|
||||
},
|
||||
"mintMethod": { ... },
|
||||
"eligibilityMethod": { ... },
|
||||
"contractArgs": { ... },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Provide a webhook handler URL
|
||||
|
||||
Webhooks are configured separately for testnet and production checkout on the dashboard. Webhook URLs must be publicly accessible `https` endpoints.
|
||||
|
||||
Do not provide a `localhost` URL to test your local server. We recommend testing your development server with a service like [ngrok](https://ngrok.io/) to serve a temporary public URL.
|
||||
|
||||
**Please return a `2xx` response for unexpected or unused event types to prevent unnecessary retries.**
|
||||
|
||||
### Verify the signature header
|
||||
|
||||
To ensure the request came from thirdweb, each webhook request signs the payload and provides this signature in the `x-thirdweb-signature` header.
|
||||
|
||||
To verify this signature, create a **SHA-256 HMAC hash** with your **thirdweb payments secret key** that can be found in your Team > Project > Connect > Pay > Settings in [dashboard](https://thirdweb.com/team) and the **body payload as the message** (as a JSON-encoded string).
|
||||
|
||||
### Example implementation
|
||||
|
||||
Here's a simplified HTTP handler in Next.js:
|
||||
|
||||
```typescript
|
||||
import { createHmac, timingSafeEqual } from "crypto";
|
||||
|
||||
const thirdwebCheckoutsWebhookHandler = (req, res) => {
|
||||
const apiKey = "<YOUR_API_KEY>"; // Your thirdweb payments secret key
|
||||
|
||||
// Get the provided signature.
|
||||
const signature = req.headers["x-thirdweb-signature"];
|
||||
// Compute the expected signature.
|
||||
const hash = createHmac("sha256", apiKey)
|
||||
.update(JSON.stringify(req.body)) // {"event":"transfer:succeeded","result":{"id":...
|
||||
.digest("hex");
|
||||
// Confirm the provided signature matches.
|
||||
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(hash))) {
|
||||
return res.status(400).send("Signature mismatch!");
|
||||
}
|
||||
|
||||
switch (req.body.event) {
|
||||
case "transfer:succeeded":
|
||||
// Handle when an NFT was delivered.
|
||||
case "transfer:failed":
|
||||
// Handle when an NFT could not be delivered.
|
||||
default:
|
||||
// Ignore all other events and return 2xx.
|
||||
}
|
||||
|
||||
return res.status(200).send("OK");
|
||||
};
|
||||
```
|
||||
|
||||
### Test the webhook response
|
||||
|
||||
Use the **Test webhook** button to send a dummy payload to your webhook URL and see response status/body.
|
||||
|
||||
### View recent webhook events
|
||||
|
||||
Select the **List events** button to view the recent webhook events, including the request body and response status/body from your backend. This view is useful to debug misconfigured webhook handlers.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why do I need to verify the signature header?
|
||||
|
||||
If your server is public, a bad actor can spoof a webhook request. Verifying the signature ensures the payload has not been changed. If a bad actor changes the webhook request body, the signature would not match the signed payload.
|
||||
|
||||
### Why is my signature header mismatched?
|
||||
|
||||
Here are common reasons the signature header may be mismatched.
|
||||
|
||||
- Check if the header is set lower-cased. Some server frameworks (e.g. Next.js) use lowercase request header names since they are case-insensitive (RFC 2616).
|
||||
- Make sure you're passing the entire body as the message in the HMAC signature. Some frameworks require you to configure the HTTP handler to not parse the request body (e.g. [Next.js](https://nextjs.org/docs/api-routes/request-helpers)).
|
||||
- Make sure your API key is valid.
|
||||
|
||||
### What IP address will webhook requests come from?
|
||||
|
||||
Webhooks will be sent from the IP address `44.225.232.73`.
|
||||
|
||||
### How often will webhook requests be retried?
|
||||
|
||||
Webhooks are retried every five minutes for up to one hour until a `2xx` response is returned.
|
||||
|
||||
### Can I filter which webhook events are sent?
|
||||
|
||||
Currently there is no way to filter which events are sent to your webhook URLs. thirdweb may add new webhook event types without notice. Please ignore events that you don't need by returning a `2xx` response.
|
||||
@@ -1,84 +0,0 @@
|
||||
import { DocImage } from "@doc";
|
||||
import checkoutsOverviewImage from "./assets/checkouts-overview.png";
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Payments",
|
||||
icon: "infra",
|
||||
},
|
||||
title: "thirdweb Payments",
|
||||
description:
|
||||
"Checkout delivers the easiest NFT payments experience for you and your buyers. Sell NFTs on any one of our supported EVM chains and allow your users to pay with any supported payment option. You and your buyers can expect instant payouts and instant NFT processing - all done through your smart contract.",
|
||||
});
|
||||
|
||||
# thirdweb Payments
|
||||
|
||||
Checkouts delivers the easiest NFT payments experience for you and your buyers. Sell NFTs on any one of our supported EVM chains and allow your users to pay with
|
||||
any supported payment option. You and your buyers can expect instant payouts and instant NFT processing - all done through your smart contract.
|
||||
|
||||
<DocImage src={checkoutsOverviewImage} alt="Checkouts Process Diagram" />
|
||||
|
||||
### Accepted payment options for buyers
|
||||
|
||||
thirdweb accepts fiat payments via Credit & Debit Card, Apple Pay, and Google Pay. Card and other fiat payments are accepted from all 50 US states and most countries. We also support crypto payments via ETH.
|
||||
|
||||
### Supported chains and currencies for developers
|
||||
|
||||
thirdweb currently supports checkout for NFTs on any of the following chains with any of the following currencies:
|
||||
|
||||
| Blockchain (Mainnet) | Supported Currencies |
|
||||
| :------------------- | :------------------------------ |
|
||||
| Arbitrum Nova | ETH |
|
||||
| Arbitrum One | ETH, USDC.e\* |
|
||||
| Avalanche | AVAX, USDC.e\* |
|
||||
| Ethereum | ETH, USDC\* |
|
||||
| Optimism | ETH, USDC\* |
|
||||
| Polygon | MATIC, USDC\*, USDC.e\*, WETH\* |
|
||||
| Zora | ETH |
|
||||
|
||||
\* - ERC-20 tokens are available for pro or enterprise customers only
|
||||
|
||||
#
|
||||
|
||||
| Blockchain (Testnet) | Supported Currencies |
|
||||
| :------------------- | :------------------- |
|
||||
| Arbitrum Sepolia | ETH |
|
||||
| Avalanche Fuji | AVAX |
|
||||
| Base Sepolia | ETH |
|
||||
| Sepolia | ETH |
|
||||
| Zora Testnet | ETH |
|
||||
| Polygon Amoy Testnet | MATIC |
|
||||
|
||||
### Fraud prevention & chargeback protection
|
||||
|
||||
thirdweb deters bots and fraudulent activity by using multiple data points about the buyer's device, network, behavior, payment, and more. Only high-risk buyers will need to verify their identity with an ID and selfie.
|
||||
|
||||
We protect you from these concerns by offering **full chargeback protection.**
|
||||
|
||||
### Reliable NFT Delivery
|
||||
|
||||
thirdweb manages a fleet of funded crypto wallets to handle blockchain transactions at scale. Queues are automated to monitor for stuck transactions, failed onchain calls, and low funds.
|
||||
|
||||
### Conversion-optimized UX
|
||||
|
||||
Our checkout flow accounts for many cases to provide buyers a seamless experience with minimal steps:
|
||||
|
||||
- Does the buyer need a crypto wallet or do they already have one?
|
||||
- Do they exhibit bot-like or suspicious behavior?
|
||||
- Do they have enough tokens to mint directly from their wallet?
|
||||
- Is your user trying to purchase more than one NFT?
|
||||
- Is your user allowlisted to purchase?
|
||||
- Are there enough NFTs remaining to purchase?
|
||||
|
||||
### Webhooks & custom metadata
|
||||
|
||||
Configure webhooks to notify your backend when payments and transfers are completed. Webhooks allow you to unlock NFT-gated utilities, send customized emails, and more.
|
||||
|
||||
Provide custom metadata to tag purchases with added information.
|
||||
|
||||
### Analytics
|
||||
|
||||
View purchases to your checkouts including breakdowns by payment method and wallet. Export your data with additional details including buyer location, transaction hash, conversion rate, and custom metadata.
|
||||
|
||||
Get visibility into failed transactions to speed up debugging and resolve support questions.
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
import { PaymentsNFTCheckoutIcon } from "@/icons";
|
||||
|
||||
const checkoutSlug = "/payments/nft-checkout";
|
||||
|
||||
export const sidebar: SideBar = {
|
||||
name: "Payments",
|
||||
links: [
|
||||
{
|
||||
name: "Overview",
|
||||
href: "/payments",
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
name: "NFT Checkout",
|
||||
icon: <PaymentsNFTCheckoutIcon />,
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
name: "Get Started",
|
||||
href: `${checkoutSlug}/getting-started`,
|
||||
links: [
|
||||
{
|
||||
name: "Enable Contract for Payments",
|
||||
href: `${checkoutSlug}/enable-contract`,
|
||||
},
|
||||
{
|
||||
name: "Create Checkout Link",
|
||||
href: `${checkoutSlug}/checkout-link`,
|
||||
},
|
||||
{
|
||||
name: "Go Live Checklist",
|
||||
href: `${checkoutSlug}/go-live-checklist`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Embedded Elements",
|
||||
|
||||
links: [
|
||||
{
|
||||
name: "Overview",
|
||||
href: `${checkoutSlug}/elements`,
|
||||
},
|
||||
{
|
||||
name: "CheckoutWithCard",
|
||||
href: `${checkoutSlug}/checkout-with-card`,
|
||||
},
|
||||
{
|
||||
name: "CheckoutWithEth",
|
||||
href: `${checkoutSlug}/checkout-with-eth`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
href: `${checkoutSlug}/webhooks`,
|
||||
},
|
||||
{ name: "Translations", href: `${checkoutSlug}/translations` },
|
||||
{ name: "Marketplaces", href: `${checkoutSlug}/marketplaces` },
|
||||
{
|
||||
name: "One-Time Checkout Link",
|
||||
href: `${checkoutSlug}/one-time-checkout-link`,
|
||||
},
|
||||
{
|
||||
name: "Pre-built Contracts",
|
||||
href: `${checkoutSlug}/pre-built-contracts`,
|
||||
},
|
||||
{
|
||||
name: "Custom Contracts",
|
||||
href: `${checkoutSlug}/custom-contracts`,
|
||||
},
|
||||
{
|
||||
name: "ERC-20 Pricing",
|
||||
href: `${checkoutSlug}/erc20-pricing`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ separator: true },
|
||||
{ name: "API Reference", href: `${checkoutSlug}/api-reference` },
|
||||
{ name: "FAQ", href: `${checkoutSlug}/faq` },
|
||||
],
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Details, Callout, createMetadata } from "@doc";
|
||||
import ActiveChainMDX from "./props/activeChain.mdx";
|
||||
import ClientIdMDX from "./props/clientId.mdx";
|
||||
import AuthConfigMDX from "./props/authConfig.mdx";
|
||||
import SupportedWalletsMDX from "./props/supportedWallets.mdx";
|
||||
import SupportedChainsMDX from "./props/supportedChains.mdx";
|
||||
import AutoConnectMDX from "./props/autoConnect.mdx";
|
||||
import LocaleMDX from "./props/locale.mdx";
|
||||
import ThemeMDX from "./props/theme.mdx";
|
||||
import DappMetaMDX from "./props/dAppMeta.mdx";
|
||||
import SDKOptionsMDX from "./props/sdkOptions.mdx";
|
||||
import StorageInterfaceMDX from "./props/storageInterface.mdx";
|
||||
import QueryClientMDX from "./props/queryClient.mdx";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "ThirdwebProvider: thirdweb React Native SDK",
|
||||
icon: "react",
|
||||
},
|
||||
title: "ThirdwebProvider | thirdweb React Native SDK",
|
||||
description: "API references for the ThirdwebProvider component",
|
||||
});
|
||||
|
||||
# ThirdwebProvider
|
||||
|
||||
The `ThirdwebProvider` is a wrapper component that provides access to all of the SDK’s hooks and UI components.
|
||||
|
||||
<Callout variant='info' title="API Key">
|
||||
|
||||
You will require an API key to use thirdweb's infrastructure services with the SDK.
|
||||
|
||||
you need to first obtain an API key from the [dashboard](https://thirdweb.com/team/~/~) by creating a new project and then copy the "Client ID" and pass it to the `ThirdwebProvider` as the `clientId` prop.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Usage
|
||||
|
||||
Wrap your app in the `ThirdwebProvider` to access the SDK’s functionality from anywhere in your app.
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function Example() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain="ethereum" clientId="your-client-id">
|
||||
<App />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
<Details id="activeChain" summary="activeChain" tags={["Optional"]}>
|
||||
<ActiveChainMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="authConfig" summary="authConfig" tags={["Optional"]}>
|
||||
<AuthConfigMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="autoConnect" summary="autoConnect" tags={["Optional"]}>
|
||||
<AutoConnectMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="clientId" summary="clientId" tags={["Recommended", "Optional"]}>
|
||||
<ClientIdMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="dAppMeta" summary="dAppMeta" tags={["Optional"]}>
|
||||
<DappMetaMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="locale" summary="locale" tags={["Optional"]}>
|
||||
<LocaleMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="queryClient" summary="queryClient" tags={["Optional"]}>
|
||||
<QueryClientMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="sdkOptions" summary="sdkOptions" tags={["Optional"]}>
|
||||
<SDKOptionsMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="storageInterface" summary="storageInterface" tags={["Optional"]}>
|
||||
<StorageInterfaceMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="supportedChains" summary="supportedChains" tags={["Optional"]}>
|
||||
<SupportedChainsMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="supportedWallets" summary="supportedWallets" tags={["Optional"]}>
|
||||
<SupportedWalletsMDX />
|
||||
</Details>
|
||||
|
||||
<Details id="theme" summary="theme" tags={["Optional"]}>
|
||||
<ThemeMDX />
|
||||
</Details>
|
||||
@@ -1,151 +0,0 @@
|
||||
import { Details, Callout } from "@doc";
|
||||
import DefaultChainsMDX from "../../common/defaultChains.mdx";
|
||||
|
||||
The `activeChain` prop determines which chain you want your app to be operating on.
|
||||
|
||||
It defaults to `"ethereum"` if `activeChain` prop is not provided.
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
import { Gnosis } from "@thirdweb-dev/chains";
|
||||
|
||||
function Example() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain={Gnosis} clientId="your-client-id">
|
||||
<App />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Details id="using-default-chain" summary="Using a default supported chain">
|
||||
|
||||
If you are using one of our <a href="#default-chains">default chains</a>, provide the name of the chain as a `string` to the `activeChain` prop.
|
||||
|
||||
<DefaultChainsMDX />
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain="ethereum" clientId="your-client-id">
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
<Details id="using-chains-package" summary="Import a chain from chains package">
|
||||
|
||||
If the chain you are looking for is not one of default supported chains, you can import a chain from the [@thirdweb-dev/chains](https://github.com/thirdweb-dev/js/tree/v4/legacy_packages/chains) package which has 1000+ chains.
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
import { <chain_id> } from "@thirdweb-dev/chains";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain={<chain_id>} clientId="your-client-id">
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Override Default Values
|
||||
|
||||
Override the default values (such as an RPC URL) for any given chain.
|
||||
|
||||
<Callout variant='info'>
|
||||
By default, the `@thirdweb-dev/chains` provides free-to-use RPCs. No configuration required!
|
||||
|
||||
[View the default RPC URLs for each network](https://github.com/thirdweb-dev/js/tree/v4/legacy_packages/chains/chains).
|
||||
|
||||
</Callout>
|
||||
|
||||
Using the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax),
|
||||
you can override any properties of a chain, such as the `rpc` field.
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
import { <chain_id> } from "@thirdweb-dev/chains";
|
||||
|
||||
const activeChain = {
|
||||
...<chain_id>,
|
||||
rpc: ["https://<your-rpc-to-use>.com"], // Override the "rpc" field.
|
||||
// ... Override any other fields you want to customize.
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<ThirdwebProvider activeChain={activeChain}>
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
<Details id="create-custom-chain" summary="Specify a custom chain">
|
||||
|
||||
If your chain is not included in the `@thirdweb-dev/chains` package,
|
||||
you can provide the chain information yourself to the `activeChain` prop.
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
const customChain = {
|
||||
// Required information for connecting to the network
|
||||
chainId: 59140, // Chain ID of the network
|
||||
rpc: ["<your-rpc-url-here>"], // Array of RPC URLs to use
|
||||
|
||||
// Information for adding the network to your wallet (how it will appear for first time users) === \\
|
||||
// Information about the chain's native currency (i.e. the currency that is used to pay for gas)
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: "Consensys ETH",
|
||||
symbol: "crETH",
|
||||
},
|
||||
shortName: "czkevm", // Display value shown in the wallet UI
|
||||
slug: "consensys", // Display value shown in the wallet UI
|
||||
testnet: true, // Boolean indicating whether the chain is a testnet or mainnet
|
||||
chain: "ConsenSys", // Name of the network
|
||||
name: "ConsenSys zkEVM Testnet", // Name of the network
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<ThirdwebProvider activeChain={customChain}>
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="Local Node" id='local-node'>
|
||||
|
||||
If you are running a local node using a tool such as [Hardhat](https://hardhat.org/hardhat-network/docs/overview) or
|
||||
[Anvil](https://github.com/foundry-rs/foundry/blob/master/anvil/README.md),
|
||||
provide `"localhost"` as the `activeChain` prop, (or `Localhost` imported from `@thirdweb-dev/chains`).
|
||||
|
||||
[Deploy](/contracts/deploy/overview) or [import](https://thirdweb.com/team/~/~/contracts) your contracts to the dashboard to interact with them.
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function MyApp() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain="localhost">
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
@@ -1,26 +0,0 @@
|
||||
The configuration object for setting up [Auth](/connect/auth), allowing users to sign in with their wallet.
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `authUrl` | `string` | The backend URL of the authentication endpoints. For example, if your endpoints are at `/api/auth/login`, `/api/auth/logout`, etc. then this should be set to `/api/auth`. |
|
||||
| `domain` | `string` | The frontend domain used to generate the login payload. This domain should match the domain used on your auth backend. |
|
||||
| `secureStorage` | `ISecureStorage` | Secure storage to use when working with JWT tokens. ** By default auth uses cookies so no need to set this unless you want to specifically use JWT tokens ** |
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function MyApp() {
|
||||
return (
|
||||
<ThirdwebProvider
|
||||
authConfig={{
|
||||
authUrl: "/api/auth",
|
||||
domain: "https://example.com",
|
||||
}}
|
||||
>
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
When the user has connected their wallet to your site, this flag determines
|
||||
whether or not you want to automatically connect to the last connected wallet when user visits your site again in the future.
|
||||
|
||||
Defaults to `true`
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function MyApp() {
|
||||
return (
|
||||
<ThirdwebProvider autoConnect={true}>
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
The `clientId` prop is required to use the thirdweb infrastructure services with the SDK. You can get a client ID by creating an API key [on thirdweb dashboard](https://thirdweb.com/create-api-key).
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function MyApp() {
|
||||
return (
|
||||
<ThirdwebProvider activeChain="ethereum" clientId="your-client-id">
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
This Metadata is passed to the wallet. Some wallets use this information to display the metadata of the app to the user when a user is connecting the wallet to your app.
|
||||
|
||||
If no `dAppMeta` prop is set, it defaults to below object:
|
||||
|
||||
```js
|
||||
{
|
||||
name: "thirdweb powered dApp",
|
||||
url: "https://thirdweb.com",
|
||||
};
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------------- | ------- | ----------------------------------------------------------------- |
|
||||
| `name` | string | the name of your app |
|
||||
| `description` | string | optional - a description of your app |
|
||||
| `logoUrl` | string | optional - a URL that points to a logo (or favicon) of your app |
|
||||
| `url` | string | optional - the URL where your app is hosted |
|
||||
| `isDarkMode` | boolean | optional - whether to show the connect dialog in dark mode or not |
|
||||
|
||||
```jsx
|
||||
import { ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
function MyApp() {
|
||||
return (
|
||||
<ThirdwebProvider
|
||||
dAppMeta={{
|
||||
name: "My App",
|
||||
description: "My app description",
|
||||
logoUrl: "https://example.com/logo.png",
|
||||
url: "https://example.com",
|
||||
isDarkMode: true,
|
||||
}}
|
||||
>
|
||||
<YourApp />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||