Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c768a0ef9 | ||
|
|
a388439022 | ||
|
|
7b873d38db | ||
|
|
d33a943176 | ||
|
|
d5e0b6eb73 | ||
|
|
75e9be0358 | ||
|
|
a704c3f2b0 | ||
|
|
b0eccf365a | ||
|
|
f3525c7c01 | ||
|
|
e516b4ddc1 | ||
|
|
dffa457a39 | ||
|
|
859032dc17 |
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",
|
||||
];
|
||||
|
||||
@@ -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" | "none";
|
||||
}) {
|
||||
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>
|
||||
{type !== "none" && <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} />
|
||||
|
||||
@@ -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";
|
||||
|
||||
+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>
|
||||
))}
|
||||
|
||||
+11
@@ -1,5 +1,6 @@
|
||||
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: {
|
||||
@@ -32,6 +33,16 @@ async function AsyncAiAnalytics(props: {
|
||||
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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+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`}
|
||||
/>
|
||||
);
|
||||
|
||||
+21
-2
@@ -1,6 +1,10 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
import { getUniversalBridgeUsage } from "@/api/analytics";
|
||||
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
|
||||
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";
|
||||
@@ -76,7 +80,22 @@ function BridgeChartCardUI(props: {
|
||||
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/bridge`}
|
||||
/>
|
||||
<div className="p-6 grow flex items-center justify-center">
|
||||
<TotalVolumePieChart data={props.data} hideTotal={true} />
|
||||
{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>
|
||||
);
|
||||
|
||||
+8
-5
@@ -1,7 +1,7 @@
|
||||
"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";
|
||||
|
||||
@@ -39,10 +39,13 @@ export function ProjectHighlightsCard(props: {
|
||||
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,
|
||||
|
||||
+11
@@ -1,5 +1,6 @@
|
||||
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: {
|
||||
@@ -63,6 +64,16 @@ async function AsyncIndexerRequestsChartCard(props: {
|
||||
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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-1
@@ -1,5 +1,6 @@
|
||||
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: {
|
||||
@@ -23,11 +24,23 @@ async function AsyncRPCRequestsChartCard(props: {
|
||||
props.authToken,
|
||||
).catch(() => undefined);
|
||||
|
||||
const isEmpty = requestsData?.every((d) => d.count === 0);
|
||||
|
||||
return (
|
||||
<RPCRequestsChartUI
|
||||
isPending={false}
|
||||
data={requestsData || []}
|
||||
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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+11
@@ -1,5 +1,6 @@
|
||||
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: {
|
||||
@@ -41,6 +42,16 @@ async function AsyncX402RequestsChart(props: {
|
||||
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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+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
-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",
|
||||
}}
|
||||
/>
|
||||
|
||||
+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}
|
||||
|
||||
@@ -82,7 +82,7 @@ export function PieChartCard({
|
||||
{isPending ? (
|
||||
<LoadingChartState />
|
||||
) : isAllEmpty ? (
|
||||
<EmptyChartState type="none"> {emptyChartState} </EmptyChartState>
|
||||
<EmptyChartState content={emptyChartState} />
|
||||
) : (
|
||||
<PieChart
|
||||
data={processedData}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,21 @@
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.40",
|
||||
"version": "0.0.42",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @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
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.98"
|
||||
"version": "0.2.100"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# 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
|
||||
|
||||
@@ -430,5 +430,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.115.1"
|
||||
"version": "5.115.3"
|
||||
}
|
||||
|
||||
@@ -723,6 +723,7 @@ function BridgeWidgetContent(
|
||||
if (screen.id === "6:success") {
|
||||
return (
|
||||
<SuccessScreen
|
||||
type="payment-success"
|
||||
// from props
|
||||
client={props.client}
|
||||
hasPaymentId={!!props.paymentLinkId}
|
||||
|
||||
@@ -730,6 +730,7 @@ function CheckoutWidgetContent(
|
||||
if (screen.id === "6:success") {
|
||||
return (
|
||||
<SuccessScreen
|
||||
type="payment-success"
|
||||
// from props
|
||||
client={props.client}
|
||||
hasPaymentId={!!props.paymentLinkId}
|
||||
|
||||
@@ -786,6 +786,7 @@ function TransactionWidgetContent(
|
||||
<SuccessScreen
|
||||
// from props
|
||||
client={props.client}
|
||||
type="payment-success"
|
||||
hasPaymentId={!!props.paymentLinkId}
|
||||
// others
|
||||
completedStatuses={screen.completedStatuses}
|
||||
|
||||
@@ -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,6 +60,7 @@ export function SuccessScreen({
|
||||
client,
|
||||
hasPaymentId = false,
|
||||
showContinueWithTx,
|
||||
type,
|
||||
}: SuccessScreenProps) {
|
||||
const theme = useCustomTheme();
|
||||
const [viewState, setViewState] = useState<ViewState>("success");
|
||||
@@ -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,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, 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";
|
||||
@@ -8,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";
|
||||
@@ -16,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";
|
||||
@@ -170,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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -321,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<{
|
||||
@@ -364,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 (
|
||||
@@ -399,19 +390,8 @@ function SwapWidgetContent(
|
||||
amountSelection={amountSelection}
|
||||
setAmountSelection={setAmountSelection}
|
||||
onSwap={(data) => {
|
||||
trackPayEvent({
|
||||
chainId: data.result.intent.originChainId,
|
||||
client: props.client,
|
||||
event: "payment_details",
|
||||
fromToken: data.result.intent.originTokenAddress,
|
||||
toChainId: data.result.intent.destinationChainId,
|
||||
toToken: data.result.intent.destinationTokenAddress,
|
||||
walletAddress:
|
||||
activeWalletInfo?.activeWallet?.getAccount()?.address,
|
||||
walletType: activeWalletInfo?.activeWallet?.id,
|
||||
});
|
||||
setScreen({
|
||||
id: "2:preview",
|
||||
id: "3:execute",
|
||||
buyToken: data.buyToken,
|
||||
sellToken: data.sellToken,
|
||||
sellTokenBalance: data.sellTokenBalance,
|
||||
@@ -425,43 +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,
|
||||
hasEnoughBalance: true,
|
||||
}}
|
||||
preparedQuote={screen.preparedQuote}
|
||||
currency={props.currency}
|
||||
modeInfo={{
|
||||
mode: "fund_wallet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "3:execute") {
|
||||
return (
|
||||
<StepRunner
|
||||
@@ -472,8 +415,7 @@ function SwapWidgetContent(
|
||||
onBack={() => {
|
||||
setScreen({
|
||||
...screen,
|
||||
id: "2:preview",
|
||||
sellTokenBalance: screen.sellTokenBalance,
|
||||
id: "1:swap-ui",
|
||||
});
|
||||
}}
|
||||
onCancel={() => props.onCancel?.(screen.preparedQuote)}
|
||||
@@ -498,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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.197
|
||||
|
||||
## 0.2.196
|
||||
|
||||
## 0.2.195
|
||||
|
||||
## 0.2.194
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.195"
|
||||
"version": "0.2.197"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user