Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c768a0ef9 | ||
|
|
a388439022 | ||
|
|
7b873d38db | ||
|
|
d33a943176 | ||
|
|
d5e0b6eb73 | ||
|
|
75e9be0358 | ||
|
|
a704c3f2b0 | ||
|
|
b0eccf365a | ||
|
|
f3525c7c01 | ||
|
|
e516b4ddc1 | ||
|
|
dffa457a39 | ||
|
|
859032dc17 | ||
|
|
e0ee69b061 | ||
|
|
b6be8d67ef | ||
|
|
6ddb15cf02 | ||
|
|
551ec680df | ||
|
|
7b9ce493c8 | ||
|
|
b27fcd3ba0 | ||
|
|
c40603f36e | ||
|
|
458246fea1 |
Vendored
+5
-2
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
@@ -18,8 +20,9 @@
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"quickfix.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
"quickfix.biome": "always",
|
||||
"source.fixAll.biome": "always",
|
||||
"source.organizeImports.biome": "always"
|
||||
},
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
@@ -68,4 +68,7 @@ module.exports = [
|
||||
"/token",
|
||||
"/vault",
|
||||
"/monetize/bridge",
|
||||
// ai
|
||||
"/ai-privacy-policy",
|
||||
"/ai-terms",
|
||||
];
|
||||
|
||||
+130
-130
@@ -1,132 +1,132 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@sentry/nextjs": "9.34.0",
|
||||
"@shazow/whatsabi": "0.22.2",
|
||||
"@stripe/react-stripe-js": "4.0.2",
|
||||
"@stripe/stripe-js": "7.9.0",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@thirdweb-dev/api": "workspace:*",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@thirdweb-dev/vault-sdk": "workspace:*",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"@vercel/og": "^0.6.8",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"abitype": "1.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"fuse.js": "7.1.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.6.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^7.2.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.3",
|
||||
"remark-gfm": "4.0.1",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"stripe": "17.7.0",
|
||||
"swagger-ui-react": "^5.24.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-debounce": "^10.0.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/bundle-analyzer": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
"@storybook/nextjs": "9.0.15",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-storybook": "9.0.15",
|
||||
"knip": "5.60.2",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.6",
|
||||
"storybook": "9.0.15",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"name": "thirdweb-dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
|
||||
"build-storybook": "storybook build",
|
||||
"build:analyze": "ANALYZE=true pnpm run build",
|
||||
"dev": "next dev --turbopack",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@sentry/nextjs": "9.34.0",
|
||||
"@shazow/whatsabi": "0.22.2",
|
||||
"@stripe/react-stripe-js": "4.0.2",
|
||||
"@stripe/stripe-js": "7.9.0",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@thirdweb-dev/api": "workspace:*",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@thirdweb-dev/vault-sdk": "workspace:*",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"@vercel/og": "^0.6.8",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"abitype": "1.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"fuse.js": "7.1.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.6.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^7.2.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.2.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.3",
|
||||
"remark-gfm": "4.0.1",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"stripe": "17.7.0",
|
||||
"swagger-ui-react": "^5.24.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-debounce": "^10.0.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/bundle-analyzer": "15.3.6",
|
||||
"@next/eslint-plugin-next": "15.3.6",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
"@storybook/nextjs": "9.0.15",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-storybook": "9.0.15",
|
||||
"knip": "5.60.2",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.6",
|
||||
"storybook": "9.0.15",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"name": "thirdweb-dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
|
||||
"build-storybook": "storybook build",
|
||||
"build:analyze": "ANALYZE=true pnpm run build",
|
||||
"dev": "next dev --turbopack",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,50 +1,23 @@
|
||||
"use client";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useId, useMemo } from "react";
|
||||
import { Area, AreaChart, Bar, BarChart } from "recharts";
|
||||
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||
import { ArrowUpRightIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { LoadingDots } from "@/components/ui/LoadingDots";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
type FakeCartData = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
function generateRandomData(): FakeCartData[] {
|
||||
return Array.from({ length: 30 }, () => ({
|
||||
value: Math.floor(Math.random() * 100 + 30),
|
||||
}));
|
||||
}
|
||||
|
||||
const skeletonChartConfig = {
|
||||
value: {
|
||||
color: "hsl(var(--muted)/90%)",
|
||||
label: "Value",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function EmptyChartState({
|
||||
children,
|
||||
type,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
type: "bar" | "area";
|
||||
}) {
|
||||
const barChartData = useMemo(() => generateRandomData(), []);
|
||||
|
||||
export function EmptyChartState({ content }: { content?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative z-0 h-full w-full">
|
||||
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center text-base text-muted-foreground">
|
||||
{children ?? (
|
||||
{content || (
|
||||
<div className="flex items-center gap-3 flex-col">
|
||||
<div className="rounded-full border p-2 bg-background">
|
||||
<XIcon className="size-4" />
|
||||
</div>
|
||||
<p className="text-base"> No data available </p>
|
||||
<p className="text-sm"> No data available </p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SkeletonBarChart data={barChartData} type={type} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,7 +26,7 @@ export function LoadingChartState({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none flex h-full w-full items-center justify-center rounded-lg bg-muted/30",
|
||||
"pointer-events-none flex h-full w-full items-center justify-center bg-card rounded-lg",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -62,51 +35,40 @@ export function LoadingChartState({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonBarChart(props: {
|
||||
data: FakeCartData[];
|
||||
type: "bar" | "area";
|
||||
export function EmptyChartStateGetStartedCTA(props: {
|
||||
link: {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
title: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const fillAreaSkeletonId = useId();
|
||||
return (
|
||||
<ChartContainer
|
||||
className="pointer-events-none h-full w-full blur-[5px] aspect-auto"
|
||||
config={skeletonChartConfig}
|
||||
>
|
||||
{props.type === "bar" ? (
|
||||
<BarChart
|
||||
data={props.data}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<Bar dataKey="value" fill="var(--color-value)" radius={8} />
|
||||
</BarChart>
|
||||
) : (
|
||||
<AreaChart
|
||||
data={props.data}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={fillAreaSkeletonId} x1="0" x2="0" y1="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={"hsl(var(--muted-foreground)/0.5)"}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop offset="95%" stopColor="transparent" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey="value"
|
||||
fill={`url(#${fillAreaSkeletonId})`}
|
||||
radius={8}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
<div className="flex items-center flex-col">
|
||||
<div className="rounded-full border p-2 mb-4">
|
||||
<XIcon className="size-4 text-foreground" />
|
||||
</div>
|
||||
<div className="mb-5 space-y-1">
|
||||
<h3 className="text-base text-foreground text-center font-medium">
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.description && (
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
className="rounded-full gap-2"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
<Link href={props.link.href}>
|
||||
{props.link.label} <ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,9 +90,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState type="area">
|
||||
{props.emptyChartState}
|
||||
</EmptyChartState>
|
||||
<EmptyChartState content={props.emptyChartState} />
|
||||
) : (
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
|
||||
@@ -78,9 +78,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState type="bar">
|
||||
{props.emptyChartState}
|
||||
</EmptyChartState>
|
||||
<EmptyChartState content={props.emptyChartState} />
|
||||
) : (
|
||||
<BarChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
export function TotalValueChartHeader(props: {
|
||||
total: number;
|
||||
title: string;
|
||||
isUsd?: boolean;
|
||||
isPending: boolean;
|
||||
viewMoreLink: string | undefined;
|
||||
}) {
|
||||
@@ -18,7 +19,9 @@ export function TotalValueChartHeader(props: {
|
||||
render={(value) => {
|
||||
return (
|
||||
<p className="text-3xl font-semibold tracking-tight">
|
||||
{compactNumberFormatter.format(value)}
|
||||
{props.isUsd
|
||||
? compactUSDFormatter.format(value)
|
||||
: compactNumberFormatter.format(value)}
|
||||
</p>
|
||||
);
|
||||
}}
|
||||
@@ -48,3 +51,10 @@ const compactNumberFormatter = new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
const compactUSDFormatter = new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 2,
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
THIRDWEB_BRIDGE_URL,
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_ENGINE_CLOUD_URL,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
THIRDWEB_PAY_DOMAIN,
|
||||
@@ -38,6 +39,7 @@ export function getConfiguredThirdwebClient(options: {
|
||||
rpc: THIRDWEB_RPC_DOMAIN,
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
storage: THIRDWEB_STORAGE_DOMAIN,
|
||||
engineCloud: new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,3 +25,6 @@ export const THIRDWEB_INSIGHT_API_DOMAIN =
|
||||
|
||||
export const THIRDWEB_BRIDGE_URL =
|
||||
process.env.NEXT_PUBLIC_BRIDGE_URL || "bridge.thirdweb-dev.com";
|
||||
|
||||
export const THIRDWEB_ENGINE_CLOUD_URL =
|
||||
process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "https://engine.thirdweb-dev.com";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export const formatTickerNumber = (value: number) => {
|
||||
|
||||
+11
-9
@@ -63,15 +63,17 @@ function MetricPlaceholders({
|
||||
{metric.label}
|
||||
</span>
|
||||
<div className="h-32 w-full">
|
||||
<EmptyChartState type="area">
|
||||
{status === "active" ? (
|
||||
<Badge>Coming Soon</Badge>
|
||||
) : status === "pending" ? (
|
||||
<p className="text-xs">Activation in progress.</p>
|
||||
) : (
|
||||
<p className="text-xs">Activate service to view metrics.</p>
|
||||
)}
|
||||
</EmptyChartState>
|
||||
<EmptyChartState
|
||||
content={
|
||||
status === "active" ? (
|
||||
<Badge>Coming Soon</Badge>
|
||||
) : status === "pending" ? (
|
||||
<p className="text-xs">Activation in progress.</p>
|
||||
) : (
|
||||
<p className="text-xs">Activate service to view metrics.</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getAiUsage } from "@/api/analytics";
|
||||
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
|
||||
import { AiTokenUsageChartCardUI } from "../ai/analytics/chart/AiTokenUsageChartCard";
|
||||
|
||||
async function AsyncAiAnalytics(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const stats = await getAiUsage(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
return (
|
||||
<AiTokenUsageChartCardUI
|
||||
title="AI token volume"
|
||||
isPending={false}
|
||||
aiUsageStats={stats}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
|
||||
emptyChartState={
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View more details",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No AI usage found in selected time period"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AIAnalyticsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
fallback={
|
||||
<AiTokenUsageChartCardUI
|
||||
isPending={true}
|
||||
title="AI token volume"
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
|
||||
aiUsageStats={[]}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncAiAnalytics
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
from={props.from}
|
||||
to={props.to}
|
||||
interval={props.interval}
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
);
|
||||
}
|
||||
+11
-1
@@ -1,5 +1,6 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getEOAAndInAppWalletConnections } from "@/api/analytics";
|
||||
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
|
||||
import {
|
||||
AutoMergeBarChart,
|
||||
type StatData,
|
||||
@@ -75,7 +76,16 @@ function AllWalletConnectionsChartUI(props: {
|
||||
isPending={props.isPending}
|
||||
exportButton={undefined}
|
||||
maxLabelsToShow={5}
|
||||
emptyChartState={undefined}
|
||||
emptyChartState={
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View Configuration",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets/configure`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No wallet connections found in selected time period"
|
||||
/>
|
||||
}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets`}
|
||||
/>
|
||||
);
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getUniversalBridgeUsage } from "@/api/analytics";
|
||||
import {
|
||||
EmptyChartState,
|
||||
EmptyChartStateGetStartedCTA,
|
||||
LoadingChartState,
|
||||
} from "@/components/analytics/empty-chart-state";
|
||||
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
|
||||
import type { UniversalBridgeStats } from "@/types/analytics";
|
||||
import { TotalVolumePieChart } from "../payments/components/TotalVolumePieChart";
|
||||
|
||||
async function AsyncBridgeCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const data = await getUniversalBridgeUsage(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
return (
|
||||
<BridgeChartCardUI
|
||||
data={data}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function BridgeChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
fallback={<LoadingChartState className="min-h-[40px] h-full border" />}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncBridgeCard {...props} />
|
||||
</ResponsiveSuspense>
|
||||
);
|
||||
}
|
||||
|
||||
function BridgeChartCardUI(props: {
|
||||
data: UniversalBridgeStats[];
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const total =
|
||||
props.data.reduce((acc, curr) => acc + curr.amountUsdCents, 0) / 100;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-card border rounded-lg">
|
||||
<TotalValueChartHeader
|
||||
title="Bridge Volume"
|
||||
total={total}
|
||||
isUsd={true}
|
||||
isPending={false}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/bridge`}
|
||||
/>
|
||||
<div className="p-6 grow flex items-center justify-center">
|
||||
{total === 0 ? (
|
||||
<EmptyChartState
|
||||
content={
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View Configuration",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No bridge volume found in selected time period"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TotalVolumePieChart data={props.data} hideTotal={true} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+9
-28
@@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { EmptyStateContent } from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import { useSetResponsiveSearchParams } from "responsive-rsc";
|
||||
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
|
||||
import type { InAppWalletStats, UniversalBridgeStats } from "@/types/analytics";
|
||||
import { CombinedBarChartCard } from "../../../../components/Analytics/CombinedBarChartCard";
|
||||
|
||||
type AggregatedMetrics = {
|
||||
activeUsers: number;
|
||||
newUsers: number;
|
||||
totalVolume: number;
|
||||
feesCollected: number;
|
||||
};
|
||||
|
||||
@@ -37,29 +36,20 @@ export function ProjectHighlightsCard(props: {
|
||||
const chartConfig = {
|
||||
activeUsers: { color: "hsl(var(--chart-1))", label: "Active Users" },
|
||||
newUsers: { color: "hsl(var(--chart-3))", label: "New Users" },
|
||||
totalVolume: {
|
||||
color: "hsl(var(--chart-2))",
|
||||
emptyContent: (
|
||||
<EmptyStateContent
|
||||
description="Onramp, swap, and bridge with thirdweb's Payments."
|
||||
link="https://portal.thirdweb.com/payments"
|
||||
metric="Payments"
|
||||
/>
|
||||
),
|
||||
isCurrency: true,
|
||||
label: "Total Volume",
|
||||
},
|
||||
feesCollected: {
|
||||
color: "hsl(var(--chart-4))",
|
||||
emptyContent: (
|
||||
<EmptyStateContent
|
||||
description="Your app hasn't collected any fees yet."
|
||||
link={`/team/${teamSlug}/${projectSlug}/bridge/configuration`}
|
||||
metric="Fees"
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View Configuration",
|
||||
href: `/team/${teamSlug}/${projectSlug}/bridge/configuration`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No bridge revenue generated in selected time period"
|
||||
/>
|
||||
),
|
||||
isCurrency: true,
|
||||
label: "Bridge Fee Revenue",
|
||||
label: "Bridge Revenue",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -129,14 +119,6 @@ function processTimeSeriesData(
|
||||
.filter((u) => new Date(u.date).toISOString().slice(0, 10) === date)
|
||||
.reduce((acc, curr) => acc + curr.newUsers, 0);
|
||||
|
||||
const volume = volumeStats
|
||||
.filter(
|
||||
(v) =>
|
||||
new Date(v.date).toISOString().slice(0, 10) === date &&
|
||||
v.status === "completed",
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
|
||||
|
||||
const fees = volumeStats
|
||||
.filter(
|
||||
(v) =>
|
||||
@@ -150,7 +132,6 @@ function processTimeSeriesData(
|
||||
date: date,
|
||||
feesCollected: fees,
|
||||
newUsers: newUsers,
|
||||
totalVolume: volume,
|
||||
});
|
||||
}
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { getInAppWalletUsage, getUniversalBridgeUsage } from "@/api/analytics";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import type { Range } from "@/components/analytics/date-range-selector";
|
||||
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
|
||||
import { ProjectHighlightsCard } from "./highlights-card-ui";
|
||||
import type { PageParams, PageSearchParams } from "./types";
|
||||
|
||||
export function ProjectHighlightCard(props: {
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
interval: "day" | "week";
|
||||
params: PageParams;
|
||||
range: Range;
|
||||
searchParams: PageSearchParams;
|
||||
project: Project;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
fallback={<LoadingChartState className="h-[458px] border" />}
|
||||
searchParamsUsed={["from", "to", "interval", "appHighlights"]}
|
||||
>
|
||||
<AsyncAppHighlightsCard
|
||||
authToken={props.authToken}
|
||||
client={props.client}
|
||||
interval={props.interval}
|
||||
params={props.params}
|
||||
project={props.project}
|
||||
range={props.range}
|
||||
selectedChart={
|
||||
typeof props.searchParams.appHighlights === "string"
|
||||
? props.searchParams.appHighlights
|
||||
: undefined
|
||||
}
|
||||
selectedChartQueryParam="appHighlights"
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncAppHighlightsCard(props: {
|
||||
project: Project;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
selectedChartQueryParam: string;
|
||||
selectedChart: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
params: PageParams;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [aggregatedUserStats, walletUserStatsTimeSeries, universalBridgeUsage] =
|
||||
await Promise.allSettled([
|
||||
getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getUniversalBridgeUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
if (
|
||||
walletUserStatsTimeSeries.status === "fulfilled" &&
|
||||
universalBridgeUsage.status === "fulfilled"
|
||||
) {
|
||||
return (
|
||||
<ProjectHighlightsCard
|
||||
aggregatedUserStats={
|
||||
aggregatedUserStats.status === "fulfilled"
|
||||
? aggregatedUserStats.value
|
||||
: []
|
||||
}
|
||||
selectedChart={props.selectedChart}
|
||||
selectedChartQueryParam={props.selectedChartQueryParam}
|
||||
teamSlug={props.params.team_slug}
|
||||
projectSlug={props.params.project_slug}
|
||||
userStats={
|
||||
walletUserStatsTimeSeries.status === "fulfilled"
|
||||
? walletUserStatsTimeSeries.value
|
||||
: []
|
||||
}
|
||||
volumeStats={
|
||||
universalBridgeUsage.status === "fulfilled"
|
||||
? universalBridgeUsage.value
|
||||
: []
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyStateCard
|
||||
link="https://portal.thirdweb.com/wallets"
|
||||
metric="Wallets"
|
||||
/>
|
||||
);
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getInsightStatusCodeUsage } from "@/api/analytics";
|
||||
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
|
||||
import { RequestsByStatusGraph } from "../gateway/indexer/components/RequestsByStatusGraph";
|
||||
|
||||
export function IndexerRequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
fallback={
|
||||
<RequestsByStatusGraph
|
||||
data={[]}
|
||||
isPending={true}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncIndexerRequestsChartCard
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
from={props.from}
|
||||
to={props.to}
|
||||
interval={props.interval}
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncIndexerRequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const requestsData = await getInsightStatusCodeUsage(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<RequestsByStatusGraph
|
||||
data={requestsData && "data" in requestsData ? requestsData.data : []}
|
||||
isPending={false}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
|
||||
emptyChartState={
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View more details",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No indexer requests found in selected time period"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import type { Range } from "@/components/analytics/date-range-selector";
|
||||
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
|
||||
import { TransactionsChartCardAsync } from "../components/Transactions";
|
||||
import { AIAnalyticsChartCard } from "./ai-card";
|
||||
import { AllWalletConnectionsChart } from "./all-wallet-connections-chart";
|
||||
import { BridgeChartCard } from "./bridge-card";
|
||||
import { ProjectHighlightCard } from "./highlights-card";
|
||||
import { IndexerRequestsChartCard } from "./indexer-card";
|
||||
import { RPCRequestsChartCard } from "./rpc-card";
|
||||
import type { PageParams, PageSearchParams } from "./types";
|
||||
import { X402RequestsChartCard } from "./x402-card";
|
||||
|
||||
export async function ProjectAnalytics(props: {
|
||||
project: Project;
|
||||
params: PageParams;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
searchParams: PageSearchParams;
|
||||
client: ThirdwebClient;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { project, params, range, interval, searchParams, client, authToken } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{/* highlights */}
|
||||
<ProjectHighlightCard
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
interval={interval}
|
||||
params={params}
|
||||
project={project}
|
||||
range={range}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* wallets */}
|
||||
<AllWalletConnectionsChart
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
authToken={authToken}
|
||||
projectId={project.id}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
|
||||
{/* bridge */}
|
||||
<BridgeChartCard
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Indexer */}
|
||||
<IndexerRequestsChartCard
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
{/* RPC */}
|
||||
<RPCRequestsChartCard
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* x402 */}
|
||||
<X402RequestsChartCard
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
{/* AI */}
|
||||
<AIAnalyticsChartCard
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Transactions, Chains, Contracts */}
|
||||
<ResponsiveSuspense
|
||||
fallback={<LoadingChartState className="h-[458px] border" />}
|
||||
searchParamsUsed={["from", "to", "interval", "client_transactions"]}
|
||||
>
|
||||
<TransactionsChartCardAsync
|
||||
client={client}
|
||||
params={{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
}}
|
||||
authToken={authToken}
|
||||
selectedChart={
|
||||
typeof searchParams.client_transactions === "string"
|
||||
? searchParams.client_transactions
|
||||
: undefined
|
||||
}
|
||||
selectedChartQueryParam="client_transactions"
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getRpcUsageByType } from "@/api/analytics";
|
||||
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
|
||||
import { RPCRequestsChartUI } from "../gateway/rpc/components/RequestsGraph";
|
||||
|
||||
async function AsyncRPCRequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const requestsData = await getRpcUsageByType(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
const isEmpty = requestsData?.every((d) => d.count === 0);
|
||||
|
||||
return (
|
||||
<RPCRequestsChartUI
|
||||
isPending={false}
|
||||
data={isEmpty ? [] : requestsData || []}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`}
|
||||
emptyChartState={
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View more details",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No RPC requests found in selected time period"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RPCRequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
fallback={
|
||||
<RPCRequestsChartUI
|
||||
isPending={true}
|
||||
data={[]}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncRPCRequestsChartCard
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
from={props.from}
|
||||
to={props.to}
|
||||
interval={props.interval}
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export type PageParams = {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
};
|
||||
|
||||
export type PageSearchParams = {
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
type: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
appHighlights: string | undefined | string[];
|
||||
client_transactions: string | undefined | string[];
|
||||
totalSponsored: string | undefined | string[];
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getX402Settlements } from "@/api/analytics";
|
||||
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
|
||||
import { X402RequestsChartCardUI } from "./x402-requests-chart";
|
||||
|
||||
async function AsyncX402RequestsChart(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const stats = await getX402Settlements(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
const isAllEmpty = stats.every((stat) => stat.totalRequests === 0);
|
||||
|
||||
return (
|
||||
<X402RequestsChartCardUI
|
||||
stats={
|
||||
isAllEmpty
|
||||
? []
|
||||
: stats.map((stat) => ({
|
||||
requests: stat.totalRequests,
|
||||
time: stat.date,
|
||||
}))
|
||||
}
|
||||
isPending={false}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
emptyChartState={
|
||||
<EmptyChartStateGetStartedCTA
|
||||
link={{
|
||||
label: "View Configuration",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/x402/configuration`,
|
||||
}}
|
||||
title="No data available"
|
||||
description="No X402 requests found in selected time period"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function X402RequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
fallback={
|
||||
<X402RequestsChartCardUI
|
||||
stats={[]}
|
||||
isPending={true}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AsyncX402RequestsChart
|
||||
from={props.from}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
to={props.to}
|
||||
interval={props.interval}
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
);
|
||||
}
|
||||
+2
@@ -10,6 +10,7 @@ export function X402RequestsChartCardUI(props: {
|
||||
isPending: boolean;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
emptyChartState?: React.ReactElement | undefined;
|
||||
}) {
|
||||
const total = useMemo(() => {
|
||||
return props.stats.reduce((acc, curr) => acc + curr.requests, 0);
|
||||
@@ -41,6 +42,7 @@ export function X402RequestsChartCardUI(props: {
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/x402`}
|
||||
/>
|
||||
}
|
||||
emptyChartState={props.emptyChartState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+3
-2
@@ -20,6 +20,7 @@ export function AiTokenUsageChartCardUI(props: {
|
||||
isPending: boolean;
|
||||
title: string;
|
||||
viewMoreLink: string | undefined;
|
||||
emptyChartState?: React.ReactElement | undefined;
|
||||
}) {
|
||||
const { aiUsageStats } = props;
|
||||
|
||||
@@ -93,7 +94,7 @@ export function AiTokenUsageChartCardUI(props: {
|
||||
)
|
||||
}
|
||||
data={chartData}
|
||||
emptyChartState={<AiTokenUsageEmptyChartState />}
|
||||
emptyChartState={props.emptyChartState}
|
||||
hideLabel={false}
|
||||
isPending={props.isPending}
|
||||
showLegend={false}
|
||||
@@ -109,7 +110,7 @@ export function AiTokenUsageChartCardUI(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function AiTokenUsageEmptyChartState() {
|
||||
function _AiTokenUsageEmptyChartState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<h3 className="text-base font-semibold text-foreground mb-1 text-center">
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export function QuickStartSection(props: {
|
||||
id="webhooks"
|
||||
features={["Instant events", "Transaction verification"]}
|
||||
link={{
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/webhooks/payments`,
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/webhooks`,
|
||||
label: "Setup Webhooks",
|
||||
}}
|
||||
/>
|
||||
|
||||
+5
-8
@@ -1,4 +1,3 @@
|
||||
import { WebhookIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
@@ -43,13 +42,7 @@ export default async function Layout(props: {
|
||||
instantly
|
||||
</>
|
||||
),
|
||||
actions: {
|
||||
secondary: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
|
||||
label: "Webhooks",
|
||||
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
|
||||
},
|
||||
},
|
||||
actions: null,
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
@@ -75,6 +68,10 @@ export default async function Layout(props: {
|
||||
name: "Configuration",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/bridge/configuration`,
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/bridge/webhooks`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export default async function Page(props: {
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link
|
||||
href={`/team/${params.team_slug}/${params.project_slug}/webhooks/payments`}
|
||||
href={`/team/${params.team_slug}/${params.project_slug}/bridge/webhooks`}
|
||||
>
|
||||
Webhooks
|
||||
</Link>
|
||||
+10
-4
@@ -12,6 +12,7 @@ import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
WebhookIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type PropsWithChildren, useMemo, useState } from "react";
|
||||
@@ -95,9 +96,14 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
|
||||
if (!webhooksQuery.data?.length) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-6 rounded-lg border border-border bg-card p-8 text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="font-semibold text-xl">No webhooks configured</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="rounded-full p-2 border bg-background inline-flex mb-3">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="font-semibold text-base mb-1">
|
||||
No webhooks configured
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Create a webhook to receive notifications for bridge, swap or onramp
|
||||
events.
|
||||
</p>
|
||||
@@ -126,7 +132,7 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
|
||||
webhook={webhook}
|
||||
clientId={props.clientId}
|
||||
teamId={props.teamId}
|
||||
layoutPath={`/team/${props.teamSlug}/${props.projectSlug}/webhooks/payments`}
|
||||
layoutPath={`/team/${props.teamSlug}/${props.projectSlug}/bridge/webhooks`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { PayWebhooksPage } from "../../payments/webhooks/components/webhooks.client";
|
||||
import { PayWebhooksPage } from "./components/webhooks.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
-9
@@ -6,7 +6,6 @@ import {
|
||||
DoorOpenIcon,
|
||||
HomeIcon,
|
||||
Settings2Icon,
|
||||
WebhookIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
FullWidthSidebarLayout,
|
||||
@@ -136,14 +135,6 @@ export function ProjectSidebarLayout(props: {
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/webhooks/contracts`,
|
||||
icon: WebhookIcon,
|
||||
isActive: (pathname) => {
|
||||
return pathname.startsWith(`${props.layoutPath}/webhooks`);
|
||||
},
|
||||
label: "Webhooks",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/settings`,
|
||||
icon: Settings2Icon,
|
||||
|
||||
+227
-1
@@ -3,6 +3,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowLeftRightIcon,
|
||||
EllipsisVerticalIcon,
|
||||
RefreshCcwIcon,
|
||||
@@ -10,17 +11,22 @@ import {
|
||||
ShuffleIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createThirdwebClient,
|
||||
Engine,
|
||||
getContract,
|
||||
readContract,
|
||||
type ThirdwebClient,
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { useWalletBalance } from "thirdweb/react";
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import { SwapWidget, useWalletBalance } from "thirdweb/react";
|
||||
import { isAddress, shortenAddress } from "thirdweb/utils";
|
||||
import { createWalletAdapter } from "thirdweb/wallets";
|
||||
import { z } from "zod";
|
||||
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
@@ -69,6 +75,7 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
|
||||
|
||||
type GetProjectServerWallets = (params: {
|
||||
@@ -127,6 +134,11 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
|
||||
const [isSendOpen, setIsSendOpen] = useState(false);
|
||||
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
|
||||
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
|
||||
const [isSwapOpen, setIsSwapOpen] = useState(false);
|
||||
|
||||
// Persist swap credentials in memory so users don't have to re-enter them
|
||||
const [swapSecretKey, setSwapSecretKey] = useState("");
|
||||
const [swapVaultAccessToken, setSwapVaultAccessToken] = useState("");
|
||||
|
||||
// Initialize chain and token from localStorage or defaults
|
||||
const [selectedChainId, setSelectedChainId] = useState(() => {
|
||||
@@ -310,6 +322,15 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
|
||||
<SendIcon className="size-4" />
|
||||
Withdraw
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 bg-background hover:bg-accent/50"
|
||||
onClick={() => setIsSwapOpen(true)}
|
||||
>
|
||||
<ArrowLeftRightIcon className="size-4" />
|
||||
Swap
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -394,6 +415,25 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog onOpenChange={setIsSwapOpen} open={isSwapOpen}>
|
||||
<DialogContent className="gap-0 p-0 overflow-hidden max-w-md">
|
||||
<SwapProjectWalletModalContent
|
||||
client={props.client}
|
||||
chainId={selectedChainId}
|
||||
tokenAddress={selectedTokenAddress}
|
||||
walletAddress={projectWallet.address}
|
||||
chain={chain}
|
||||
isManagedVault={isManagedVault}
|
||||
publishableKey={project.publishableKey}
|
||||
secretKey={swapSecretKey}
|
||||
setSecretKey={setSwapSecretKey}
|
||||
vaultAccessToken={swapVaultAccessToken}
|
||||
setVaultAccessToken={setSwapVaultAccessToken}
|
||||
onClose={() => setIsSwapOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -573,6 +613,192 @@ function ChangeProjectWalletDialogContent(props: {
|
||||
);
|
||||
}
|
||||
|
||||
type SwapProjectWalletModalContentProps = {
|
||||
client: ThirdwebClient;
|
||||
chainId: number;
|
||||
tokenAddress: string | undefined;
|
||||
walletAddress: string;
|
||||
chain: Chain;
|
||||
isManagedVault: boolean;
|
||||
publishableKey: string;
|
||||
secretKey: string;
|
||||
setSecretKey: (value: string) => void;
|
||||
vaultAccessToken: string;
|
||||
setVaultAccessToken: (value: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function SwapProjectWalletModalContent(
|
||||
props: SwapProjectWalletModalContentProps,
|
||||
) {
|
||||
const {
|
||||
client,
|
||||
chainId,
|
||||
tokenAddress,
|
||||
walletAddress,
|
||||
chain,
|
||||
isManagedVault,
|
||||
publishableKey,
|
||||
secretKey,
|
||||
setSecretKey,
|
||||
vaultAccessToken,
|
||||
setVaultAccessToken,
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const [screen, setScreen] = useState<"credentials" | "swap">("credentials");
|
||||
const { theme } = useTheme();
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
|
||||
const hasRequiredCredentials = isManagedVault
|
||||
? secretKey.trim().length > 0
|
||||
: secretKey.trim().length > 0 && vaultAccessToken.trim().length > 0;
|
||||
|
||||
const swapClient = useMemo(() => {
|
||||
if (!secretKey.trim()) {
|
||||
return null;
|
||||
}
|
||||
// use the inputted secret key to create the client used for the serverWallet
|
||||
return createThirdwebClient({
|
||||
clientId: publishableKey,
|
||||
secretKey: secretKey.trim(),
|
||||
});
|
||||
}, [secretKey, publishableKey]);
|
||||
|
||||
const activeWallet = useMemo(() => {
|
||||
if (!swapClient) {
|
||||
return undefined;
|
||||
}
|
||||
const vaultAccessTokenValue = vaultAccessToken.trim();
|
||||
return createWalletAdapter({
|
||||
adaptedAccount: Engine.serverWallet({
|
||||
client: swapClient,
|
||||
address: walletAddress,
|
||||
...(vaultAccessTokenValue
|
||||
? { vaultAccessToken: vaultAccessTokenValue }
|
||||
: {}),
|
||||
}),
|
||||
chain: chain,
|
||||
client: swapClient,
|
||||
onDisconnect: () => {},
|
||||
switchChain: () => {},
|
||||
});
|
||||
}, [swapClient, walletAddress, chain, vaultAccessToken]);
|
||||
|
||||
// Screen 1: Credentials
|
||||
if (screen === "credentials") {
|
||||
return (
|
||||
<div>
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<DialogTitle>Swap Tokens</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter your credentials to swap tokens from your project wallet
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-4 pb-4 lg:px-6 lg:pb-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="swap-secret-key"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
Project secret key
|
||||
</label>
|
||||
<Input
|
||||
id="swap-secret-key"
|
||||
type="password"
|
||||
placeholder="Enter your project secret key"
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{secretKeyHelper}</p>
|
||||
</div>
|
||||
|
||||
{!isManagedVault && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="swap-vault-access-token"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
Vault access token
|
||||
</label>
|
||||
<Input
|
||||
id="swap-vault-access-token"
|
||||
type="password"
|
||||
placeholder="Enter a vault access token"
|
||||
value={vaultAccessToken}
|
||||
onChange={(e) => setVaultAccessToken(e.target.value)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{vaultAccessTokenHelper}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-4">
|
||||
<Button onClick={onClose} type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setScreen("swap")}
|
||||
type="button"
|
||||
disabled={!hasRequiredCredentials}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Screen 2: Swap Widget
|
||||
return (
|
||||
<div className="w-full">
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1 h-auto w-auto"
|
||||
onClick={() => setScreen("credentials")}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<DialogTitle>Swap Tokens</DialogTitle>
|
||||
<DialogDescription>
|
||||
Swap tokens from your project wallet
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-4 pb-4 lg:px-6 lg:pb-6 flex justify-center">
|
||||
{activeWallet && (
|
||||
<SwapWidget
|
||||
client={props.client}
|
||||
prefill={{
|
||||
sellToken: {
|
||||
chainId: chainId,
|
||||
tokenAddress: tokenAddress,
|
||||
},
|
||||
}}
|
||||
activeWallet={activeWallet}
|
||||
theme={getSDKTheme(t)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const createSendFormSchema = (secretKeyLabel: string) =>
|
||||
z.object({
|
||||
chainId: z.number({
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ export default async function Page(props: {
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://api.thirdweb.com/reference#tag/contracts",
|
||||
href: "https://api.thirdweb.com/reference#tag/gateway",
|
||||
},
|
||||
{
|
||||
type: "webhooks",
|
||||
|
||||
+2
-2
@@ -3,7 +3,6 @@ import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { shortenLargeNumber } from "thirdweb/utils";
|
||||
import type { InsightStatusCodeStats } from "@/api/analytics";
|
||||
import { EmptyChartState } from "@/components/analytics/empty-chart-state";
|
||||
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
|
||||
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
@@ -17,6 +16,7 @@ export function RequestsByStatusGraph(props: {
|
||||
data: InsightStatusCodeStats[];
|
||||
isPending: boolean;
|
||||
viewMoreLink: string | undefined;
|
||||
emptyChartState?: React.ReactElement | undefined;
|
||||
}) {
|
||||
const topStatusCodesToShow = 10;
|
||||
|
||||
@@ -105,7 +105,7 @@ export function RequestsByStatusGraph(props: {
|
||||
/>
|
||||
}
|
||||
data={chartData}
|
||||
emptyChartState={<EmptyChartState type="bar" />}
|
||||
emptyChartState={props.emptyChartState}
|
||||
hideLabel={false}
|
||||
isPending={props.isPending}
|
||||
showLegend
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ export function RPCRequestsChartUI(props: {
|
||||
data: RpcUsageTypeStats[];
|
||||
viewMoreLink: string | undefined;
|
||||
isPending: boolean;
|
||||
emptyChartState?: React.ReactElement | undefined;
|
||||
}) {
|
||||
const total = useMemo(() => {
|
||||
return props.data.reduce((acc, curr) => acc + curr.count, 0);
|
||||
@@ -73,6 +74,7 @@ export function RPCRequestsChartUI(props: {
|
||||
xAxis={{
|
||||
showHour: showBreakdownByHour,
|
||||
}}
|
||||
emptyChartState={props.emptyChartState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,9 @@
|
||||
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
ResponsiveSearchParamsProvider,
|
||||
ResponsiveSuspense,
|
||||
} from "responsive-rsc";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
getAiUsage,
|
||||
getInAppWalletUsage,
|
||||
getInsightStatusCodeUsage,
|
||||
getRpcUsageByType,
|
||||
getUniversalBridgeUsage,
|
||||
getX402Settlements,
|
||||
isProjectActive,
|
||||
} from "@/api/analytics";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
import { isProjectActive } from "@/api/analytics";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject, type Project } from "@/api/project/projects";
|
||||
import type {
|
||||
DurationId,
|
||||
Range,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import type { DurationId } from "@/components/analytics/date-range-selector";
|
||||
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
|
||||
import { ProjectAvatar } from "@/components/blocks/avatar/project-avatar";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
@@ -28,39 +11,16 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getProjectWallet } from "@/lib/server/project-wallet";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { AiTokenUsageChartCardUI } from "./ai/analytics/chart/AiTokenUsageChartCard";
|
||||
import { ProjectAnalytics } from "./_analytics/project-analytics";
|
||||
import type { PageParams, PageSearchParams } from "./_analytics/types";
|
||||
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
|
||||
import { ProjectWalletSection } from "./components/project-wallet/project-wallet";
|
||||
import { TransactionsChartCardAsync } from "./components/Transactions";
|
||||
import { RequestsByStatusGraph } from "./gateway/indexer/components/RequestsByStatusGraph";
|
||||
import { RPCRequestsChartUI } from "./gateway/rpc/components/RequestsGraph";
|
||||
import { ProjectHighlightsCard } from "./overview/highlights-card";
|
||||
import { AllWalletConnectionsChart } from "./wallets/analytics/chart/all-wallet-connections-chart";
|
||||
import { X402RequestsChartCardUI } from "./x402/analytics/x402-requests-chart";
|
||||
|
||||
type PageParams = {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
};
|
||||
|
||||
type PageSearchParams = {
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
type: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
appHighlights: string | undefined | string[];
|
||||
client_transactions: string | undefined | string[];
|
||||
totalSponsored: string | undefined | string[];
|
||||
};
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<PageParams>;
|
||||
searchParams: Promise<PageSearchParams>;
|
||||
};
|
||||
|
||||
// Revalidate this page data every 30 seconds
|
||||
export const revalidate = 30;
|
||||
|
||||
export default async function ProjectOverviewPage(props: PageProps) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
@@ -104,25 +64,25 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
const projectWallet = await getProjectWallet(project);
|
||||
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
isProjectIcon: true,
|
||||
icon: () => (
|
||||
<ProjectAvatar
|
||||
className="size-11"
|
||||
client={client}
|
||||
src={project.image ?? ""}
|
||||
/>
|
||||
),
|
||||
title: project.name,
|
||||
description: "Your project's overview and analytics",
|
||||
// explicitly NO actions on the overview page
|
||||
actions: null,
|
||||
}}
|
||||
>
|
||||
{isActive ? (
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
isProjectIcon: true,
|
||||
icon: () => (
|
||||
<ProjectAvatar
|
||||
className="size-11"
|
||||
client={client}
|
||||
src={project.image ?? ""}
|
||||
/>
|
||||
),
|
||||
title: project.name,
|
||||
description: "Your project's overview and analytics",
|
||||
// explicitly NO actions on the overview page
|
||||
actions: null,
|
||||
}}
|
||||
>
|
||||
{isActive ? (
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<div className="flex flex-col gap-4 md:gap-10">
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
@@ -144,413 +104,24 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-6">
|
||||
<ProjectFTUX
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
projectWalletSection={
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
projectWallet={projectWallet}
|
||||
client={client}
|
||||
layout="row"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPage>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function ProjectAnalytics(props: {
|
||||
project: Project;
|
||||
params: PageParams;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
searchParams: PageSearchParams;
|
||||
client: ThirdwebClient;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { project, params, range, interval, searchParams, client, authToken } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{/* project highlights */}
|
||||
<ResponsiveSuspense
|
||||
fallback={<LoadingChartState className="h-[458px] border" />}
|
||||
searchParamsUsed={["from", "to", "interval", "appHighlights"]}
|
||||
>
|
||||
<AsyncAppHighlightsCard
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
interval={interval}
|
||||
params={params}
|
||||
project={project}
|
||||
range={range}
|
||||
selectedChart={
|
||||
typeof searchParams.appHighlights === "string"
|
||||
? searchParams.appHighlights
|
||||
: undefined
|
||||
}
|
||||
selectedChartQueryParam="appHighlights"
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
{/* wallet connections */}
|
||||
<AllWalletConnectionsChart
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
authToken={authToken}
|
||||
projectId={project.id}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
|
||||
{/* indexer and RPC requests */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<ResponsiveSuspense
|
||||
fallback={
|
||||
<RequestsByStatusGraph
|
||||
data={[]}
|
||||
isPending={true}
|
||||
viewMoreLink={`/team/${params.team_slug}/${params.project_slug}/gateway/indexer`}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncIndexerRequestsChartCard
|
||||
</ResponsiveSearchParamsProvider>
|
||||
) : (
|
||||
<div className="pt-6">
|
||||
<ProjectFTUX
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
projectWalletSection={
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
projectWallet={projectWallet}
|
||||
client={client}
|
||||
layout="row"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
<ResponsiveSuspense
|
||||
fallback={
|
||||
<RPCRequestsChartUI
|
||||
isPending={true}
|
||||
data={[]}
|
||||
viewMoreLink={`/team/${params.team_slug}/${params.project_slug}/gateway/rpc`}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncRPCRequestsChartCard
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* x402 */}
|
||||
<ResponsiveSuspense
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
fallback={
|
||||
<X402RequestsChartCardUI
|
||||
stats={[]}
|
||||
isPending={true}
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AsyncX402RequestsChart
|
||||
from={range.from}
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
|
||||
{/* AI tokens */}
|
||||
<ResponsiveSuspense
|
||||
fallback={
|
||||
<AiTokenUsageChartCardUI
|
||||
isPending={true}
|
||||
title="AI token volume"
|
||||
viewMoreLink={`/team/${params.team_slug}/${params.project_slug}/ai/analytics`}
|
||||
aiUsageStats={[]}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncAiAnalytics
|
||||
teamSlug={params.team_slug}
|
||||
projectSlug={params.project_slug}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
interval={interval}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
|
||||
{/* transactions */}
|
||||
<ResponsiveSuspense
|
||||
fallback={<LoadingChartState className="h-[458px] border" />}
|
||||
searchParamsUsed={["from", "to", "interval", "client_transactions"]}
|
||||
>
|
||||
<TransactionsChartCardAsync
|
||||
client={client}
|
||||
params={{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
}}
|
||||
authToken={authToken}
|
||||
selectedChart={
|
||||
typeof searchParams.client_transactions === "string"
|
||||
? searchParams.client_transactions
|
||||
: undefined
|
||||
}
|
||||
selectedChartQueryParam="client_transactions"
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncIndexerRequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const requestsData = await getInsightStatusCodeUsage(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<RequestsByStatusGraph
|
||||
data={requestsData && "data" in requestsData ? requestsData.data : []}
|
||||
isPending={false}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncRPCRequestsChartCard(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const requestsData = await getRpcUsageByType(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<RPCRequestsChartUI
|
||||
isPending={false}
|
||||
data={requestsData || []}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncAppHighlightsCard(props: {
|
||||
project: Project;
|
||||
range: Range;
|
||||
interval: "day" | "week";
|
||||
selectedChartQueryParam: string;
|
||||
selectedChart: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
params: PageParams;
|
||||
authToken: string;
|
||||
}) {
|
||||
const [aggregatedUserStats, walletUserStatsTimeSeries, universalBridgeUsage] =
|
||||
await Promise.allSettled([
|
||||
getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: "all",
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getInAppWalletUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
getUniversalBridgeUsage(
|
||||
{
|
||||
from: props.range.from,
|
||||
period: props.interval,
|
||||
projectId: props.project.id,
|
||||
teamId: props.project.teamId,
|
||||
to: props.range.to,
|
||||
},
|
||||
props.authToken,
|
||||
),
|
||||
]);
|
||||
|
||||
if (
|
||||
walletUserStatsTimeSeries.status === "fulfilled" &&
|
||||
universalBridgeUsage.status === "fulfilled"
|
||||
) {
|
||||
return (
|
||||
<ProjectHighlightsCard
|
||||
aggregatedUserStats={
|
||||
aggregatedUserStats.status === "fulfilled"
|
||||
? aggregatedUserStats.value
|
||||
: []
|
||||
}
|
||||
selectedChart={props.selectedChart}
|
||||
selectedChartQueryParam={props.selectedChartQueryParam}
|
||||
teamSlug={props.params.team_slug}
|
||||
projectSlug={props.params.project_slug}
|
||||
userStats={
|
||||
walletUserStatsTimeSeries.status === "fulfilled"
|
||||
? walletUserStatsTimeSeries.value
|
||||
: []
|
||||
}
|
||||
volumeStats={
|
||||
universalBridgeUsage.status === "fulfilled"
|
||||
? universalBridgeUsage.value
|
||||
: []
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyStateCard
|
||||
link="https://portal.thirdweb.com/wallets"
|
||||
metric="Wallets"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncAiAnalytics(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const stats = await getAiUsage(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
return (
|
||||
<AiTokenUsageChartCardUI
|
||||
title="AI token volume"
|
||||
isPending={false}
|
||||
aiUsageStats={stats}
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncX402RequestsChart(props: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
interval: "day" | "week";
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const stats = await getX402Settlements(
|
||||
{
|
||||
from: props.from,
|
||||
period: props.interval,
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
to: props.to,
|
||||
},
|
||||
props.authToken,
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
const isAllEmpty = stats.every((stat) => stat.totalRequests === 0);
|
||||
|
||||
return (
|
||||
<X402RequestsChartCardUI
|
||||
stats={
|
||||
isAllEmpty
|
||||
? []
|
||||
: stats.map((stat) => ({
|
||||
requests: stat.totalRequests,
|
||||
time: stat.date,
|
||||
}))
|
||||
}
|
||||
isPending={false}
|
||||
teamSlug={props.teamSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPage>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export function AdvancedSection({
|
||||
id="webhooks"
|
||||
features={["Instant events", "Transaction verification"]}
|
||||
link={{
|
||||
href: `/team/${teamSlug}/${projectSlug}/webhooks/payments`,
|
||||
href: `/team/${teamSlug}/${projectSlug}/bridge/webhooks`,
|
||||
label: "Setup Webhooks",
|
||||
}}
|
||||
/>
|
||||
|
||||
-15
@@ -67,24 +67,9 @@ export async function PayAnalytics(props: {
|
||||
walletDataPromise,
|
||||
]);
|
||||
|
||||
const hasVolume = volumeData.some((d) => d.amountUsdCents > 0);
|
||||
const hasWallet = walletData.some((d) => d.count > 0);
|
||||
|
||||
if (!hasVolume && !hasWallet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold tracking-tight mb-1">
|
||||
Analytics
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm">
|
||||
Track Bridge volume, customers, payouts, and success rates.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4 flex justify-start">
|
||||
<PayAnalyticsFilter />
|
||||
</div>
|
||||
|
||||
+52
-45
@@ -12,7 +12,10 @@ type VolData = {
|
||||
color: string;
|
||||
};
|
||||
|
||||
export function TotalVolumePieChart(props: { data: UniversalBridgeStats[] }) {
|
||||
export function TotalVolumePieChart(props: {
|
||||
data: UniversalBridgeStats[];
|
||||
hideTotal?: boolean;
|
||||
}) {
|
||||
const data = props.data;
|
||||
const isEmpty =
|
||||
data.length === 0 || data.every((x) => x.amountUsdCents === 0);
|
||||
@@ -36,20 +39,20 @@ export function TotalVolumePieChart(props: { data: UniversalBridgeStats[] }) {
|
||||
.filter((x) => x.type === "onramp")
|
||||
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
|
||||
|
||||
const volumeData: VolData[] = !isEmpty
|
||||
? [
|
||||
{
|
||||
amount: cryptoTotalUSD,
|
||||
color: "hsl(var(--chart-1))",
|
||||
name: "Crypto",
|
||||
},
|
||||
{
|
||||
amount: fiatTotalUSD,
|
||||
color: "hsl(var(--chart-2))",
|
||||
name: "Fiat",
|
||||
},
|
||||
]
|
||||
: skeletonData;
|
||||
const volumeData: VolData[] = [
|
||||
{
|
||||
amount: cryptoTotalUSD,
|
||||
color: "hsl(var(--chart-1))",
|
||||
name: "Crypto",
|
||||
},
|
||||
{
|
||||
amount: fiatTotalUSD,
|
||||
color: "hsl(var(--chart-2))",
|
||||
name: "Fiat",
|
||||
},
|
||||
];
|
||||
|
||||
const chartVolumeData = !isEmpty ? volumeData : skeletonData;
|
||||
|
||||
return (
|
||||
<section className="w-full">
|
||||
@@ -62,7 +65,7 @@ export function TotalVolumePieChart(props: { data: UniversalBridgeStats[] }) {
|
||||
cornerRadius={100}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={volumeData}
|
||||
data={chartVolumeData}
|
||||
dataKey="amount"
|
||||
innerRadius="80%"
|
||||
outerRadius="100%"
|
||||
@@ -72,41 +75,45 @@ export function TotalVolumePieChart(props: { data: UniversalBridgeStats[] }) {
|
||||
outline: "none",
|
||||
}}
|
||||
>
|
||||
{volumeData.map((entry, index) => (
|
||||
{chartVolumeData.map((entry, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: ok
|
||||
<Cell fill={entry.color} key={index} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center p-10">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<p className="font-medium text-sm"> Total Volume</p>
|
||||
{!props.hideTotal && (
|
||||
<div className="absolute inset-0 flex items-center justify-center p-10">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<p className="font-medium text-sm text-muted-foreground">
|
||||
Total Volume
|
||||
</p>
|
||||
|
||||
<SkeletonContainer
|
||||
loadedData={
|
||||
!data
|
||||
? undefined
|
||||
: data.length > 0
|
||||
? toUSD(cryptoTotalUSD + fiatTotalUSD)
|
||||
: "NA"
|
||||
}
|
||||
render={(totalAmount) => {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-3xl tracking-tighter",
|
||||
totalAmount.length > 6 ? "text-3xl" : "text-4xl",
|
||||
)}
|
||||
>
|
||||
{totalAmount}
|
||||
</p>
|
||||
);
|
||||
}}
|
||||
skeletonData="$100"
|
||||
/>
|
||||
<SkeletonContainer
|
||||
loadedData={
|
||||
!data
|
||||
? undefined
|
||||
: data.length > 0
|
||||
? toUSD(cryptoTotalUSD + fiatTotalUSD)
|
||||
: "NA"
|
||||
}
|
||||
render={(totalAmount) => {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-2xl tracking-tighter",
|
||||
totalAmount.length > 6 ? "text-2xl" : "text-3xl",
|
||||
)}
|
||||
>
|
||||
{totalAmount}
|
||||
</p>
|
||||
);
|
||||
}}
|
||||
skeletonData="$100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Right */}
|
||||
<div className="items-center border-border border-t pt-5 lg:flex lg:border-none lg:pt-0 lg:pr-10">
|
||||
@@ -114,9 +121,9 @@ export function TotalVolumePieChart(props: { data: UniversalBridgeStats[] }) {
|
||||
{volumeData.map((v) => (
|
||||
<VolumeLegend
|
||||
amount={
|
||||
!data ? undefined : data.length > 0 ? toUSD(v.amount) : "NA"
|
||||
isEmpty ? "NA" : data.length > 0 ? toUSD(v.amount) : "NA"
|
||||
}
|
||||
color={v.color}
|
||||
color={isEmpty ? "hsl(var(--muted))" : v.color}
|
||||
key={v.name}
|
||||
label={v.name}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -66,7 +66,7 @@ export default async function Page(props: {
|
||||
),
|
||||
},
|
||||
secondary: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/bridge/webhooks`,
|
||||
label: "Webhooks",
|
||||
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
|
||||
},
|
||||
@@ -85,7 +85,7 @@ export default async function Page(props: {
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://api.thirdweb.com/reference#tag/payments",
|
||||
href: "https://api.thirdweb.com/reference#tag/x402",
|
||||
},
|
||||
],
|
||||
}}
|
||||
|
||||
+15
-49
@@ -1,16 +1,10 @@
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getFilteredProjectContracts } from "@/api/project/getSortedDeployedContracts";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { ContractTable } from "@/components/contract-components/tables/contract-table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -21,10 +15,11 @@ import {
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { TokenIcon } from "@/icons/TokenIcon";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { Cards, ImportTokenButton } from "./cards";
|
||||
import { Cards, ImportTokenButton } from "../cards";
|
||||
|
||||
export default async function Page(props: {
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
@@ -110,48 +105,19 @@ export default async function Page(props: {
|
||||
},
|
||||
],
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
name: "Overview",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/tokens`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/tokens/webhooks`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<AssetsPageAsync
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
projectId={project.id}
|
||||
projectSlug={params.project_slug}
|
||||
teamId={team.id}
|
||||
teamSlug={params.team_slug}
|
||||
/>
|
||||
</Suspense>
|
||||
{props.children}
|
||||
</ProjectPage>
|
||||
);
|
||||
}
|
||||
|
||||
async function AssetsPageAsync(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const deployedContracts = await getFilteredProjectContracts({
|
||||
authToken: props.authToken,
|
||||
type: "token-contracts",
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={<GenericLoadingPage />}>
|
||||
<ContractTable
|
||||
client={props.client}
|
||||
contracts={deployedContracts}
|
||||
pageSize={10}
|
||||
projectId={props.projectId}
|
||||
projectSlug={props.projectSlug}
|
||||
teamId={props.teamId}
|
||||
teamSlug={props.teamSlug}
|
||||
variant="asset"
|
||||
/>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getFilteredProjectContracts } from "@/api/project/getSortedDeployedContracts";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { ContractTable } from "@/components/contract-components/tables/contract-table";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const [authToken, team, project] = await Promise.all([
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
]);
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/tokens`);
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<AssetsPageAsync
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
projectId={project.id}
|
||||
projectSlug={params.project_slug}
|
||||
teamId={team.id}
|
||||
teamSlug={params.team_slug}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function AssetsPageAsync(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
client: ThirdwebClient;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const deployedContracts = await getFilteredProjectContracts({
|
||||
authToken: props.authToken,
|
||||
type: "token-contracts",
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={<GenericLoadingPage />}>
|
||||
<ContractTable
|
||||
client={props.client}
|
||||
contracts={deployedContracts}
|
||||
pageSize={10}
|
||||
projectId={props.projectId}
|
||||
projectSlug={props.projectSlug}
|
||||
teamId={props.teamId}
|
||||
teamSlug={props.teamSlug}
|
||||
variant="asset"
|
||||
/>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { WebhookFormValues } from "../utils/webhookTypes";
|
||||
import type { WebhookFormValues } from "./utils/webhookTypes";
|
||||
|
||||
interface BasicInfoStepProps {
|
||||
form: UseFormReturn<WebhookFormValues>;
|
||||
+9
-9
@@ -16,26 +16,26 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useAbiMultiFetch } from "../hooks/useAbiProcessing";
|
||||
import { useTestWebhook } from "../hooks/useTestWebhook";
|
||||
import BasicInfoStep from "./BasicInfoStep";
|
||||
import { FilterDetailsStep } from "./FilterDetailsStep";
|
||||
import ReviewStep from "./ReviewStep";
|
||||
import { useAbiMultiFetch } from "./useAbiProcessing";
|
||||
import { useTestWebhook } from "./useTestWebhook";
|
||||
import {
|
||||
extractEventSignatures,
|
||||
extractFunctionSignatures,
|
||||
} from "../utils/abiUtils";
|
||||
import type { WebhookPayload } from "../utils/webhookPayloadUtils";
|
||||
} from "./utils/abiUtils";
|
||||
import type { WebhookPayload } from "./utils/webhookPayloadUtils";
|
||||
import {
|
||||
buildEventWebhookPayload,
|
||||
buildTransactionWebhookPayload,
|
||||
} from "../utils/webhookPayloadUtils";
|
||||
} from "./utils/webhookPayloadUtils";
|
||||
import {
|
||||
type WebhookFormStep,
|
||||
WebhookFormSteps,
|
||||
type WebhookFormValues,
|
||||
webhookFormSchema,
|
||||
} from "../utils/webhookTypes";
|
||||
import BasicInfoStep from "./BasicInfoStep";
|
||||
import { FilterDetailsStep } from "./FilterDetailsStep";
|
||||
import ReviewStep from "./ReviewStep";
|
||||
} from "./utils/webhookTypes";
|
||||
|
||||
interface CreateWebhookModalProps {
|
||||
projectClientId: string;
|
||||
+2
-2
@@ -17,13 +17,13 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { truncateMiddle } from "../utils/abiUtils";
|
||||
import { truncateMiddle } from "./utils/abiUtils";
|
||||
import type {
|
||||
AbiData,
|
||||
EventSignature,
|
||||
FunctionSignature,
|
||||
WebhookFormValues,
|
||||
} from "../utils/webhookTypes";
|
||||
} from "./utils/webhookTypes";
|
||||
|
||||
interface FilterDetailsStepProps {
|
||||
form: UseFormReturn<WebhookFormValues>;
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { CheckIcon, PlayIcon, XIcon } from "lucide-react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { truncateMiddle } from "../utils/abiUtils";
|
||||
import type { WebhookFormValues } from "../utils/webhookTypes";
|
||||
import { truncateMiddle } from "./utils/abiUtils";
|
||||
import type { WebhookFormValues } from "./utils/webhookTypes";
|
||||
|
||||
interface WebhookTestResult {
|
||||
status: string;
|
||||
+1
-1
@@ -17,9 +17,9 @@ import { Button } from "@/components/ui/button";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useTestWebhook } from "../hooks/useTestWebhook";
|
||||
import { CreateContractWebhookButton } from "./CreateWebhookModal";
|
||||
import { RelativeTime } from "./RelativeTime";
|
||||
import { useTestWebhook } from "./useTestWebhook";
|
||||
|
||||
function getEventType(filters: WebhookFilters): string {
|
||||
if (!filters || typeof filters !== "object") {
|
||||
+10
-6
@@ -1,8 +1,9 @@
|
||||
import { XIcon } from "lucide-react";
|
||||
import { getWebhooks, type WebhookResponse } from "@/api/insight/webhooks";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { CreateContractWebhookButton } from "../components/CreateWebhookModal";
|
||||
import { ContractsWebhooksTable } from "../components/WebhooksTable";
|
||||
import { CreateContractWebhookButton } from "./CreateWebhookModal";
|
||||
import { ContractsWebhooksTable } from "./WebhooksTable";
|
||||
|
||||
export async function ContractsWebhooksPageContent(props: {
|
||||
project: Project;
|
||||
@@ -50,11 +51,14 @@ export async function ContractsWebhooksPageContent(props: {
|
||||
webhooks={webhooks}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-border p-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-border py-14 px-6 text-center">
|
||||
<div>
|
||||
<h3 className="mb-1 font-medium text-lg">No webhooks found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Create a webhook to get started.
|
||||
<div className="rounded-full p-2 border bg-background inline-flex mb-3">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="mb-1 font-semibold text-base">No webhooks found</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Create a webhook to get started
|
||||
</p>
|
||||
</div>
|
||||
<CreateContractWebhookButton
|
||||
+2
-2
@@ -5,12 +5,12 @@ import { useMemo } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { getContract, resolveAbiFromContractApi } from "thirdweb/contract";
|
||||
import { useGetV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { parseAddresses } from "../utils/webhookPayloadUtils";
|
||||
import { parseAddresses } from "./utils/webhookPayloadUtils";
|
||||
import type {
|
||||
AbiData,
|
||||
EventSignature,
|
||||
FunctionSignature,
|
||||
} from "../utils/webhookTypes";
|
||||
} from "./utils/webhookTypes";
|
||||
|
||||
export function useAbiMultiFetch({
|
||||
isOpen,
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getSupportedWebhookChains } from "@/api/insight/webhooks";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { ContractsWebhooksPageContent } from "../contract-webhooks/contract-webhooks-page";
|
||||
import { ContractsWebhooksPageContent } from "./components/contract-webhooks-page";
|
||||
|
||||
export default async function ContractsPage(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import { WebhookIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [authToken, params] = await Promise.all([getAuthToken(), props.params]);
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!project || !authToken) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
icon: WebhookIcon,
|
||||
title: "Webhooks",
|
||||
description: "Get notified for Bridge, Swap and Onramp events.",
|
||||
actions: null,
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/bridge/webhooks",
|
||||
},
|
||||
],
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
name: "Contracts",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
|
||||
},
|
||||
{
|
||||
name: "Bridge",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</ProjectPage>
|
||||
);
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function WebhooksPage(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
// for the moment redirect to the "/contracts webhooks"
|
||||
redirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -22,7 +22,7 @@ export function X402EmptyState(props: { walletAddress?: string }) {
|
||||
label: "View Docs",
|
||||
},
|
||||
]}
|
||||
title="Start Monetizing your API"
|
||||
title="Payment gate your API"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export async function GET(request: Request) {
|
||||
price: "$0.01",
|
||||
facilitator: thirdwebX402Facilitator,
|
||||
});
|
||||
|
||||
|
||||
if (result.status === 200) {
|
||||
// Payment successful, continue to app logic
|
||||
return Response.json({ data: "premium content" });
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
CartesianGrid,
|
||||
BarChart as RechartsBarChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { EmptyChartState } from "@/components/analytics/empty-chart-state";
|
||||
import {
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatTickerNumber } from "@/utils/format-utils";
|
||||
import { toUSD } from "@/utils/number";
|
||||
|
||||
@@ -23,6 +23,7 @@ export function BarChart({
|
||||
tooltipLabel,
|
||||
isCurrency = false,
|
||||
emptyChartContent,
|
||||
chartContentClassName,
|
||||
}: {
|
||||
chartConfig: ChartConfig;
|
||||
data: { [key in string]: number | string }[];
|
||||
@@ -30,10 +31,11 @@ export function BarChart({
|
||||
tooltipLabel?: string;
|
||||
isCurrency?: boolean;
|
||||
emptyChartContent?: React.ReactNode;
|
||||
chartContentClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<ChartContainer
|
||||
className="aspect-auto h-[250px] w-full pt-6"
|
||||
className={cn("aspect-auto h-[275px] w-full pt-6", chartContentClassName)}
|
||||
config={{
|
||||
[activeKey]: {
|
||||
label: tooltipLabel ?? chartConfig[activeKey]?.label,
|
||||
@@ -42,16 +44,11 @@ export function BarChart({
|
||||
}}
|
||||
>
|
||||
{data.length === 0 || data.every((d) => d[activeKey] === 0) ? (
|
||||
<EmptyChartState type="bar"> {emptyChartContent} </EmptyChartState>
|
||||
<div className="h-full">
|
||||
<EmptyChartState content={emptyChartContent} />
|
||||
</div>
|
||||
) : (
|
||||
<RechartsBarChart
|
||||
accessibilityLayer
|
||||
data={data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<RechartsBarChart accessibilityLayer data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
@@ -67,13 +64,7 @@ export function BarChart({
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
dataKey={activeKey}
|
||||
tickFormatter={(value: number) => formatTickerNumber(value)}
|
||||
tickLine={false}
|
||||
width={48}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
|
||||
@@ -83,7 +83,7 @@ export function CombinedBarChartCard<
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6 sm:pl-0">
|
||||
<CardContent className="">
|
||||
<BarChart
|
||||
activeKey={activeChart}
|
||||
chartConfig={chartConfig}
|
||||
|
||||
@@ -8,13 +8,16 @@ import {
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { formatTickerNumber } from "@/utils/format-utils";
|
||||
import { toUSD } from "@/utils/number";
|
||||
|
||||
export function PieChart({
|
||||
title,
|
||||
data,
|
||||
isCurrency = false,
|
||||
}: {
|
||||
title: string;
|
||||
data: { value: number; label: string; fill?: string }[];
|
||||
isCurrency?: boolean;
|
||||
}) {
|
||||
const chartConfig: ChartConfig = Object.fromEntries(
|
||||
Object.entries(data).map(([name, value]) => [
|
||||
@@ -42,7 +45,11 @@ export function PieChart({
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel
|
||||
valueFormatter={(v: unknown) => formatTickerNumber(v as number)}
|
||||
valueFormatter={(v: unknown) =>
|
||||
isCurrency
|
||||
? toUSD(v as number)
|
||||
: formatTickerNumber(v as number)
|
||||
}
|
||||
/>
|
||||
}
|
||||
cursor={false}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "@/components/analytics/empty-chart-state";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -18,10 +23,18 @@ export function PieChartCard({
|
||||
title,
|
||||
data,
|
||||
aggregateFn = (data) => data.reduce((acc, curr) => acc + curr.value, 0),
|
||||
isCurrency = false,
|
||||
customHeader,
|
||||
isPending = false,
|
||||
emptyChartState,
|
||||
}: {
|
||||
title: string;
|
||||
data: ChartData[];
|
||||
aggregateFn?: (data: ChartData[]) => number;
|
||||
isCurrency?: boolean;
|
||||
customHeader?: React.ReactNode;
|
||||
isPending?: boolean;
|
||||
emptyChartState?: React.ReactNode;
|
||||
}) {
|
||||
const processedData = (() => {
|
||||
// Sort by value descending
|
||||
@@ -53,48 +66,69 @@ export function PieChartCard({
|
||||
fill: item.fill || `hsl(var(--chart-${index + 1}))`,
|
||||
}));
|
||||
|
||||
const isAllEmpty = useMemo(() => {
|
||||
return data.every((item) => item.value === 0);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="border-border border-b p-0">
|
||||
<Stat label={title} value={aggregateFn(data)} />
|
||||
</CardHeader>
|
||||
{customHeader || (
|
||||
<CardHeader className="border-border border-b p-0">
|
||||
<Stat label={title} value={aggregateFn(data)} />
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className="flex-1 p-4">
|
||||
<PieChart data={processedData} title={title} />
|
||||
{isPending ? (
|
||||
<LoadingChartState />
|
||||
) : isAllEmpty ? (
|
||||
<EmptyChartState content={emptyChartState} />
|
||||
) : (
|
||||
<PieChart
|
||||
data={processedData}
|
||||
title={title}
|
||||
isCurrency={isCurrency}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
<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,
|
||||
link,
|
||||
}: {
|
||||
label: string;
|
||||
fill?: string;
|
||||
link?: string;
|
||||
}) => (
|
||||
<div className="flex items-center gap-2" key={fill}>
|
||||
<div
|
||||
className="size-2 rounded-full"
|
||||
style={{ background: fill }}
|
||||
/>
|
||||
{link ? (
|
||||
<Link
|
||||
className="text-muted-foreground text-xs hover:text-link-foreground"
|
||||
href={link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
{!isAllEmpty && (
|
||||
<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,
|
||||
link,
|
||||
}: {
|
||||
label: string;
|
||||
fill?: string;
|
||||
link?: string;
|
||||
}) => (
|
||||
<div className="flex items-center gap-2" key={fill}>
|
||||
<div
|
||||
className="size-2 rounded-full"
|
||||
style={{ background: fill }}
|
||||
/>
|
||||
{link ? (
|
||||
<Link
|
||||
className="text-muted-foreground text-xs hover:text-link-foreground"
|
||||
href={link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
+83
-83
@@ -1,85 +1,85 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"fuse.js": "7.1.0",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
"@storybook/nextjs": "9.0.15",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-storybook": "9.0.15",
|
||||
"knip": "5.60.2",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.6",
|
||||
"storybook": "9.0.15",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"name": "nebula-app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
|
||||
"build-storybook": "storybook build",
|
||||
"build:analyze": "ANALYZE=true pnpm run build",
|
||||
"dev": "next dev --turbopack",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "0.0.2"
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"fuse.js": "7.1.0",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.2.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.2.1",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/eslint-plugin-next": "15.3.6",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
"@storybook/nextjs": "9.0.15",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-storybook": "9.0.15",
|
||||
"knip": "5.60.2",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.6",
|
||||
"storybook": "9.0.15",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"name": "nebula-app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
|
||||
"build-storybook": "storybook build",
|
||||
"build:analyze": "ANALYZE=true pnpm run build",
|
||||
"dev": "next dev --turbopack",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "0.0.2"
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@abstract-foundation/agw-react": "^1.10.0",
|
||||
"@ai-sdk/react": "^2.0.25",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@scalar/api-reference-react": "^0.7.42",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@thirdweb-dev/ai-sdk-provider": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"ai": "^5.0.25",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"geist": "^1.5.1",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "12.1.3",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-pick-color": "^2.0.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"streamdown": "^1.1.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"thirdweb": "workspace:*",
|
||||
"use-debounce": "^10.0.5",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-config-next": "15.3.5",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"knip": "5.60.2",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"name": "playground-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "rm -rf .next && next dev --turbopack",
|
||||
"fix": "eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"prefix": "biome check ./src --fix",
|
||||
"prelint": "biome check ./src",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "0.1.1"
|
||||
"dependencies": {
|
||||
"@abstract-foundation/agw-react": "^1.10.0",
|
||||
"@ai-sdk/react": "^2.0.25",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@scalar/api-reference-react": "^0.7.42",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@thirdweb-dev/ai-sdk-provider": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"ai": "^5.0.25",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"geist": "^1.5.1",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "12.1.3",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.2.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.2.1",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-pick-color": "^2.0.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"streamdown": "^1.1.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"thirdweb": "workspace:*",
|
||||
"use-debounce": "^10.0.5",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-config-next": "15.3.6",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"knip": "5.60.2",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"name": "playground-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "rm -rf .next && next dev --turbopack",
|
||||
"fix": "eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"prefix": "biome check ./src --fix",
|
||||
"prelint": "biome check ./src",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "0.1.1"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"@dirtycajunrice/klee": "^1.0.6",
|
||||
"@mdx-js/loader": "3.1.0",
|
||||
"@mdx-js/react": "3.1.0",
|
||||
"@next/mdx": "15.3.5",
|
||||
"@next/mdx": "15.3.6",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
@@ -21,7 +21,7 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next": "15.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
@@ -29,9 +29,9 @@
|
||||
"openapi-types": "12.1.3",
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react": "19.2.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dom": "19.2.1",
|
||||
"react-html-parser": "2.0.2",
|
||||
"react-markdown": "10.1.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
@@ -46,13 +46,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.6",
|
||||
"@types/flexsearch": "^0.7.42",
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-html-parser": "^2.0.7",
|
||||
"@types/tryghost__content-api": "^1.3.16",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
|
||||
@@ -71,7 +71,7 @@ export const sidebar: SideBar = {
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/reference#tag/contracts",
|
||||
href: "/reference#tag/gateway",
|
||||
icon: <CodeIcon />,
|
||||
name: "HTTP API",
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ Server wallets are wallets that are managed by your own application, like a trea
|
||||
|
||||
### Use an existing Server Wallet
|
||||
|
||||
Once created, you can use your server wallet by passing it as the `from` field of the [thirdweb API](/reference#tag/transactions/post/v1/transactions).
|
||||
Once created, you can use your server wallet by passing it as the `from` field of the [thirdweb API](/reference#tag/wallets/post/v1/transactions).
|
||||
|
||||
### Create a new Server Wallet
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Send, monitor, and manage transactions. Send transactions from user or server wa
|
||||
|
||||
### Send raw transactions
|
||||
|
||||
Send a raw transaction from a [user wallet](/wallets/users) from the frontend, or [server wallet](/wallets/server) from the backend using the [thirdweb API](/reference#tag/transactions/post/v1/transactions).
|
||||
Send a raw transaction from a [user wallet](/wallets/users) from the frontend, or [server wallet](/wallets/server) from the backend using the [thirdweb API](/reference#tag/wallets/post/v1/transactions).
|
||||
|
||||
- For server wallets, you can execute transactions with just your project secret key.
|
||||
- For user wallets in React applications that use the SDK, you can obtain the user wallet auth token (JWT) with the [`useAuthToken()`](/references/typescript/v5/useAuthToken) hook.
|
||||
@@ -74,7 +74,7 @@ Send, monitor, and manage transactions. Send transactions from user or server wa
|
||||
|
||||
### Send contract calls
|
||||
|
||||
Send a transaction to a contract from a [user wallet](/wallets/users) from the frontend, or [server wallet](/wallets/server) from the backend using the [thirdweb API](/reference#tag/contracts/post/v1/contracts/{chainId}/{address}/write).
|
||||
Send a transaction to a contract from a [user wallet](/wallets/users) from the frontend, or [server wallet](/wallets/server) from the backend using the [thirdweb API](/reference#tag/wallets/post/v1/contracts/{chainId}/{address}/write).
|
||||
|
||||
- For server wallets, you can execute transactions with just your project secret key.
|
||||
- For user wallets in React applications that use the SDK, you can obtain the user wallet auth token (JWT) with the [`useAuthToken()`](/references/typescript/v5/useAuthToken) hook.
|
||||
|
||||
@@ -82,6 +82,10 @@ export const sidebar: SideBar = {
|
||||
href: `${walletSlug}/monitor`,
|
||||
name: "Monitor Transactions",
|
||||
},
|
||||
{
|
||||
href: `${walletSlug}/solana`,
|
||||
name: "Solana Transactions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ separator: true },
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
Callout,
|
||||
createMetadata,
|
||||
OpenApiEndpoint,
|
||||
Steps,
|
||||
Step,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "Solana Server Wallets",
|
||||
icon: "wallets",
|
||||
},
|
||||
title: "Solana Server Wallets | thirdweb",
|
||||
description:
|
||||
"Create and manage Solana server wallets for backend applications",
|
||||
});
|
||||
|
||||
# Solana Server Wallets
|
||||
|
||||
Create and manage Solana wallets for backend applications using thirdweb's secure server wallet infrastructure.
|
||||
|
||||
<Callout variant="info" title="Funding Required">
|
||||
Solana server wallets are **not sponsored**. You must fund your wallet with SOL to transact.
|
||||
</Callout>
|
||||
|
||||
## Capabilities
|
||||
|
||||
- **Wallet Management**: Create and list Solana wallets
|
||||
- **Balance & Transactions**: Check balances, send SOL/SPL tokens, query transaction status
|
||||
- **Signing**: Sign messages and transactions without broadcasting
|
||||
- **Token Swaps**: Get quotes and execute swaps (mainnet only)
|
||||
- **Raw Transactions**: Build and broadcast custom transactions with full instruction control
|
||||
|
||||
## Getting Started
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Server Wallet">
|
||||
Create a Solana server wallet from the [dashboard](https://thirdweb.com/team/~/~/settings/server-wallets) or via API.
|
||||
</Step>
|
||||
|
||||
<Step title="Fund Your Wallet">
|
||||
Transfer SOL to your wallet address to cover transaction fees.
|
||||
</Step>
|
||||
|
||||
<Step title="Start Transacting">
|
||||
Send tokens, sign messages, execute swaps, or submit custom transactions.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## Wallet Management
|
||||
|
||||
### List Solana Wallets
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/wallets" method="GET" />
|
||||
|
||||
### Create Solana Wallet
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/wallets" method="POST" />
|
||||
|
||||
---
|
||||
|
||||
## Balance & Tokens
|
||||
|
||||
### Get Wallet Balance
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/wallets/{address}/balance" method="GET" />
|
||||
|
||||
### Send Tokens
|
||||
|
||||
Transfer native SOL or SPL tokens. Automatically handles token account creation for SPL tokens.
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/send" method="POST" />
|
||||
|
||||
---
|
||||
|
||||
## Message & Transaction Signing
|
||||
|
||||
### Sign Message
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/sign-message" method="POST" />
|
||||
|
||||
### Sign Transaction
|
||||
|
||||
Sign a transaction without broadcasting. Provide a serialized transaction or instructions to assemble one.
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/sign-transaction" method="POST" />
|
||||
|
||||
### Broadcast Signed Transaction
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/broadcast-transaction" method="POST" />
|
||||
|
||||
---
|
||||
|
||||
## Custom Transactions
|
||||
|
||||
### Send Transaction
|
||||
|
||||
Submit a transaction composed of one or more instructions. Transactions are queued and processed asynchronously.
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/transactions" method="POST" />
|
||||
|
||||
### Get Transaction Status
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/transactions/{transactionId}" method="GET" />
|
||||
|
||||
---
|
||||
|
||||
## Token Swaps
|
||||
|
||||
<Callout variant="info" title="Mainnet Only">
|
||||
Token swaps are only available on Solana mainnet.
|
||||
</Callout>
|
||||
|
||||
### Get Swap Quote
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/swap" method="GET" />
|
||||
|
||||
### Execute Swap
|
||||
|
||||
<OpenApiEndpoint path="/v1/solana/swap" method="POST" />
|
||||
|
||||
---
|
||||
|
||||
## Example Workflow
|
||||
|
||||
<Tabs defaultValue="create">
|
||||
<TabsList>
|
||||
<TabsTrigger value="create">1. Create Wallet</TabsTrigger>
|
||||
<TabsTrigger value="fund">2. Check Balance</TabsTrigger>
|
||||
<TabsTrigger value="send">3. Send Tokens</TabsTrigger>
|
||||
<TabsTrigger value="swap">4. Swap Tokens</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="create">
|
||||
```bash
|
||||
curl -X POST "https://api.thirdweb.com/v1/solana/wallets" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-secret-key: YOUR_SECRET_KEY" \
|
||||
-d '{"label": "treasury-wallet"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"address": "8JBLmveV4YF5AQ7EVnKJgyj6etGgVkPp3tYqQMTu3p5B",
|
||||
"label": "treasury-wallet",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="fund">
|
||||
```bash
|
||||
curl -X GET "https://api.thirdweb.com/v1/solana/wallets/8JBLmveV4YF5AQ7EVnKJgyj6etGgVkPp3tYqQMTu3p5B/balance?chainId=solana:mainnet" \
|
||||
-H "x-secret-key: YOUR_SECRET_KEY"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"chainId": "solana:mainnet",
|
||||
"decimals": 9,
|
||||
"displayValue": "1.5",
|
||||
"value": "1500000000"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="send">
|
||||
```bash
|
||||
curl -X POST "https://api.thirdweb.com/v1/solana/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-secret-key: YOUR_SECRET_KEY" \
|
||||
-d '{
|
||||
"from": "8JBLmveV4YF5AQ7EVnKJgyj6etGgVkPp3tYqQMTu3p5B",
|
||||
"to": "FhtwVYF1wKAm7fWmE2N5P2eCv13wt2aT8W4Q9NQ9YcJH",
|
||||
"amount": "100000000",
|
||||
"chainId": "solana:mainnet"
|
||||
}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"transactionId": "tx_abc123..."
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="swap">
|
||||
```bash
|
||||
curl -X POST "https://api.thirdweb.com/v1/solana/swap" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-secret-key: YOUR_SECRET_KEY" \
|
||||
-d '{
|
||||
"address": "8JBLmveV4YF5AQ7EVnKJgyj6etGgVkPp3tYqQMTu3p5B",
|
||||
"tokenIn": "So11111111111111111111111111111111111111112",
|
||||
"tokenOut": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
||||
"amount": "100000000",
|
||||
"chainId": "solana:mainnet"
|
||||
}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"signature": "5ttCNobho7nk5F...",
|
||||
"inputMint": "So11111111111111111111111111111111111111112",
|
||||
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
||||
"inputAmount": "100000000",
|
||||
"outputAmount": "95423156",
|
||||
"requestId": "uuid-1234-5678"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Network Identifiers
|
||||
|
||||
| Network | Identifier |
|
||||
|---------|------------|
|
||||
| Mainnet | `solana:mainnet` |
|
||||
| Devnet | `solana:devnet` |
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Callout, createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "Solana Facilitator",
|
||||
icon: "solana",
|
||||
},
|
||||
title: "Solana Facilitator",
|
||||
description: "Verify and settle Solana transactions through your own facilitator wallets.",
|
||||
});
|
||||
|
||||
# Solana Facilitator
|
||||
|
||||
Solana is now a supported network for x402 payments. You can gate, verify, and settle Solana transactions through your own facilitator wallets — fully end-to-end.
|
||||
|
||||
<Callout variant="info" title="Supported Networks">
|
||||
We support Solana Mainnet and Devnet. Use the following network identifiers:
|
||||
|
||||
- **Mainnet**: `solana:mainnet`
|
||||
- **Devnet**: `solana:devnet`
|
||||
</Callout>
|
||||
|
||||
This enables you to spin up your own server wallet, prepare payment payloads, and handle verification + settlement directly on Solana (devnet or mainnet). No middle layers. No bloat.
|
||||
|
||||
## Features
|
||||
|
||||
- **Solana facilitator flow** — create or register a Solana server wallet, quote pricing with `/v1/payments/x402/accepts`, and settle signed transactions with `/v1/payments/x402/settle`.
|
||||
- **Unified verify path** — `/v1/payments/x402/verify` now validates Solana payloads the same way it does for EVM, so you can reuse your middleware checks across chains.
|
||||
- **End-to-end examples** — refreshed snippets for Node environments that show the exact headers, payloads, and success envelopes you should expect in production.
|
||||
|
||||
## Fast Start Guide
|
||||
|
||||
### 1. Create a facilitator wallet
|
||||
|
||||
First, create a server-side wallet that will act as the facilitator.
|
||||
|
||||
```ts
|
||||
const response = await fetch("https://api.thirdweb.com/v1/solana/wallets", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-secret-key": "YOUR_SECRET_KEY",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
label: "my-solana-facilitator",
|
||||
}),
|
||||
});
|
||||
|
||||
const wallet = await response.json();
|
||||
console.log(wallet.result.address);
|
||||
```
|
||||
|
||||
The returned address is the server-side payer for settles, so double-check that it appears in your project’s server wallet list.
|
||||
|
||||
<Callout variant="info">
|
||||
**Fund the wallet:** Top up SOL for fees on the target cluster (devnet or mainnet-beta).
|
||||
</Callout>
|
||||
|
||||
### 2. Quote access
|
||||
|
||||
Use `/v1/payments/x402/accepts` to generate the payment requirements.
|
||||
|
||||
```ts
|
||||
const acceptsResponse = await fetch(
|
||||
"https://api.thirdweb.com/v1/payments/x402/accepts",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-secret-key": "YOUR_SECRET_KEY",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
resourceUrl: "https://example.com/solana-protected",
|
||||
method: "POST",
|
||||
network: "solana:devnet",
|
||||
price: {
|
||||
amount: "0",
|
||||
asset: {
|
||||
address: "So11111111111111111111111111111111111111112",
|
||||
decimals: 9,
|
||||
tokenProgram: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
},
|
||||
},
|
||||
serverWalletAddress: "YOUR_SOLANA_FACILITATOR_ADDRESS",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const { accepts } = await acceptsResponse.json();
|
||||
const paymentRequirements = accepts[0];
|
||||
```
|
||||
|
||||
Keep `paymentRequirements` around—clients will sign against it, and the facilitator will reuse it when settling.
|
||||
|
||||
### 3. Verify before you settle
|
||||
|
||||
Once you have the signed payment payload from the client, you can verify it.
|
||||
|
||||
```ts
|
||||
const verifyResponse = await fetch(
|
||||
"https://api.thirdweb.com/v1/payments/x402/verify",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-secret-key": "YOUR_SECRET_KEY",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
paymentPayload: SIGNED_PAYMENT_PAYLOAD,
|
||||
paymentRequirements,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const verifyData = await verifyResponse.json();
|
||||
console.log(verifyData.isValid);
|
||||
```
|
||||
|
||||
A truthy `isValid` means the signatures, blockhash, and amounts all check out.
|
||||
|
||||
### 4. Settle on-chain
|
||||
|
||||
Settle the transaction using your facilitator wallet.
|
||||
|
||||
```ts
|
||||
const settleResponse = await fetch(
|
||||
"https://api.thirdweb.com/v1/payments/x402/settle",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-secret-key": "YOUR_SECRET_KEY",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
paymentPayload: SIGNED_PAYMENT_PAYLOAD,
|
||||
paymentRequirements,
|
||||
waitUntil: "submitted",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const settleData = await settleResponse.json();
|
||||
console.log(settleData.transaction);
|
||||
```
|
||||
|
||||
<Callout variant="info">
|
||||
Use `waitUntil: "confirmed"` if you need finality before responding to the client.
|
||||
</Callout>
|
||||
|
||||
Watch your server wallet dashboard or any Solana explorer for the transaction signature the settle endpoint returns.
|
||||
|
||||
### Fetch protected resources
|
||||
|
||||
Let payers fetch protected resources. If the client hits the protected endpoint first, have it replay the request through `/v1/payments/x402/fetch`. The API will answer with the payload you need for verification and settlement.
|
||||
|
||||
```ts
|
||||
const fetchResponse = await fetch(
|
||||
"https://api.thirdweb.com/v1/payments/x402/fetch?" +
|
||||
new URLSearchParams({
|
||||
url: "https://example.com/solana-protected",
|
||||
from: "PAYER_SOLANA_ADDRESS",
|
||||
chainId: "solana:devnet",
|
||||
}),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-secret-key": "YOUR_SECRET_KEY",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const fetchData = await fetchResponse.json();
|
||||
```
|
||||
@@ -37,6 +37,10 @@ export const sidebar: SideBar = {
|
||||
href: `${x402Slug}/facilitator`,
|
||||
name: "Facilitator",
|
||||
},
|
||||
{
|
||||
href: `${x402Slug}/facilitator/solana`,
|
||||
name: "Solana",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.42
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7b873d3`](https://github.com/thirdweb-dev/js/commit/7b873d38db373151ef42dfa7bc1e083568cb5432), [`b0eccf3`](https://github.com/thirdweb-dev/js/commit/b0eccf365a7521ec882fca9e680837c84147041e), [`a704c3f`](https://github.com/thirdweb-dev/js/commit/a704c3f2b00434f8f50653aa2825b4adf7cd4696)]:
|
||||
- thirdweb@5.115.3
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.197
|
||||
|
||||
## 0.0.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`859032d`](https://github.com/thirdweb-dev/js/commit/859032dc1761afdb7a4e12b40534e3a98b771498)]:
|
||||
- thirdweb@5.115.2
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.196
|
||||
|
||||
## 0.0.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`551ec68`](https://github.com/thirdweb-dev/js/commit/551ec680df3a84d2cacca4dfb9f0522fd7383f7a), [`b6be8d6`](https://github.com/thirdweb-dev/js/commit/b6be8d67ef74f425e911d900a9adb19f9a70efd9)]:
|
||||
- thirdweb@5.115.1
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.195
|
||||
|
||||
## 0.0.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.39",
|
||||
"version": "0.0.42",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -14,16 +14,16 @@
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@thirdweb-dev/wagmi-adapter": "workspace:*",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"thirdweb": "workspace:*",
|
||||
"viem": "2.39.0",
|
||||
"wagmi": "2.19.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"@wagmi/cli": "latest",
|
||||
"buffer": "^6.0.3",
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next": "15.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-hook-form": "7.55.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "2.0.6",
|
||||
@@ -25,10 +25,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.6",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"eslint": "8.57.0",
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.100
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7b873d3`](https://github.com/thirdweb-dev/js/commit/7b873d38db373151ef42dfa7bc1e083568cb5432), [`b0eccf3`](https://github.com/thirdweb-dev/js/commit/b0eccf365a7521ec882fca9e680837c84147041e), [`a704c3f`](https://github.com/thirdweb-dev/js/commit/a704c3f2b00434f8f50653aa2825b4adf7cd4696)]:
|
||||
- thirdweb@5.115.3
|
||||
|
||||
## 0.2.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`859032d`](https://github.com/thirdweb-dev/js/commit/859032dc1761afdb7a4e12b40534e3a98b771498)]:
|
||||
- thirdweb@5.115.2
|
||||
|
||||
## 0.2.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`551ec68`](https://github.com/thirdweb-dev/js/commit/551ec680df3a84d2cacca4dfb9f0522fd7383f7a), [`b6be8d6`](https://github.com/thirdweb-dev/js/commit/b6be8d67ef74f425e911d900a9adb19f9a70efd9)]:
|
||||
- thirdweb@5.115.1
|
||||
|
||||
## 0.2.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.97"
|
||||
"version": "0.2.100"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.115.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8509](https://github.com/thirdweb-dev/js/pull/8509) [`7b873d3`](https://github.com/thirdweb-dev/js/commit/7b873d38db373151ef42dfa7bc1e083568cb5432) Thanks [@MananTank](https://github.com/MananTank)! - Update success message shown in SwapWidget component
|
||||
|
||||
- [#8501](https://github.com/thirdweb-dev/js/pull/8501) [`b0eccf3`](https://github.com/thirdweb-dev/js/commit/b0eccf365a7521ec882fca9e680837c84147041e) Thanks [@MananTank](https://github.com/MananTank)! - Remove tabs from token selection UI in SwapWidget
|
||||
|
||||
- [#8504](https://github.com/thirdweb-dev/js/pull/8504) [`a704c3f`](https://github.com/thirdweb-dev/js/commit/a704c3f2b00434f8f50653aa2825b4adf7cd4696) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow passing an activeWallet to SwapWidget
|
||||
|
||||
## 5.115.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8496](https://github.com/thirdweb-dev/js/pull/8496) [`859032d`](https://github.com/thirdweb-dev/js/commit/859032dc1761afdb7a4e12b40534e3a98b771498) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Skip swap review step when coming from swap widget
|
||||
|
||||
## 5.115.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8490](https://github.com/thirdweb-dev/js/pull/8490) [`551ec68`](https://github.com/thirdweb-dev/js/commit/551ec680df3a84d2cacca4dfb9f0522fd7383f7a) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Payment selection tracking
|
||||
|
||||
- [#8495](https://github.com/thirdweb-dev/js/pull/8495) [`b6be8d6`](https://github.com/thirdweb-dev/js/commit/b6be8d67ef74f425e911d900a9adb19f9a70efd9) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Faster token balance load query
|
||||
|
||||
## 5.115.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/prompts": "2.4.9",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@viem/anvil": "0.0.10",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
@@ -79,8 +79,8 @@
|
||||
"knip": "5.60.2",
|
||||
"msw": "2.7.5",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-native": "0.78.1",
|
||||
"react-native-aes-gcm-crypto": "0.2.2",
|
||||
"react-native-passkey": "3.1.0",
|
||||
@@ -430,5 +430,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.115.0"
|
||||
"version": "5.115.3"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Quote } from "../../../bridge/index.js";
|
||||
import { ApiError } from "../../../bridge/types/Errors.js";
|
||||
import type { Token, TokenWithPrices } from "../../../bridge/types/Token.js";
|
||||
import type { TokenWithPrices } from "../../../bridge/types/Token.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { getThirdwebBaseUrl } from "../../../utils/domains.js";
|
||||
import { getClientFetch } from "../../../utils/fetch.js";
|
||||
@@ -29,7 +29,7 @@ import { useActiveWallet } from "./wallets/useActiveWallet.js";
|
||||
* ```
|
||||
*/
|
||||
export function usePaymentMethods(options: {
|
||||
destinationToken: Token;
|
||||
destinationToken: TokenWithPrices;
|
||||
destinationAmount: string;
|
||||
client: ThirdwebClient;
|
||||
payerWallet?: Wallet;
|
||||
@@ -65,6 +65,8 @@ export function usePaymentMethods(options: {
|
||||
"amount",
|
||||
toUnits(destinationAmount, destinationToken.decimals).toString(),
|
||||
);
|
||||
// dont include quotes to speed up the query
|
||||
url.searchParams.set("includeQuotes", "false");
|
||||
|
||||
const clientFetch = getClientFetch(client);
|
||||
const response = await clientFetch(url.toString());
|
||||
@@ -80,8 +82,9 @@ export function usePaymentMethods(options: {
|
||||
|
||||
const {
|
||||
data: allValidOriginTokens,
|
||||
}: { data: { quote: Quote; balance: string; token: TokenWithPrices }[] } =
|
||||
await response.json();
|
||||
}: {
|
||||
data: { quote?: Quote; balance: string; token: TokenWithPrices }[];
|
||||
} = await response.json();
|
||||
|
||||
// Sort by enough balance to pay THEN gross balance
|
||||
const validTokenQuotes = allValidOriginTokens.map((s) => ({
|
||||
@@ -92,7 +95,7 @@ export function usePaymentMethods(options: {
|
||||
quote: s.quote,
|
||||
}));
|
||||
|
||||
const sufficientBalanceQuotes = validTokenQuotes
|
||||
const sortedValidTokenQuotes = validTokenQuotes
|
||||
.filter((s) => !!s.originToken.prices.USD)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
@@ -114,18 +117,29 @@ export function usePaymentMethods(options: {
|
||||
)
|
||||
: [];
|
||||
const finalQuotes = supportedTokens
|
||||
? sufficientBalanceQuotes.filter((q) =>
|
||||
? sortedValidTokenQuotes.filter((q) =>
|
||||
tokensToInclude.find(
|
||||
(t) =>
|
||||
t.chainId === q.originToken.chainId &&
|
||||
t.address.toLowerCase() === q.originToken.address.toLowerCase(),
|
||||
),
|
||||
)
|
||||
: sufficientBalanceQuotes;
|
||||
return finalQuotes.map((x) => ({
|
||||
...x,
|
||||
action: "buy",
|
||||
}));
|
||||
: sortedValidTokenQuotes;
|
||||
|
||||
const requiredUsdValue =
|
||||
(destinationToken.prices?.["USD"] ?? 0) * Number(destinationAmount);
|
||||
|
||||
return finalQuotes.map((x) => {
|
||||
const tokenUsdValue =
|
||||
(x.originToken.prices?.["USD"] ?? 0) *
|
||||
Number(toTokens(x.balance, x.originToken.decimals));
|
||||
const hasEnoughBalance = tokenUsdValue >= requiredUsdValue;
|
||||
return {
|
||||
...x,
|
||||
action: "buy",
|
||||
hasEnoughBalance,
|
||||
};
|
||||
});
|
||||
},
|
||||
queryKey: [
|
||||
"payment-methods",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { TokenWithPrices } from "../../../../bridge/index.js";
|
||||
import type { Chain } from "../../../../chains/types.js";
|
||||
@@ -327,18 +326,17 @@ export type BuyWidgetProps = {
|
||||
* @bridge
|
||||
*/
|
||||
export function BuyWidget(props: BuyWidgetProps) {
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:buy_widget:render",
|
||||
toChainId: props.chain?.id,
|
||||
toToken: props.tokenAddress,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["buy_widget:render"],
|
||||
});
|
||||
const hasFiredRenderEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredRenderEvent.current) return;
|
||||
hasFiredRenderEvent.current = true;
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:buy_widget:render",
|
||||
toChainId: props.chain?.id,
|
||||
toToken: props.tokenAddress,
|
||||
});
|
||||
}, [props.client, props.chain?.id, props.tokenAddress]);
|
||||
|
||||
// if branding is disabled for widget, disable it for connect options too
|
||||
const connectOptions = useMemo(() => {
|
||||
@@ -500,6 +498,12 @@ function BridgeWidgetContent(
|
||||
client={props.client}
|
||||
connectOptions={props.connectOptions}
|
||||
onContinue={(destinationAmount, destinationToken, receiverAddress) => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "payment_selection",
|
||||
toChainId: destinationToken.chainId,
|
||||
toToken: destinationToken.address,
|
||||
});
|
||||
setScreen({
|
||||
id: "2:methodSelection",
|
||||
destinationAmount,
|
||||
@@ -548,6 +552,20 @@ function BridgeWidgetContent(
|
||||
handleError(error, undefined);
|
||||
}}
|
||||
onPaymentMethodSelected={(paymentMethod) => {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.chainId
|
||||
: undefined,
|
||||
client: props.client,
|
||||
event: "ub:ui:loading_quote:fund_wallet",
|
||||
fromToken:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.address
|
||||
: undefined,
|
||||
toChainId: screen.destinationToken.chainId,
|
||||
toToken: screen.destinationToken.address,
|
||||
});
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "3:load-quote",
|
||||
@@ -581,6 +599,37 @@ function BridgeWidgetContent(
|
||||
handleError(error, undefined);
|
||||
}}
|
||||
onQuoteReceived={(preparedQuote, request) => {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.type === "onramp"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.originChainId,
|
||||
client: props.client,
|
||||
event: "payment_details",
|
||||
fromToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.type === "onramp"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.originTokenAddress,
|
||||
toChainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.type === "onramp"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.destinationChainId,
|
||||
toToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.type === "onramp"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.destinationTokenAddress,
|
||||
walletAddress:
|
||||
screen.paymentMethod.payerWallet?.getAccount()?.address,
|
||||
walletType: screen.paymentMethod.payerWallet?.id,
|
||||
});
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "4:preview",
|
||||
@@ -674,6 +723,7 @@ function BridgeWidgetContent(
|
||||
if (screen.id === "6:success") {
|
||||
return (
|
||||
<SuccessScreen
|
||||
type="payment-success"
|
||||
// from props
|
||||
client={props.client}
|
||||
hasPaymentId={!!props.paymentLinkId}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { TokenWithPrices } from "../../../../bridge/index.js";
|
||||
import type { Chain } from "../../../../chains/types.js";
|
||||
@@ -335,18 +334,17 @@ function CheckoutWidgetContentWrapper(props: CheckoutWidgetProps) {
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:checkout_widget:render",
|
||||
toChainId: props.chain.id,
|
||||
toToken: props.tokenAddress,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["checkout_widget:render"],
|
||||
});
|
||||
const hasFiredRenderEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredRenderEvent.current) return;
|
||||
hasFiredRenderEvent.current = true;
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:checkout_widget:render",
|
||||
toChainId: props.chain.id,
|
||||
toToken: props.tokenAddress,
|
||||
});
|
||||
}, [props.client, props.chain.id, props.tokenAddress]);
|
||||
|
||||
// if branding is disabled for widget, disable it for connect options too
|
||||
const connectOptions = useMemo(() => {
|
||||
@@ -522,6 +520,12 @@ function CheckoutWidgetContent(
|
||||
buttonLabel={props.buttonLabel}
|
||||
// others
|
||||
onContinue={(destinationAmount, destinationToken, receiverAddress) => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "payment_selection",
|
||||
toChainId: destinationToken.chainId,
|
||||
toToken: destinationToken.address,
|
||||
});
|
||||
setScreen({
|
||||
id: "2:methodSelection",
|
||||
destinationAmount,
|
||||
@@ -556,6 +560,20 @@ function CheckoutWidgetContent(
|
||||
handleError(error, undefined);
|
||||
}}
|
||||
onPaymentMethodSelected={(paymentMethod) => {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.chainId
|
||||
: undefined,
|
||||
client: props.client,
|
||||
event: "ub:ui:loading_quote:direct_payment",
|
||||
fromToken:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.address
|
||||
: undefined,
|
||||
toChainId: screen.destinationToken.chainId,
|
||||
toToken: screen.destinationToken.address,
|
||||
});
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "3:load-quote",
|
||||
@@ -589,6 +607,35 @@ function CheckoutWidgetContent(
|
||||
handleError(error, undefined);
|
||||
}}
|
||||
onQuoteReceived={(preparedQuote, request) => {
|
||||
if (
|
||||
preparedQuote.type === "buy" ||
|
||||
preparedQuote.type === "sell" ||
|
||||
preparedQuote.type === "transfer"
|
||||
) {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.originChainId,
|
||||
client: props.client,
|
||||
event: "payment_details",
|
||||
fromToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.originTokenAddress,
|
||||
toChainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.destinationChainId,
|
||||
toToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.destinationTokenAddress,
|
||||
walletAddress:
|
||||
screen.paymentMethod.payerWallet?.getAccount()?.address,
|
||||
walletType: screen.paymentMethod.payerWallet?.id,
|
||||
});
|
||||
}
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "4:preview",
|
||||
@@ -683,6 +730,7 @@ function CheckoutWidgetContent(
|
||||
if (screen.id === "6:success") {
|
||||
return (
|
||||
<SuccessScreen
|
||||
type="payment-success"
|
||||
// from props
|
||||
client={props.client}
|
||||
hasPaymentId={!!props.paymentLinkId}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { CrossCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { ThirdwebClient } from "../../../../client/client.js";
|
||||
import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js";
|
||||
@@ -38,17 +38,16 @@ export function ErrorBanner({
|
||||
|
||||
const { userMessage } = useBridgeError({ error });
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client,
|
||||
error: error.message,
|
||||
event: "ub:ui:error",
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["error_banner", userMessage],
|
||||
});
|
||||
const hasFiredErrorEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredErrorEvent.current) return;
|
||||
hasFiredErrorEvent.current = true;
|
||||
trackPayEvent({
|
||||
client,
|
||||
error: error.message,
|
||||
event: "ub:ui:error",
|
||||
});
|
||||
}, [client, error.message]);
|
||||
|
||||
return (
|
||||
<Container flex="column" fullHeight gap="md" p="md">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { Token } from "../../../../bridge/types/Token.js";
|
||||
import type { ThirdwebClient } from "../../../../client/client.js";
|
||||
import type { PurchaseData } from "../../../../pay/types.js";
|
||||
@@ -92,7 +90,7 @@ export function QuoteLoader({
|
||||
purchaseData,
|
||||
paymentLinkId,
|
||||
feePayer,
|
||||
mode,
|
||||
mode: _mode,
|
||||
}: QuoteLoaderProps) {
|
||||
const request: BridgePrepareRequest = getBridgeParams({
|
||||
amount,
|
||||
@@ -107,28 +105,6 @@ export function QuoteLoader({
|
||||
});
|
||||
const prepareQuery = useBridgePrepare(request);
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.chainId
|
||||
: undefined,
|
||||
client,
|
||||
event: `ub:ui:loading_quote:${mode}`,
|
||||
fromToken:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.address
|
||||
: undefined,
|
||||
toChainId: destinationToken.chainId,
|
||||
toToken: destinationToken.address,
|
||||
walletAddress: sender,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["loading_quote", paymentMethod.type],
|
||||
});
|
||||
|
||||
// Handle successful quote
|
||||
useEffect(() => {
|
||||
if (prepareQuery.data) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { TokenWithPrices } from "../../../../bridge/index.js";
|
||||
import type { Chain } from "../../../../chains/types.js";
|
||||
@@ -349,18 +349,17 @@ type TransactionQueryResult =
|
||||
};
|
||||
|
||||
export function TransactionWidgetContentWrapper(props: TransactionWidgetProps) {
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
chainId: props.transaction.chain.id,
|
||||
client: props.client,
|
||||
event: "ub:ui:transaction_widget:render",
|
||||
toToken: props.tokenAddress,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["transaction_widget:render"],
|
||||
});
|
||||
const hasFiredRenderEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredRenderEvent.current) return;
|
||||
hasFiredRenderEvent.current = true;
|
||||
trackPayEvent({
|
||||
chainId: props.transaction.chain.id,
|
||||
client: props.client,
|
||||
event: "ub:ui:transaction_widget:render",
|
||||
toToken: props.tokenAddress,
|
||||
});
|
||||
}, [props.client, props.transaction.chain.id, props.tokenAddress]);
|
||||
|
||||
const localQuery = useConnectLocale(props.locale || "en_US");
|
||||
|
||||
@@ -573,6 +572,12 @@ function TransactionWidgetContent(
|
||||
}}
|
||||
connectOptions={props.connectOptions}
|
||||
onContinue={(destinationAmount, destinationToken, receiverAddress) => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "payment_selection",
|
||||
toChainId: destinationToken.chainId,
|
||||
toToken: destinationToken.address,
|
||||
});
|
||||
setScreen({
|
||||
id: "buy:1.methodSelection",
|
||||
destinationAmount,
|
||||
@@ -618,6 +623,20 @@ function TransactionWidgetContent(
|
||||
handleError(error);
|
||||
}}
|
||||
onPaymentMethodSelected={(paymentMethod) => {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.chainId
|
||||
: undefined,
|
||||
client: props.client,
|
||||
event: "ub:ui:loading_quote:transaction",
|
||||
fromToken:
|
||||
paymentMethod.type === "wallet"
|
||||
? paymentMethod.originToken.address
|
||||
: undefined,
|
||||
toChainId: screen.destinationToken.chainId,
|
||||
toToken: screen.destinationToken.address,
|
||||
});
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "buy:2.load-quote",
|
||||
@@ -651,6 +670,35 @@ function TransactionWidgetContent(
|
||||
handleError(error);
|
||||
}}
|
||||
onQuoteReceived={(preparedQuote, request) => {
|
||||
if (
|
||||
preparedQuote.type === "buy" ||
|
||||
preparedQuote.type === "sell" ||
|
||||
preparedQuote.type === "transfer"
|
||||
) {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.originChainId,
|
||||
client: props.client,
|
||||
event: "payment_details",
|
||||
fromToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.originTokenAddress,
|
||||
toChainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.destinationChainId,
|
||||
toToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.destinationTokenAddress,
|
||||
walletAddress:
|
||||
screen.paymentMethod.payerWallet?.getAccount()?.address,
|
||||
walletType: screen.paymentMethod.payerWallet?.id,
|
||||
});
|
||||
}
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "buy:3.preview",
|
||||
@@ -738,6 +786,7 @@ function TransactionWidgetContent(
|
||||
<SuccessScreen
|
||||
// from props
|
||||
client={props.client}
|
||||
type="payment-success"
|
||||
hasPaymentId={!!props.paymentLinkId}
|
||||
// others
|
||||
completedStatuses={screen.completedStatuses}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { trackPayEvent } from "../../../../analytics/track/pay.js";
|
||||
import type { Chain } from "../../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../../client/client.js";
|
||||
@@ -27,17 +27,17 @@ export function UnsupportedTokenScreen(props: UnsupportedTokenScreenProps) {
|
||||
|
||||
const { data: chainMetadata } = useChainMetadata(chain);
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
chainId: chain.id,
|
||||
client,
|
||||
event: "ub:ui:unsupported_token",
|
||||
fromToken: tokenAddress,
|
||||
});
|
||||
},
|
||||
queryKey: ["unsupported_token"],
|
||||
});
|
||||
const hasFiredUnsupportedEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredUnsupportedEvent.current) return;
|
||||
hasFiredUnsupportedEvent.current = true;
|
||||
trackPayEvent({
|
||||
chainId: chain.id,
|
||||
client,
|
||||
event: "ub:ui:unsupported_token",
|
||||
fromToken: tokenAddress,
|
||||
});
|
||||
}, [chain.id, client, tokenAddress]);
|
||||
|
||||
if (chainMetadata?.testnet) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
|
||||
import { defineChain } from "../../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
@@ -251,16 +250,15 @@ export type BridgeWidgetProps = {
|
||||
export function BridgeWidget(props: BridgeWidgetProps) {
|
||||
const [tab, setTab] = useState<"swap" | "buy">("swap");
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:bridge_widget:render",
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["bridge_widget:render"],
|
||||
});
|
||||
const hasFiredRenderEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredRenderEvent.current) return;
|
||||
hasFiredRenderEvent.current = true;
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:bridge_widget:render",
|
||||
});
|
||||
}, [props.client]);
|
||||
|
||||
return (
|
||||
<CustomThemeProvider theme={props.theme}>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
|
||||
import { defineChain } from "../../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
|
||||
@@ -82,41 +80,6 @@ export function PaymentDetails({
|
||||
}
|
||||
};
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
if (
|
||||
preparedQuote.type === "buy" ||
|
||||
preparedQuote.type === "sell" ||
|
||||
preparedQuote.type === "transfer"
|
||||
) {
|
||||
trackPayEvent({
|
||||
chainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.originChainId,
|
||||
client,
|
||||
event: "payment_details",
|
||||
fromToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.originTokenAddress,
|
||||
toChainId:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.chainId
|
||||
: preparedQuote.intent.destinationChainId,
|
||||
toToken:
|
||||
preparedQuote.type === "transfer"
|
||||
? preparedQuote.intent.tokenAddress
|
||||
: preparedQuote.intent.destinationTokenAddress,
|
||||
walletAddress: paymentMethod.payerWallet?.getAccount()?.address,
|
||||
walletType: paymentMethod.payerWallet?.id,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
queryKey: ["payment_details", preparedQuote.type],
|
||||
});
|
||||
|
||||
const chainsQuery = useChainsQuery(
|
||||
preparedQuote.steps.flatMap((s) => [
|
||||
defineChain(s.originToken.chainId),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
|
||||
import type { Token } from "../../../../../bridge/types/Token.js";
|
||||
import type { TokenWithPrices } from "../../../../../bridge/types/Token.js";
|
||||
import { defineChain } from "../../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
|
||||
@@ -27,7 +26,7 @@ type PaymentSelectionProps = {
|
||||
/**
|
||||
* The destination token to bridge to
|
||||
*/
|
||||
destinationToken: Token;
|
||||
destinationToken: TokenWithPrices;
|
||||
|
||||
/**
|
||||
* The destination amount to bridge
|
||||
@@ -129,21 +128,6 @@ export function PaymentSelection({
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<Step>(initialStep);
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client,
|
||||
event: "payment_selection",
|
||||
toChainId: destinationToken.chainId,
|
||||
toToken: destinationToken.address,
|
||||
walletAddress: payerWallet?.getAccount()?.address,
|
||||
walletType: payerWallet?.id,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["payment_selection"],
|
||||
});
|
||||
|
||||
const payerWallet =
|
||||
currentStep.type === "tokenSelection"
|
||||
? currentStep.selectedWallet
|
||||
@@ -176,6 +160,10 @@ export function PaymentSelection({
|
||||
};
|
||||
|
||||
const handleWalletSelected = (wallet: Wallet) => {
|
||||
trackPayEvent({
|
||||
client,
|
||||
event: "ub:ui:token_selection",
|
||||
});
|
||||
setCurrentStep({ selectedWallet: wallet, type: "tokenSelection" });
|
||||
};
|
||||
|
||||
@@ -184,6 +172,10 @@ export function PaymentSelection({
|
||||
};
|
||||
|
||||
const handleFiatSelected = () => {
|
||||
trackPayEvent({
|
||||
client,
|
||||
event: "ub:ui:fiat_provider_selection",
|
||||
});
|
||||
setCurrentStep({ type: "fiatProviderSelection" });
|
||||
};
|
||||
|
||||
|
||||
@@ -47,10 +47,7 @@ function PaymentMethodTokenRow({
|
||||
}: PaymentMethodTokenRowProps) {
|
||||
const theme = useCustomTheme();
|
||||
|
||||
const displayOriginAmount = paymentMethod.quote.originAmount;
|
||||
const hasEnoughBalance = displayOriginAmount
|
||||
? paymentMethod.balance >= displayOriginAmount
|
||||
: false;
|
||||
const hasEnoughBalance = paymentMethod.hasEnoughBalance;
|
||||
const currencyPrice = paymentMethod.originToken.prices[currency || "USD"];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { CheckIcon } from "@radix-ui/react-icons";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import type { WindowAdapter } from "../../../../core/adapters/WindowAdapter.js";
|
||||
@@ -46,6 +46,8 @@ type SuccessScreenProps = {
|
||||
* Whether or not this payment is associated with a payment ID. If it does, we show a different message.
|
||||
*/
|
||||
hasPaymentId: boolean;
|
||||
|
||||
type: "swap-success" | "payment-success";
|
||||
};
|
||||
|
||||
type ViewState = "success" | "detail";
|
||||
@@ -58,48 +60,49 @@ export function SuccessScreen({
|
||||
client,
|
||||
hasPaymentId = false,
|
||||
showContinueWithTx,
|
||||
type,
|
||||
}: SuccessScreenProps) {
|
||||
const theme = useCustomTheme();
|
||||
const [viewState, setViewState] = useState<ViewState>("success");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
if (preparedQuote.type === "buy" || preparedQuote.type === "sell") {
|
||||
trackPayEvent({
|
||||
chainId: preparedQuote.intent.originChainId,
|
||||
client: client,
|
||||
event: "ub:ui:success_screen",
|
||||
fromToken: preparedQuote.intent.originTokenAddress,
|
||||
toChainId: preparedQuote.intent.destinationChainId,
|
||||
toToken: preparedQuote.intent.destinationTokenAddress,
|
||||
walletAddress: preparedQuote.intent.sender,
|
||||
});
|
||||
}
|
||||
if (preparedQuote.type === "transfer") {
|
||||
trackPayEvent({
|
||||
chainId: preparedQuote.intent.chainId,
|
||||
client: client,
|
||||
event: "ub:ui:success_screen",
|
||||
fromToken: preparedQuote.intent.tokenAddress,
|
||||
toChainId: preparedQuote.intent.chainId,
|
||||
toToken: preparedQuote.intent.tokenAddress,
|
||||
walletAddress: preparedQuote.intent.sender,
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["bridge/v1/wallets"],
|
||||
const hasFiredSuccessEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredSuccessEvent.current) return;
|
||||
hasFiredSuccessEvent.current = true;
|
||||
|
||||
if (preparedQuote.type === "buy" || preparedQuote.type === "sell") {
|
||||
trackPayEvent({
|
||||
chainId: preparedQuote.intent.originChainId,
|
||||
client: client,
|
||||
event: "ub:ui:success_screen",
|
||||
fromToken: preparedQuote.intent.originTokenAddress,
|
||||
toChainId: preparedQuote.intent.destinationChainId,
|
||||
toToken: preparedQuote.intent.destinationTokenAddress,
|
||||
walletAddress: preparedQuote.intent.sender,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["walletBalance"],
|
||||
}
|
||||
if (preparedQuote.type === "transfer") {
|
||||
trackPayEvent({
|
||||
chainId: preparedQuote.intent.chainId,
|
||||
client: client,
|
||||
event: "ub:ui:success_screen",
|
||||
fromToken: preparedQuote.intent.tokenAddress,
|
||||
toChainId: preparedQuote.intent.chainId,
|
||||
toToken: preparedQuote.intent.tokenAddress,
|
||||
walletAddress: preparedQuote.intent.sender,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["payment-methods"],
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["success_screen", preparedQuote.type],
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["bridge/v1/wallets"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["walletBalance"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["payment-methods"],
|
||||
});
|
||||
}, [client, preparedQuote, queryClient]);
|
||||
|
||||
if (viewState === "detail") {
|
||||
return (
|
||||
@@ -112,6 +115,13 @@ export function SuccessScreen({
|
||||
);
|
||||
}
|
||||
|
||||
const title =
|
||||
type === "swap-success" ? "Swap Successful" : "Payment Successful";
|
||||
const description =
|
||||
type === "swap-success"
|
||||
? "Your token swap has been completed successfully."
|
||||
: "Your payment has been completed successfully.";
|
||||
|
||||
return (
|
||||
<Container flex="column" fullHeight px="md" pb="md" pt="md+">
|
||||
<Spacer y="3xl" />
|
||||
@@ -151,7 +161,7 @@ export function SuccessScreen({
|
||||
marginBottom: spacing.xxs,
|
||||
}}
|
||||
>
|
||||
Payment Successful
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text center color="secondaryText" size="sm">
|
||||
@@ -159,7 +169,7 @@ export function SuccessScreen({
|
||||
? "You can now close this page and return to the application."
|
||||
: showContinueWithTx
|
||||
? "Click continue to execute your transaction."
|
||||
: "Your payment has been completed successfully."}
|
||||
: description}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
@@ -173,7 +183,7 @@ export function SuccessScreen({
|
||||
onClick={() => setViewState("detail")}
|
||||
variant="secondary"
|
||||
>
|
||||
View Payment Receipt
|
||||
View Transaction Receipt
|
||||
</Button>
|
||||
|
||||
{!hasPaymentId && (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
|
||||
import type { Buy, Sell } from "../../../../../bridge/index.js";
|
||||
import type { TokenWithPrices } from "../../../../../bridge/types/Token.js";
|
||||
@@ -9,6 +8,7 @@ import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
|
||||
import { getAddress } from "../../../../../utils/address.js";
|
||||
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
|
||||
import { CustomThemeProvider } from "../../../../core/design-system/CustomThemeProvider.js";
|
||||
import type { Theme } from "../../../../core/design-system/index.js";
|
||||
import type { BridgePrepareRequest } from "../../../../core/hooks/useBridgePrepare.js";
|
||||
@@ -17,7 +17,6 @@ import { webWindowAdapter } from "../../../adapters/WindowAdapter.js";
|
||||
import { EmbedContainer } from "../../ConnectWallet/Modal/ConnectEmbed.js";
|
||||
import { DynamicHeight } from "../../components/DynamicHeight.js";
|
||||
import { ErrorBanner } from "../ErrorBanner.js";
|
||||
import { PaymentDetails } from "../payment-details/PaymentDetails.js";
|
||||
import { SuccessScreen } from "../payment-success/SuccessScreen.js";
|
||||
import { StepRunner } from "../StepRunner.js";
|
||||
import { useActiveWalletInfo } from "./hooks.js";
|
||||
@@ -171,6 +170,10 @@ export type SwapWidgetProps = {
|
||||
* Called when the user disconnects the active wallet
|
||||
*/
|
||||
onDisconnect?: () => void;
|
||||
/**
|
||||
* The wallet that should be pre-selected in the SwapWidget UI.
|
||||
*/
|
||||
activeWallet?: Wallet;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -246,16 +249,15 @@ export type SwapWidgetProps = {
|
||||
* @bridge
|
||||
*/
|
||||
export function SwapWidget(props: SwapWidgetProps) {
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:swap_widget:render",
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["swap_widget:render"],
|
||||
});
|
||||
const hasFiredRenderEvent = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasFiredRenderEvent.current) return;
|
||||
hasFiredRenderEvent.current = true;
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:swap_widget:render",
|
||||
});
|
||||
}, [props.client]);
|
||||
|
||||
return (
|
||||
<SwapWidgetContainer
|
||||
@@ -323,7 +325,7 @@ function SwapWidgetContent(
|
||||
},
|
||||
) {
|
||||
const [screen, setScreen] = useState<SwapWidgetScreen>({ id: "1:swap-ui" });
|
||||
const activeWalletInfo = useActiveWalletInfo();
|
||||
const activeWalletInfo = useActiveWalletInfo(props.activeWallet);
|
||||
const isPersistEnabled = props.persistTokenSelections !== false;
|
||||
|
||||
const [amountSelection, setAmountSelection] = useState<{
|
||||
@@ -366,19 +368,6 @@ function SwapWidgetContent(
|
||||
// preload requests
|
||||
useBridgeChains(props.client);
|
||||
|
||||
const handleError = useCallback(
|
||||
(error: Error, quote: SwapPreparedQuote) => {
|
||||
console.error(error);
|
||||
props.onError?.(error, quote);
|
||||
setScreen({
|
||||
id: "error",
|
||||
preparedQuote: quote,
|
||||
error,
|
||||
});
|
||||
},
|
||||
[props.onError],
|
||||
);
|
||||
|
||||
// if wallet suddenly disconnects, show screen 1
|
||||
if (screen.id === "1:swap-ui" || !activeWalletInfo) {
|
||||
return (
|
||||
@@ -402,7 +391,7 @@ function SwapWidgetContent(
|
||||
setAmountSelection={setAmountSelection}
|
||||
onSwap={(data) => {
|
||||
setScreen({
|
||||
id: "2:preview",
|
||||
id: "3:execute",
|
||||
buyToken: data.buyToken,
|
||||
sellToken: data.sellToken,
|
||||
sellTokenBalance: data.sellTokenBalance,
|
||||
@@ -416,42 +405,6 @@ function SwapWidgetContent(
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "2:preview") {
|
||||
return (
|
||||
<PaymentDetails
|
||||
metadata={{
|
||||
title: "Review Swap",
|
||||
description: undefined,
|
||||
}}
|
||||
confirmButtonLabel="Swap"
|
||||
client={props.client}
|
||||
onBack={() => {
|
||||
setScreen({ id: "1:swap-ui" });
|
||||
}}
|
||||
onConfirm={() => {
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "3:execute",
|
||||
});
|
||||
}}
|
||||
onError={(error) => handleError(error, screen.preparedQuote)}
|
||||
paymentMethod={{
|
||||
quote: screen.quote,
|
||||
type: "wallet",
|
||||
payerWallet: activeWalletInfo.activeWallet,
|
||||
balance: screen.sellTokenBalance,
|
||||
originToken: screen.sellToken,
|
||||
action: screen.mode,
|
||||
}}
|
||||
preparedQuote={screen.preparedQuote}
|
||||
currency={props.currency}
|
||||
modeInfo={{
|
||||
mode: "fund_wallet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "3:execute") {
|
||||
return (
|
||||
<StepRunner
|
||||
@@ -462,8 +415,7 @@ function SwapWidgetContent(
|
||||
onBack={() => {
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "2:preview",
|
||||
sellTokenBalance: screen.sellTokenBalance,
|
||||
id: "1:swap-ui",
|
||||
});
|
||||
}}
|
||||
onCancel={() => props.onCancel?.(screen.preparedQuote)}
|
||||
@@ -488,6 +440,7 @@ function SwapWidgetContent(
|
||||
if (screen.id === "4:success") {
|
||||
return (
|
||||
<SuccessScreen
|
||||
type="swap-success"
|
||||
client={props.client}
|
||||
completedStatuses={screen.completedStatuses}
|
||||
onDone={() => {
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { useMemo } from "react";
|
||||
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
|
||||
import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccount.js";
|
||||
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
|
||||
import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js";
|
||||
import type { ActiveWalletInfo } from "./types.js";
|
||||
|
||||
export function useActiveWalletInfo(): ActiveWalletInfo | undefined {
|
||||
export function useActiveWalletInfo(
|
||||
activeWalletOverride?: Wallet,
|
||||
): ActiveWalletInfo | undefined {
|
||||
const activeAccount = useActiveAccount();
|
||||
const activeWallet = useActiveWallet();
|
||||
const activeChain = useActiveWalletChain();
|
||||
|
||||
return useMemo(() => {
|
||||
return activeAccount && activeWallet && activeChain
|
||||
const wallet = activeWalletOverride || activeWallet;
|
||||
const chain = activeWalletOverride?.getChain() || activeChain;
|
||||
const account = activeWalletOverride?.getAccount() || activeAccount;
|
||||
return wallet && chain && account
|
||||
? {
|
||||
activeChain,
|
||||
activeWallet,
|
||||
activeAccount,
|
||||
activeChain: chain,
|
||||
activeWallet: wallet,
|
||||
activeAccount: account,
|
||||
}
|
||||
: undefined;
|
||||
}, [activeAccount, activeWallet, activeChain]);
|
||||
}, [activeAccount, activeWallet, activeChain, activeWalletOverride]);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
radius,
|
||||
spacing,
|
||||
} from "../../../../core/design-system/index.js";
|
||||
import { CoinsIcon } from "../../ConnectWallet/icons/CoinsIcon.js";
|
||||
import { WalletDotIcon } from "../../ConnectWallet/icons/WalletDotIcon.js";
|
||||
import { Container, noScrollBar } from "../../components/basic.js";
|
||||
import { Button } from "../../components/buttons.js";
|
||||
import { Img } from "../../components/Img.js";
|
||||
@@ -50,8 +52,7 @@ function findChain(chains: BridgeChain[], activeChainId: number | undefined) {
|
||||
return chains.find((chain) => chain.chainId === activeChainId);
|
||||
}
|
||||
|
||||
const ALL_TOKENS_LIMIT = 100;
|
||||
const OWNED_TOKENS_LIMIT = 50;
|
||||
const INITIAL_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -60,8 +61,12 @@ export function SelectToken(props: SelectTokenUIProps) {
|
||||
const chainQuery = useBridgeChains(props.client);
|
||||
const [search, _setSearch] = useState("");
|
||||
const debouncedSearch = useDebouncedValue(search, 500);
|
||||
const [allTokensLimit, setAllTokensLimit] = useState(ALL_TOKENS_LIMIT);
|
||||
const [ownedTokensLimit, setOwnedTokensLimit] = useState(OWNED_TOKENS_LIMIT);
|
||||
const [limit, setLimit] = useState(INITIAL_LIMIT);
|
||||
|
||||
const setSearch = useCallback((search: string) => {
|
||||
_setSearch(search);
|
||||
setLimit(INITIAL_LIMIT);
|
||||
}, []);
|
||||
|
||||
const [_selectedChain, setSelectedChain] = useState<BridgeChain | undefined>(
|
||||
undefined,
|
||||
@@ -79,15 +84,15 @@ export function SelectToken(props: SelectTokenUIProps) {
|
||||
client: props.client,
|
||||
chainId: selectedChain?.chainId,
|
||||
search: debouncedSearch,
|
||||
limit: allTokensLimit,
|
||||
limit,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
// owned tokens
|
||||
const ownedTokensQuery = useTokenBalances({
|
||||
clientId: props.client.clientId,
|
||||
client: props.client,
|
||||
chainId: selectedChain?.chainId,
|
||||
limit: ownedTokensLimit,
|
||||
limit,
|
||||
page: 1,
|
||||
walletAddress: props.activeWalletInfo?.activeAccount.address,
|
||||
});
|
||||
@@ -104,62 +109,43 @@ export function SelectToken(props: SelectTokenUIProps) {
|
||||
});
|
||||
}, [ownedTokensQuery.data?.tokens, debouncedSearch]);
|
||||
|
||||
const setSearch = useCallback((search: string) => {
|
||||
_setSearch(search);
|
||||
setAllTokensLimit(ALL_TOKENS_LIMIT);
|
||||
setOwnedTokensLimit(OWNED_TOKENS_LIMIT);
|
||||
}, []);
|
||||
const isFetching = tokensQuery.isFetching || ownedTokensQuery.isFetching;
|
||||
|
||||
return (
|
||||
<SelectTokenUI
|
||||
{...props}
|
||||
ownedTokens={{
|
||||
data: filteredOwnedTokens || [],
|
||||
isFetching: ownedTokensQuery.isFetching,
|
||||
showMore: ownedTokensQuery.data?.pagination.hasMore
|
||||
? () => {
|
||||
setOwnedTokensLimit(ownedTokensLimit + OWNED_TOKENS_LIMIT);
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
allTokens={{
|
||||
data: tokensQuery.data || [],
|
||||
isFetching: tokensQuery.isFetching,
|
||||
showMore:
|
||||
tokensQuery.data?.length === allTokensLimit
|
||||
? () => {
|
||||
setAllTokensLimit(allTokensLimit + ALL_TOKENS_LIMIT);
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
ownedTokens={filteredOwnedTokens || []}
|
||||
allTokens={tokensQuery.data || []}
|
||||
isFetching={isFetching}
|
||||
selectedChain={selectedChain}
|
||||
setSelectedChain={setSelectedChain}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
selectedToken={props.selectedToken}
|
||||
setSelectedToken={props.setSelectedToken}
|
||||
showMore={
|
||||
tokensQuery.data?.length === limit
|
||||
? () => {
|
||||
setLimit(limit + INITIAL_LIMIT);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectTokenUI(
|
||||
props: SelectTokenUIProps & {
|
||||
allTokens: {
|
||||
data: Token[];
|
||||
isFetching: boolean;
|
||||
showMore: (() => void) | undefined;
|
||||
};
|
||||
ownedTokens: {
|
||||
data: TokenBalance[];
|
||||
isFetching: boolean;
|
||||
showMore: (() => void) | undefined;
|
||||
};
|
||||
ownedTokens: TokenBalance[];
|
||||
allTokens: Token[];
|
||||
isFetching: boolean;
|
||||
selectedChain: BridgeChain | undefined;
|
||||
setSelectedChain: (chain: BridgeChain) => void;
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
selectedToken: TokenSelection | undefined;
|
||||
setSelectedToken: (token: TokenSelection) => void;
|
||||
showMore: (() => void) | undefined;
|
||||
},
|
||||
) {
|
||||
const isMobile = useIsMobile();
|
||||
@@ -169,7 +155,7 @@ function SelectTokenUI(
|
||||
|
||||
// show tokens with icons first
|
||||
const sortedOwnedTokens = useMemo(() => {
|
||||
return props.ownedTokens.data.sort((a, b) => {
|
||||
return props.ownedTokens.sort((a, b) => {
|
||||
if (a.icon_uri && !b.icon_uri) {
|
||||
return -1;
|
||||
}
|
||||
@@ -178,11 +164,23 @@ function SelectTokenUI(
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [props.ownedTokens.data]);
|
||||
}, [props.ownedTokens]);
|
||||
|
||||
const otherTokens = useMemo(() => {
|
||||
const ownedTokenSet = new Set(
|
||||
sortedOwnedTokens.map((t) =>
|
||||
`${t.token_address}-${t.chain_id}`.toLowerCase(),
|
||||
),
|
||||
);
|
||||
return props.allTokens.filter(
|
||||
(token) =>
|
||||
!ownedTokenSet.has(`${token.address}-${token.chainId}`.toLowerCase()),
|
||||
);
|
||||
}, [props.allTokens, sortedOwnedTokens]);
|
||||
|
||||
// show tokens with icons first
|
||||
const sortedAllTokens = useMemo(() => {
|
||||
return props.allTokens.data.sort((a, b) => {
|
||||
const sortedOtherTokens = useMemo(() => {
|
||||
return otherTokens.sort((a, b) => {
|
||||
if (a.iconUri && !b.iconUri) {
|
||||
return -1;
|
||||
}
|
||||
@@ -191,7 +189,7 @@ function SelectTokenUI(
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [props.allTokens.data]);
|
||||
}, [otherTokens]);
|
||||
|
||||
// desktop
|
||||
if (!isMobile) {
|
||||
@@ -223,20 +221,15 @@ function SelectTokenUI(
|
||||
}}
|
||||
isMobile={false}
|
||||
selectedToken={props.selectedToken}
|
||||
ownedTokens={{
|
||||
...props.ownedTokens,
|
||||
data: sortedOwnedTokens,
|
||||
}}
|
||||
allTokens={{
|
||||
...props.allTokens,
|
||||
data: sortedAllTokens,
|
||||
}}
|
||||
isFetching={props.isFetching}
|
||||
ownedTokens={props.ownedTokens}
|
||||
otherTokens={sortedOtherTokens}
|
||||
showMore={props.showMore}
|
||||
selectedChain={props.selectedChain}
|
||||
onSelectChain={() => setScreen("select-chain")}
|
||||
client={props.client}
|
||||
search={props.search}
|
||||
setSearch={props.setSearch}
|
||||
isWalletConnected={props.activeWalletInfo !== undefined}
|
||||
/>
|
||||
</Container>
|
||||
</Container>
|
||||
@@ -246,20 +239,15 @@ function SelectTokenUI(
|
||||
if (screen === "select-token") {
|
||||
return (
|
||||
<TokenSelectionScreen
|
||||
isWalletConnected={props.activeWalletInfo !== undefined}
|
||||
onSelectToken={(token) => {
|
||||
props.setSelectedToken(token);
|
||||
props.onClose();
|
||||
}}
|
||||
selectedToken={props.selectedToken}
|
||||
ownedTokens={{
|
||||
...props.ownedTokens,
|
||||
data: sortedOwnedTokens,
|
||||
}}
|
||||
allTokens={{
|
||||
...props.allTokens,
|
||||
data: sortedAllTokens,
|
||||
}}
|
||||
isFetching={props.isFetching}
|
||||
ownedTokens={props.ownedTokens}
|
||||
otherTokens={sortedOtherTokens}
|
||||
showMore={props.showMore}
|
||||
selectedChain={props.selectedChain}
|
||||
isMobile={true}
|
||||
onSelectChain={() => setScreen("select-chain")}
|
||||
@@ -447,36 +435,17 @@ function TokenSelectionScreen(props: {
|
||||
client: ThirdwebClient;
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
isFetching: boolean;
|
||||
ownedTokens: TokenBalance[];
|
||||
otherTokens: Token[];
|
||||
showMore: (() => void) | undefined;
|
||||
selectedToken: TokenSelection | undefined;
|
||||
onSelectToken: (token: TokenSelection) => void;
|
||||
isWalletConnected: boolean;
|
||||
allTokens: {
|
||||
data: Token[];
|
||||
isFetching: boolean;
|
||||
showMore: (() => void) | undefined;
|
||||
};
|
||||
ownedTokens: {
|
||||
data: TokenBalance[];
|
||||
isFetching: boolean;
|
||||
showMore: (() => void) | undefined;
|
||||
};
|
||||
}) {
|
||||
const noAllTokensFound =
|
||||
!props.allTokens.isFetching && props.allTokens.data.length === 0;
|
||||
|
||||
const noOwnedTokensFound =
|
||||
!props.ownedTokens.isFetching && props.ownedTokens.data.length === 0;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"owned" | "all">("all");
|
||||
const showSkeletons =
|
||||
(props.allTokens.isFetching && activeTab === "all") ||
|
||||
(props.ownedTokens.isFetching && activeTab === "owned");
|
||||
|
||||
const showNoTokensFound =
|
||||
(activeTab === "all" && noAllTokensFound) ||
|
||||
(activeTab === "owned" && noOwnedTokensFound);
|
||||
|
||||
const theme = useCustomTheme();
|
||||
const noTokensFound =
|
||||
!props.isFetching &&
|
||||
props.otherTokens.length === 0 &&
|
||||
props.ownedTokens.length === 0;
|
||||
|
||||
return (
|
||||
<Container fullHeight flex="column">
|
||||
@@ -534,28 +503,7 @@ function TokenSelectionScreen(props: {
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Spacer y="sm" />
|
||||
|
||||
{/* tabs */}
|
||||
{props.isWalletConnected && (
|
||||
<Container
|
||||
flex="row"
|
||||
gap="xs"
|
||||
px="md"
|
||||
style={{ borderBottom: `1px solid ${theme.colors.borderColor}` }}
|
||||
>
|
||||
<TabButton
|
||||
isSelected={activeTab === "all"}
|
||||
onSelect={() => setActiveTab("all")}
|
||||
label="All Tokens"
|
||||
/>
|
||||
<TabButton
|
||||
isSelected={activeTab === "owned"}
|
||||
onSelect={() => setActiveTab("owned")}
|
||||
label="Your Tokens"
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
<Spacer y="xs" />
|
||||
|
||||
<Container
|
||||
pb="md"
|
||||
@@ -571,17 +519,36 @@ function TokenSelectionScreen(props: {
|
||||
paddingBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
{props.isWalletConnected && <Spacer y="xxs" />}
|
||||
|
||||
{showSkeletons &&
|
||||
{props.isFetching &&
|
||||
new Array(20).fill(0).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: ok
|
||||
<TokenButtonSkeleton key={i} />
|
||||
))}
|
||||
|
||||
{activeTab === "owned" &&
|
||||
!props.ownedTokens.isFetching &&
|
||||
props.ownedTokens.data.map((token) => (
|
||||
{!props.isFetching && props.ownedTokens.length > 0 && (
|
||||
<Container
|
||||
px="xs"
|
||||
py="xs"
|
||||
flex="row"
|
||||
gap="xs"
|
||||
center="y"
|
||||
color="secondaryText"
|
||||
>
|
||||
<WalletDotIcon size="14" />
|
||||
<Text
|
||||
size="sm"
|
||||
color="secondaryText"
|
||||
style={{
|
||||
overflow: "unset",
|
||||
}}
|
||||
>
|
||||
Your Tokens
|
||||
</Text>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{!props.isFetching &&
|
||||
props.ownedTokens.map((token) => (
|
||||
<TokenButton
|
||||
key={token.token_address}
|
||||
token={token}
|
||||
@@ -596,9 +563,33 @@ function TokenSelectionScreen(props: {
|
||||
/>
|
||||
))}
|
||||
|
||||
{activeTab === "all" &&
|
||||
!props.allTokens.isFetching &&
|
||||
props.allTokens.data.map((token) => (
|
||||
{!props.isFetching && props.ownedTokens.length > 0 && (
|
||||
<Container
|
||||
px="xs"
|
||||
py="xs"
|
||||
flex="row"
|
||||
gap="xs"
|
||||
center="y"
|
||||
color="secondaryText"
|
||||
style={{
|
||||
marginTop: spacing.sm,
|
||||
}}
|
||||
>
|
||||
<CoinsIcon size="14" />
|
||||
<Text
|
||||
size="sm"
|
||||
color="secondaryText"
|
||||
style={{
|
||||
overflow: "unset",
|
||||
}}
|
||||
>
|
||||
Other Tokens
|
||||
</Text>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{!props.isFetching &&
|
||||
props.otherTokens.map((token) => (
|
||||
<TokenButton
|
||||
key={token.address}
|
||||
token={token}
|
||||
@@ -613,31 +604,19 @@ function TokenSelectionScreen(props: {
|
||||
/>
|
||||
))}
|
||||
|
||||
{activeTab === "all" && props.allTokens.showMore && (
|
||||
{props.showMore && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
props.allTokens.showMore?.();
|
||||
props.showMore?.();
|
||||
}}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{activeTab === "owned" && props.ownedTokens.showMore && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
props.ownedTokens.showMore?.();
|
||||
}}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showNoTokensFound && (
|
||||
{noTokensFound && (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -669,45 +648,3 @@ const LeftContainer = /* @__PURE__ */ StyledDiv((_) => {
|
||||
position: "relative",
|
||||
};
|
||||
});
|
||||
|
||||
function TabButton(props: {
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
label: string;
|
||||
}) {
|
||||
const theme = useCustomTheme();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: "4px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost-solid"
|
||||
onClick={props.onSelect}
|
||||
style={{
|
||||
fontSize: fontSize.sm,
|
||||
padding: `10px ${spacing.xs}`,
|
||||
color: props.isSelected
|
||||
? theme.colors.primaryText
|
||||
: theme.colors.secondaryText,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</Button>
|
||||
|
||||
{props.isSelected && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: "-1.5px",
|
||||
borderBottom: `2px solid ${theme.colors.primaryText}`,
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { tokens } from "../../../../../bridge/Token.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import { isAddress } from "../../../../../utils/address.js";
|
||||
import { getThirdwebBaseUrl } from "../../../../../utils/domains.js";
|
||||
import { getClientFetch } from "../../../../../utils/fetch.js";
|
||||
|
||||
export function useTokens(options: {
|
||||
client: ThirdwebClient;
|
||||
@@ -72,7 +73,7 @@ type TokenBalancesResponse = {
|
||||
};
|
||||
|
||||
export function useTokenBalances(options: {
|
||||
clientId: string;
|
||||
client: ThirdwebClient;
|
||||
page: number;
|
||||
limit: number;
|
||||
walletAddress: string | undefined;
|
||||
@@ -101,11 +102,8 @@ export function useTokenBalances(options: {
|
||||
url.searchParams.set("sortOrder", "desc");
|
||||
url.searchParams.set("includeWithoutPrice", "false"); // filter out tokens with no price
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"x-client-id": options.clientId,
|
||||
},
|
||||
});
|
||||
const clientFetch = getClientFetch(options.client);
|
||||
const response = await clientFetch(url.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
|
||||
@@ -37,7 +37,8 @@ export type PaymentMethod =
|
||||
payerWallet: Wallet;
|
||||
originToken: TokenWithPrices;
|
||||
balance: bigint;
|
||||
quote: Quote;
|
||||
quote?: Quote;
|
||||
hasEnoughBalance: boolean;
|
||||
}
|
||||
| {
|
||||
type: "fiat";
|
||||
|
||||
@@ -75,6 +75,7 @@ const meta: Meta<typeof SuccessScreen> = {
|
||||
windowAdapter: webWindowAdapter,
|
||||
client: storyClient,
|
||||
hasPaymentId: false,
|
||||
type: "payment-success",
|
||||
},
|
||||
component: SuccessScreen,
|
||||
decorators: [
|
||||
@@ -94,6 +95,12 @@ export const Basic: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const BasicSwapSuccess: Story = {
|
||||
args: {
|
||||
type: "swap-success",
|
||||
},
|
||||
};
|
||||
|
||||
export const OnrampPayment: Story = {
|
||||
args: {
|
||||
completedStatuses: mockOnrampCompletedStatuses,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next": "15.3.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"prettier": "3.6.2",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
@@ -50,8 +50,8 @@
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
"@storybook/nextjs": "9.0.15",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "8.5.6",
|
||||
"storybook": "9.0.15",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.197
|
||||
|
||||
## 0.2.196
|
||||
|
||||
## 0.2.195
|
||||
|
||||
## 0.2.194
|
||||
|
||||
## 0.2.193
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.194"
|
||||
"version": "0.2.197"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user