Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
851fee1c83 | ||
|
|
9983e6ccfd | ||
|
|
73c924b440 | ||
|
|
2976af3a63 | ||
|
|
419fb31601 | ||
|
|
c76fe10dd0 | ||
|
|
c63ef1c4e9 | ||
|
|
c7ff3b1020 | ||
|
|
b06a9184c1 | ||
|
|
5080b4707f | ||
|
|
d6bc43c34d | ||
|
|
ef577028fd | ||
|
|
304700f318 | ||
|
|
6cd681c9ce | ||
|
|
144bb33ffd | ||
|
|
01116bb650 | ||
|
|
f07b4b56c5 | ||
|
|
9442dc7729 | ||
|
|
6c4e684ee6 | ||
|
|
68978711f8 | ||
|
|
eca4ce2b82 | ||
|
|
80d5595e7f | ||
|
|
d0bbbfd39d | ||
|
|
e26d81ce1d | ||
|
|
be3316ec1b | ||
|
|
872369770f | ||
|
|
3d48826928 | ||
|
|
fa640ad64b | ||
|
|
eb1a9c3c3e | ||
|
|
a54c1aba35 | ||
|
|
bea4f01742 | ||
|
|
e19f7a21ad | ||
|
|
c37af4f191 | ||
|
|
f964b8aaff | ||
|
|
424e6b8f9b | ||
|
|
763d3de5a8 | ||
|
|
f6f138ccbc | ||
|
|
f5d5e73e57 | ||
|
|
673e51b11c | ||
|
|
282edf7ccb | ||
|
|
a87e91b99c | ||
|
|
0e630c4550 | ||
|
|
11c7bf0955 |
@@ -18,7 +18,7 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
|
||||
2. Formatting & Linting
|
||||
|
||||
- Biome governs formatting and linting; its rules live in biome.json.
|
||||
- Run pnpm biome check --apply before committing.
|
||||
- Run `pnpm fix` & `pnpm lint` before committing, make sure there are no linting errors.
|
||||
- Avoid editor‑specific configs; rely on the shared settings.
|
||||
- make sure everything builds after each file change by running `pnpm build`
|
||||
|
||||
@@ -136,7 +136,7 @@ export function reportContractDeployed(properties: {
|
||||
}
|
||||
```
|
||||
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Housekeeping**: Inform **#eng-core-services** before renaming or removing an existing event.
|
||||
|
||||
⸻
|
||||
|
||||
@@ -21,6 +21,7 @@ module.exports = [
|
||||
"/rpc",
|
||||
"/insight",
|
||||
"/storage",
|
||||
"/gateway",
|
||||
// -- end scale category
|
||||
|
||||
// -- ai
|
||||
|
||||
@@ -115,6 +115,16 @@ const projectPageRedirects = [
|
||||
permanent: false,
|
||||
source: `${projectRoute}/connect/analytics`,
|
||||
},
|
||||
{
|
||||
destination: `${projectRoute}/gateway/indexer/:path*`,
|
||||
permanent: false,
|
||||
source: `${projectRoute}/insight/:path*`,
|
||||
},
|
||||
{
|
||||
destination: `${projectRoute}/gateway/rpc/:path*`,
|
||||
permanent: false,
|
||||
source: `${projectRoute}/rpc/:path*`,
|
||||
},
|
||||
];
|
||||
|
||||
const teamPageRedirects = [
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
WalletStats,
|
||||
WebhookLatencyStats,
|
||||
WebhookSummaryStats,
|
||||
X402QueryParams,
|
||||
X402SettlementStats,
|
||||
} from "@/types/analytics";
|
||||
import { getChains } from "./chain";
|
||||
|
||||
@@ -184,7 +186,7 @@ const cached_getInAppWalletUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch in-app wallet usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch user wallets usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -906,3 +908,43 @@ export function getInsightUsage(
|
||||
) {
|
||||
return cached_getInsightUsage(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getX402Settlements = unstable_cache(
|
||||
async (
|
||||
params: X402QueryParams,
|
||||
authToken: string,
|
||||
): Promise<X402SettlementStats[]> => {
|
||||
const searchParams = buildSearchParams(params);
|
||||
|
||||
if (params.groupBy) {
|
||||
searchParams.append("groupBy", params.groupBy);
|
||||
}
|
||||
|
||||
const res = await fetchAnalytics({
|
||||
authToken,
|
||||
url: `v2/x402/settlements?${searchParams.toString()}`,
|
||||
init: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch x402 settlements: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as X402SettlementStats[];
|
||||
},
|
||||
["getX402Settlements"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export function getX402Settlements(params: X402QueryParams, authToken: string) {
|
||||
return cached_getX402Settlements(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ type ShadcnSidebarBaseLink = {
|
||||
isActive?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type ShadcnSidebarLink =
|
||||
export type ShadcnSidebarLink =
|
||||
| ShadcnSidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: ShadcnSidebarBaseLink[];
|
||||
links: ShadcnSidebarLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
@@ -67,7 +67,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar className="pt-2" collapsible="icon" side="left">
|
||||
<Sidebar collapsible="icon" side="left">
|
||||
<SidebarContent className="p-2">
|
||||
<RenderSidebarMenu links={contentSidebarLinks} />
|
||||
</SidebarContent>
|
||||
@@ -97,10 +97,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
|
||||
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
const activeLink = useActiveShadcnSidebarLink(props.links);
|
||||
const parentSubNav = props.links.find(
|
||||
(link) =>
|
||||
"subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
|
||||
);
|
||||
const parentSubNav = findParentSubmenu(props.links, activeLink?.href);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
@@ -109,7 +106,7 @@ function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
className="h-4 bg-muted-foreground/50"
|
||||
orientation="vertical"
|
||||
/>
|
||||
{parentSubNav && "subMenu" in parentSubNav && (
|
||||
{parentSubNav && (
|
||||
<>
|
||||
<span className="text-sm">{parentSubNav.subMenu.label}</span>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
|
||||
@@ -131,24 +128,65 @@ function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("links" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
function walk(
|
||||
navLinks: ShadcnSidebarLink[],
|
||||
): ShadcnSidebarBaseLink | undefined {
|
||||
for (const link of navLinks) {
|
||||
if ("subMenu" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
|
||||
if ("links" in link && !("subMenu" in link)) {
|
||||
const nested = walk(link.links);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return walk(links);
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
function findParentSubmenu(
|
||||
links: ShadcnSidebarLink[],
|
||||
activeHref: string | undefined,
|
||||
): Extract<ShadcnSidebarLink, { subMenu: unknown }> | undefined {
|
||||
if (!activeHref) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("subMenu" in link) {
|
||||
if (link.links.some((subLink) => subLink.href === activeHref)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
if ("links" in link && !("subMenu" in link)) {
|
||||
const nested = findParentSubmenu(link.links, activeHref);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
|
||||
@@ -66,13 +66,16 @@ export type ProjectPageHeaderProps = {
|
||||
href: string;
|
||||
};
|
||||
|
||||
className?: string;
|
||||
// TODO: add task card component
|
||||
task?: never;
|
||||
};
|
||||
|
||||
export function ProjectPageHeader(props: ProjectPageHeaderProps) {
|
||||
return (
|
||||
<header className="container max-w-7xl py-6 relative">
|
||||
<header
|
||||
className={cn("container max-w-7xl py-6 relative", props.className)}
|
||||
>
|
||||
{/* top row */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
{/* left - icon */}
|
||||
|
||||
@@ -12,18 +12,20 @@ import {
|
||||
type ProjectPageProps = {
|
||||
header: ProjectPageHeaderProps;
|
||||
footer?: ProjectPageFooterProps;
|
||||
/**
|
||||
* @deprecated only for legacy pages where we still need internal tabs for the moment, currently:
|
||||
* - /webhooks
|
||||
*/
|
||||
tabs?: TabPathLink[];
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
|
||||
return (
|
||||
<div className="flex flex-col grow">
|
||||
<div className={cn(!props.tabs && "border-b")}>
|
||||
<ProjectPageHeader {...props.header} />
|
||||
<ProjectPageHeader
|
||||
{...props.header}
|
||||
className={cn(
|
||||
props.tabs && props.tabs.length > 0 ? "pt-8 pb-6" : "py-8",
|
||||
)}
|
||||
/>
|
||||
{props.tabs && (
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
@@ -32,7 +34,11 @@ export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<main className="container max-w-7xl pt-6">{props.children}</main>
|
||||
<main
|
||||
className={cn("container max-w-7xl pt-6", props.containerClassName)}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
<div className="h-20" />
|
||||
{props.footer && (
|
||||
<div className="border-t mt-auto">
|
||||
|
||||
+1
-1
@@ -337,7 +337,7 @@ export function InAppWalletUsersPageContent(
|
||||
isFetched={walletsQuery.isFetched}
|
||||
isPending={walletsQuery.isPending}
|
||||
tableContainerClassName="rounded-none border-x-0 border-b-0"
|
||||
title="in-app wallets"
|
||||
title="User wallets"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center gap-3 border-t bg-card p-6">
|
||||
|
||||
@@ -5,7 +5,6 @@ import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: banner is toggled on-demand via API content changes
|
||||
function AnnouncementBannerUI(props: {
|
||||
href: string;
|
||||
label: string;
|
||||
@@ -45,12 +44,11 @@ function AnnouncementBannerUI(props: {
|
||||
}
|
||||
|
||||
export function AnnouncementBanner() {
|
||||
// return (
|
||||
// <AnnouncementBannerUI
|
||||
// href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
|
||||
// label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
|
||||
// trackingLabel="product-rebrand"
|
||||
// />
|
||||
// );
|
||||
return null;
|
||||
return (
|
||||
<AnnouncementBannerUI
|
||||
href="https://blog.thirdweb.com/changelog/project-view-update-wallets/"
|
||||
label="Transactions, Vault, and Account Abstraction are now part of Wallets in Project Dashboard. View changes"
|
||||
trackingLabel="project-view-update-wallets"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function LinkWithCopyButton(props: {
|
||||
textToCopy: string;
|
||||
copyTooltip: string;
|
||||
className?: string;
|
||||
linkIconClassName?: string;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||
@@ -37,8 +38,15 @@ export function LinkWithCopyButton(props: {
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:underline flex items-center gap-1 tabular-nums flex-1 truncate hover:text-foreground group"
|
||||
>
|
||||
<span className="max-w-full truncate">{props.textToShow}</span>
|
||||
<ArrowUpRightIcon className="size-3.5 opacity-70 shrink-0 group-hover:opacity-100" />
|
||||
<span className="max-w-full truncate tabular-nums">
|
||||
{props.textToShow}
|
||||
</span>
|
||||
<ArrowUpRightIcon
|
||||
className={cn(
|
||||
"size-3.5 opacity-70 shrink-0 group-hover:opacity-100",
|
||||
props.linkIconClassName,
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const siwaExamplePrompts = [
|
||||
"How do I add in-app wallet with sign in with google to my react app?",
|
||||
"How do I add user wallet with sign in with google to my react app?",
|
||||
"How do I send a transaction in Unity?",
|
||||
"What does this contract revert error mean?",
|
||||
"I see thirdweb support id in my console log, can you help me?",
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export function ConnectSDKIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 8.00007V4.50073C16 3.66899 16 3.25312 15.8248 2.99755C15.6717 2.77425 15.4346 2.62257 15.1678 2.57715C14.8623 2.52517 14.4847 2.69944 13.7295 3.04799L4.85901 7.14206C4.18551 7.45291 3.84875 7.60834 3.60211 7.84939C3.38406 8.06249 3.21762 8.32262 3.1155 8.6099C3 8.93486 3 9.30575 3 10.0475V15.0001M16.5 14.5001H16.51M3 11.2001L3 17.8001C3 18.9202 3 19.4802 3.21799 19.9081C3.40973 20.2844 3.71569 20.5903 4.09202 20.7821C4.51984 21.0001 5.07989 21.0001 6.2 21.0001H17.8C18.9201 21.0001 19.4802 21.0001 19.908 20.7821C20.2843 20.5903 20.5903 20.2844 20.782 19.9081C21 19.4802 21 18.9202 21 17.8001V11.2001C21 10.08 21 9.51992 20.782 9.09209C20.5903 8.71577 20.2843 8.40981 19.908 8.21806C19.4802 8.00007 18.9201 8.00007 17.8 8.00007L6.2 8.00007C5.0799 8.00007 4.51984 8.00007 4.09202 8.21806C3.7157 8.40981 3.40973 8.71577 3.21799 9.09209C3 9.51991 3 10.08 3 11.2001ZM17 14.5001C17 14.7762 16.7761 15.0001 16.5 15.0001C16.2239 15.0001 16 14.7762 16 14.5001C16 14.2239 16.2239 14.0001 16.5 14.0001C16.7761 14.0001 17 14.2239 17 14.5001Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export function NebulaIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 153 153"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M76.3162 90.8232C82.4613 90.8232 87.4429 85.8416 87.4429 79.6965C87.4429 73.5514 82.4613 68.5698 76.3162 68.5698C70.171 68.5698 65.1895 73.5514 65.1895 79.6965C65.1895 85.8416 70.171 90.8232 76.3162 90.8232Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M91.9481 29.4892C78.7023 32.4916 63.6745 40.7425 50.4663 53.9508C46.781 57.636 43.4817 61.4629 40.5746 65.3532C44.5585 68.8492 47.0735 73.9786 47.0735 79.6955C47.0735 90.2299 38.5336 98.7698 27.9992 98.7698C27.1107 98.7698 26.2364 98.7091 25.3803 98.5915C23.3955 110.414 25.9579 119.384 31.3537 124.78C37.2202 130.646 47.3123 133.164 60.701 130.129C73.9468 127.126 88.9745 118.875 102.183 105.667C105.888 101.962 109.204 98.1131 112.122 94.2007C108.103 90.7037 105.562 85.551 105.562 79.8048C105.562 69.2704 114.102 60.7305 124.636 60.7305C125.536 60.7305 126.421 60.7928 127.288 60.9133C129.237 49.147 126.674 40.2167 121.295 34.8381C115.429 28.9716 105.337 26.4545 91.9481 29.4892ZM16.3791 94.8229C13.4047 109.493 15.9004 122.818 24.608 131.526C42.6147 149.532 80.3662 140.975 108.928 112.413C113.381 107.96 117.348 103.284 120.803 98.494C122.042 98.7466 123.324 98.8792 124.636 98.8792C135.171 98.8792 143.711 90.3393 143.711 79.8048C143.711 73.6582 140.803 68.1906 136.289 64.7023C139.234 50.0687 136.73 36.7817 128.041 28.0925C110.034 10.0859 72.2829 18.6429 43.7206 47.2051C39.2988 51.627 35.3564 56.2691 31.9179 61.024C30.6531 60.7599 29.3423 60.6211 27.9992 60.6211C17.4647 60.6211 8.9248 69.161 8.9248 79.6955C8.9248 85.8567 11.8461 91.3357 16.3791 94.8229ZM124.636 70.2677C119.369 70.2677 115.099 74.5376 115.099 79.8048C115.099 85.0721 119.369 89.342 124.636 89.342C129.904 89.342 134.174 85.0721 134.174 79.8048C134.174 74.5376 129.904 70.2677 124.636 70.2677ZM18.462 79.6955C18.462 74.4282 22.7319 70.1583 27.9992 70.1583C33.2664 70.1583 37.5363 74.4282 37.5363 79.6955C37.5363 84.9627 33.2664 89.2326 27.9992 89.2326C22.7319 89.2326 18.462 84.9627 18.462 79.6955Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function ExpoIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 -12.5 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
className={props.className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M121.309004,84.6732585 C123.402504,81.5874152 125.694292,81.1950171 127.553451,81.1950171 C129.41261,81.1950171 132.509843,81.5874152 134.604162,84.6732585 C151.106348,107.339593 178.345607,152.492 198.439108,185.798721 C211.542532,207.519499 221.6069,224.201947 223.671721,226.324944 C231.422996,234.294992 242.053551,229.327949 248.230809,220.287799 C254.312201,211.387762 256.000111,205.138399 256.000111,198.471155 C256.000111,193.930186 167.895315,30.0714244 159.022317,16.4322117 C150.48936,3.31359639 147.710044,0 133.105527,0 L122.176721,0 C107.615631,0 105.511479,3.31359639 96.9777022,16.4322117 C88.1055238,30.0714244 0.0001105152,193.930186 0.0001105152,198.471155 C0.0001105152,205.138399 1.68839227,211.387762 7.76991495,220.287799 C13.9471241,229.327949 24.5775965,234.294992 32.3286259,226.324944 C34.3936934,224.201947 44.4580605,207.519499 57.5616485,185.798721 C77.654822,152.492 104.806818,107.339593 121.309004,84.6732585 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export function NextjsIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="geist-icon"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
color: "currentColor",
|
||||
}}
|
||||
viewBox="3 3 10 10"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<g clipPath="url(#clip0_53_108)">
|
||||
<path
|
||||
d="M10.63 11V5"
|
||||
stroke="url(#paint0_linear_53_108vsxrmxu21)"
|
||||
strokeWidth={1.25}
|
||||
strokeMiterlimit={Math.SQRT2}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.995 5.00087V5H4.745V11H5.995V6.96798L12.3615 14.7076C12.712 14.4793 13.0434 14.2242 13.353 13.9453L5.99527 5.00065L5.995 5.00087Z"
|
||||
fill="url(#paint1_linear_53_108vsxrmxu21)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_53_108vsxrmxu21"
|
||||
x1={11.13}
|
||||
y1={5}
|
||||
x2={11.13}
|
||||
y2={11}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="currentColor" />
|
||||
<stop offset={0.609375} stopColor="currentColor" stopOpacity={0.57} />
|
||||
<stop offset={0.796875} stopColor="currentColor" stopOpacity={0} />
|
||||
<stop offset={1} stopColor="currentColor" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_53_108vsxrmxu21"
|
||||
x1={9.9375}
|
||||
y1={9.0625}
|
||||
x2={13.5574}
|
||||
y2={13.3992}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="currentColor" />
|
||||
<stop offset={1} stopColor="currentColor" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_53_108">
|
||||
<rect width={16} height={16} fill="red" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export function NodeJSIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M17.1725 2.29872C16.4627 1.89953 15.5373 1.90132 14.8269 2.29872C11.2689 4.26227 7.71082 6.22641 4.15216 8.18906C3.45969 8.55335 2.99264 9.29698 3.00009 10.0688V21.9328C2.99509 22.7197 3.48622 23.4705 4.19655 23.8298C5.21871 24.3736 6.2118 24.9726 7.25244 25.4802C8.45451 26.0709 9.95843 26.2015 11.1752 25.5855C12.1629 25.075 12.6016 23.9395 12.6003 22.896C12.6083 18.9806 12.6016 15.0651 12.6034 11.1496C12.6269 10.9756 12.4962 10.7896 12.3064 10.7938C11.8517 10.7866 11.3964 10.7896 10.9417 10.7926C10.7699 10.7764 10.6022 10.9191 10.6152 11.0918C10.6091 14.982 10.6164 18.8734 10.6115 22.7642C10.6214 23.3024 10.2578 23.8196 9.73913 24.0014C8.5412 24.4213 5.12198 22.2012 5.12198 22.2012C4.9965 22.1431 4.91682 22.007 4.92912 21.8718C4.92912 17.9576 4.92973 14.0433 4.92912 10.1297C4.91187 9.97191 5.00912 9.8298 5.15402 9.76538C8.70033 7.8134 12.2448 5.85654 15.7911 3.90336C15.9143 3.82115 16.086 3.8214 16.2089 3.90396C19.7552 5.85654 23.3003 7.81161 26.8472 9.76368C26.9926 9.828 27.0857 9.9725 27.0709 10.1297C27.0703 14.0433 27.0721 17.9576 27.0697 21.8713C27.0802 22.0098 27.0086 22.144 26.8793 22.2048C23.3661 24.1462 19.8129 26.025 16.3315 28.0228C16.1796 28.1099 16.0075 28.2086 15.8373 28.1126C14.9218 27.6062 14.0174 27.0801 13.1049 26.5688C13.0057 26.5069 12.8794 26.4803 12.7759 26.5496C12.3668 26.7652 11.982 26.9398 11.5122 27.1258C10.8524 27.387 10.9578 27.4938 11.5529 27.8405C12.62 28.4444 13.6889 29.0459 14.756 29.6504C15.4585 30.0888 16.4024 30.12 17.1275 29.7149C20.6861 27.7538 24.2436 25.7904 27.8029 23.8293C28.5113 23.468 29.0049 22.7202 28.9999 21.9327V10.0688C29.0068 9.31264 28.5576 8.58227 27.886 8.21259C24.3156 6.23947 20.7435 4.27064 17.1725 2.29872Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M22.5419 11.2062C21.1452 10.459 19.4836 10.4192 17.9315 10.5169C16.8102 10.6277 15.6309 10.9371 14.814 11.7409C13.9761 12.5489 13.7937 13.8537 14.1917 14.9085C14.4769 15.6539 15.1948 16.1386 15.9372 16.395C16.8935 16.7326 17.8979 16.837 18.9026 16.9414C19.819 17.0366 20.7357 17.1319 21.6165 17.4042C21.9763 17.5234 22.3953 17.7058 22.5055 18.0973C22.6073 18.5609 22.4957 19.0998 22.1193 19.4219C20.9237 20.3682 17.5979 20.2232 16.4166 19.4784C15.939 19.1611 15.7332 18.5994 15.6495 18.0641C15.6402 17.8973 15.5059 17.7443 15.3248 17.757C14.8713 17.7516 14.4178 17.7528 13.9643 17.7564C13.8061 17.7431 13.6416 17.8557 13.6329 18.0172C13.5397 20.4689 15.7914 21.5377 17.9039 21.773C19.1108 21.888 20.3442 21.8814 21.5327 21.6224C22.4261 21.419 23.3219 21.0444 23.9369 20.3563C24.6953 19.52 24.8444 18.2749 24.5043 17.2332C24.2443 16.4559 23.5012 15.9573 22.7416 15.7008C21.7086 15.3466 20.4844 15.1562 19.5488 15.0671C18.1889 14.9376 16.5729 14.9905 16.188 14.0969C16.0345 13.629 16.1651 13.048 16.5951 12.7602C17.7328 11.9885 20.0483 12.091 21.2265 12.6675C21.7675 12.9384 22.081 13.4948 22.2104 14.0565C22.2344 14.2215 22.3454 14.3937 22.5364 14.3865C22.9868 14.3955 23.4372 14.3889 23.8875 14.3895C24.0422 14.4003 24.2116 14.313 24.2418 14.1546C24.2227 12.9806 23.6232 11.7788 22.5419 11.2062Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export function ViteIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="-0.5 0 257 257"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M255.152904,37.937763 L134.896865,252.97646 C132.413943,257.416178 126.035075,257.442321 123.5149,253.02417 L0.87443175,37.9584812 C-1.87111536,33.1438084 2.24595371,27.3119153 7.70191187,28.2871109 L128.086639,49.8052023 C128.854587,49.9424525 129.640835,49.9411454 130.408783,49.8012155 L248.276014,28.3179595 C253.713738,27.3268821 257.850198,33.1136134 255.152904,37.937763 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M185.432401,0.0631038902 L96.4393502,17.500942 C94.9766549,17.7875335 93.8936852,19.0270992 93.8054529,20.5146956 L88.3311293,112.971419 C88.2023755,115.149123 90.2023075,116.839261 92.3277254,116.349082 L117.10466,110.630976 C119.422882,110.096354 121.517582,112.138114 121.041128,114.469407 L113.67994,150.515893 C113.184532,152.941955 115.462232,155.016394 117.831433,154.29681 L133.134834,149.647295 C135.507302,148.927059 137.786963,151.00738 137.285019,153.435402 L125.586724,210.056351 C124.854723,213.598061 129.565674,215.529368 131.530313,212.49287 L132.842687,210.464834 L205.359174,65.745575 C206.573511,63.3224548 204.479465,60.5594769 201.818118,61.0730542 L176.31441,65.9952397 C173.91776,66.4573155 171.878614,64.2253653 172.555061,61.8805431 L189.2009,4.17570253 C189.878001,1.82692623 187.831665,-0.406957894 185.432401,0.0631038902 Z"
|
||||
fill="hsl(var(--background))"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -248,7 +248,7 @@ export function teamSubscriptionsStub(
|
||||
// In-App Wallets
|
||||
{
|
||||
amount: usage.inAppWalletAmount?.amount || 0,
|
||||
description: `${usage.inAppWalletAmount?.quantity || 0} x In-App Wallets (Tier 1 at $0.00 / month)`,
|
||||
description: `${usage.inAppWalletAmount?.quantity || 0} x User Wallets (Tier 1 at $0.00 / month)`,
|
||||
thirdwebSku: "usage:in_app_wallet",
|
||||
},
|
||||
// AA Sponsorship
|
||||
|
||||
@@ -94,3 +94,62 @@ export interface AnalyticsQueryParams {
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsOverall {
|
||||
date: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByChainId {
|
||||
date: string;
|
||||
chainId: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByPayer {
|
||||
date: string;
|
||||
payer: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
interface X402SettlementsByReceiver {
|
||||
date: string;
|
||||
receiver: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByResource {
|
||||
date: string;
|
||||
resource: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
interface X402SettlementsByAsset {
|
||||
date: string;
|
||||
asset: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export type X402SettlementStats =
|
||||
| X402SettlementsOverall
|
||||
| X402SettlementsByChainId
|
||||
| X402SettlementsByPayer
|
||||
| X402SettlementsByReceiver
|
||||
| X402SettlementsByResource
|
||||
| X402SettlementsByAsset;
|
||||
|
||||
export interface X402QueryParams extends AnalyticsQueryParams {
|
||||
groupBy?: "overall" | "chainId" | "payer" | "resource" | "asset" | "receiver";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const usdCurrencyFormatter = new Intl.NumberFormat("en-US", {
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2, // prefix with $
|
||||
maximumFractionDigits: 6, // prefix with $
|
||||
minimumFractionDigits: 0, // don't show decimal places if value is a whole number
|
||||
notation: "compact", // at max 2 decimal places
|
||||
roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations
|
||||
|
||||
@@ -23,7 +23,7 @@ export const TEAM_PLANS: Record<
|
||||
features: [
|
||||
"Email Support",
|
||||
"48hr Guaranteed Response",
|
||||
"Custom In-App Wallet Auth",
|
||||
"Custom User Wallet Auth",
|
||||
],
|
||||
price: 99,
|
||||
subTitle: "Everything in Starter, plus:",
|
||||
|
||||
+96
-39
@@ -1,55 +1,112 @@
|
||||
import { BotIcon, CodeIcon, CoinsIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadataWithServices } from "@/types/chain";
|
||||
import { products } from "../../../../components/server/products";
|
||||
import { useMemo } from "react";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import type {
|
||||
ChainMetadataWithServices,
|
||||
ChainSupportedService,
|
||||
} from "@/types/chain";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function SupportedProductsSection(props: {
|
||||
services: ChainMetadataWithServices["services"];
|
||||
}) {
|
||||
const enabledProducts = products.filter((product) => {
|
||||
return props.services.find(
|
||||
(service) => service.service === product.id && service.enabled,
|
||||
const enabledServices = useMemo(() => {
|
||||
return props.services.reduce(
|
||||
(acc, service) => {
|
||||
acc[service.service] = service.enabled;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<ChainSupportedService, boolean>,
|
||||
);
|
||||
});
|
||||
|
||||
if (enabledProducts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
}, [props.services]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="thirdweb Products" />
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{enabledProducts.map((product) => {
|
||||
return (
|
||||
<div
|
||||
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
|
||||
key={product.id}
|
||||
>
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<product.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1">
|
||||
<Link
|
||||
className="before:absolute before:inset-0 text-base font-medium"
|
||||
href={product.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{product.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{enabledServices["connect-sdk"] && (
|
||||
<ProductCard
|
||||
icon={WalletProductIcon}
|
||||
name="Wallets"
|
||||
description="Create wallets to read and transact"
|
||||
link="https://thirdweb.com/wallets"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enabledServices["account-abstraction"] && (
|
||||
<ProductCard
|
||||
icon={PayIcon}
|
||||
name="x402"
|
||||
description="Create internet native payments with x402"
|
||||
link="https://thirdweb.com/x402"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enabledServices.pay && (
|
||||
<ProductCard
|
||||
icon={BridgeIcon}
|
||||
name="Bridge"
|
||||
description="Bridge and swap tokens across chains"
|
||||
link="https://thirdweb.com/monetize/bridge"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enabledServices.contracts && (
|
||||
<ProductCard
|
||||
icon={CoinsIcon}
|
||||
name="Tokens"
|
||||
description="Launch tokens and markets"
|
||||
link="https://thirdweb.com/token"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProductCard
|
||||
icon={BotIcon}
|
||||
name="AI"
|
||||
description="Read and write onchain via natural language"
|
||||
link="https://thirdweb.com/ai"
|
||||
/>
|
||||
|
||||
<ProductCard
|
||||
icon={CodeIcon}
|
||||
name="HTTP API"
|
||||
description="Build products with our HTTP API"
|
||||
link="https://portal.thirdweb.com/reference"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ProductCard(props: {
|
||||
icon: React.FC<{ className?: string }>;
|
||||
link: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative rounded-xl border bg-card p-4 hover:border-active-border">
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<props.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1">
|
||||
<Link
|
||||
className="before:absolute before:inset-0 text-base font-medium"
|
||||
href={props.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{props.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+16
-9
@@ -225,6 +225,10 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
moduleInfo: {
|
||||
bytecodeUri: selectedModule.metadata.bytecodeUri,
|
||||
},
|
||||
isStylus:
|
||||
(selectedModule.metadata.compilers?.stylus &&
|
||||
selectedModule.metadata.compilers?.stylus?.length > 0) ||
|
||||
false,
|
||||
});
|
||||
},
|
||||
queryKey: [
|
||||
@@ -432,6 +436,7 @@ async function isModuleCompatible(options: {
|
||||
moduleInfo: {
|
||||
bytecodeUri: string;
|
||||
};
|
||||
isStylus: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
// 1. get module's bytecode
|
||||
@@ -444,15 +449,17 @@ async function isModuleCompatible(options: {
|
||||
|
||||
// 2. check compatibility with core and installed modules
|
||||
try {
|
||||
const isCompatible = await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
});
|
||||
const isCompatible =
|
||||
options.isStylus ||
|
||||
(await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
}));
|
||||
|
||||
return isCompatible;
|
||||
} catch (e) {
|
||||
|
||||
+10
-4
@@ -10,7 +10,11 @@ import {
|
||||
CircleXIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import type { Status, Token } from "thirdweb/bridge";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import { toTokens } from "thirdweb/utils";
|
||||
@@ -123,6 +127,8 @@ function TokenInfo(props: {
|
||||
const isNativeToken =
|
||||
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
|
||||
|
||||
const tokenAddress = getAddress(props.token.address);
|
||||
|
||||
return (
|
||||
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -206,7 +212,7 @@ function TokenInfo(props: {
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">{props.addressLabel}</p>
|
||||
<WalletAddress
|
||||
address={props.walletAddress}
|
||||
address={getAddress(props.walletAddress)}
|
||||
client={props.client}
|
||||
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
|
||||
iconClassName="size-3"
|
||||
@@ -222,8 +228,8 @@ function TokenInfo(props: {
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
textToShow={props.token.address}
|
||||
textToCopy={props.token.address}
|
||||
textToShow={tokenAddress}
|
||||
textToCopy={tokenAddress}
|
||||
copyTooltip="Copy Token Address"
|
||||
className="-translate-x-1"
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Clock4Icon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import { toTokens } from "thirdweb";
|
||||
import { getAddress, toTokens } from "thirdweb";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
@@ -153,6 +153,9 @@ function GeneericTxDetails(props: {
|
||||
|
||||
const timestamp = getDatefromTimestamp(block.timestamp);
|
||||
|
||||
const fromAddress = getAddress(transaction.from);
|
||||
const toAddress = transaction.to ? getAddress(transaction.to) : undefined;
|
||||
|
||||
return (
|
||||
<div className="border rounded-xl p-4 lg:p-6 bg-card">
|
||||
{/* section 1 */}
|
||||
@@ -216,8 +219,8 @@ function GeneericTxDetails(props: {
|
||||
<section className="border-b py-6 space-y-5 lg:space-y-3">
|
||||
<GridItem label="From" tooltip="The sending party of the transaction.">
|
||||
<CopyTextButton
|
||||
textToCopy={transaction.from}
|
||||
textToShow={transaction.from}
|
||||
textToCopy={fromAddress}
|
||||
textToShow={fromAddress}
|
||||
tooltip="Copy from address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
@@ -225,14 +228,14 @@ function GeneericTxDetails(props: {
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
{transaction.to && (
|
||||
{toAddress && (
|
||||
<GridItem
|
||||
label="Interacted with (To)"
|
||||
tooltip="The receiving party of the transaction (could be a contract address)."
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={transaction.to}
|
||||
textToShow={transaction.to}
|
||||
textToCopy={toAddress}
|
||||
textToShow={toAddress}
|
||||
tooltip="Copy to address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ import {
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { products } from "../../../components/server/products";
|
||||
import { services } from "../../../components/server/products";
|
||||
|
||||
function cleanUrl(url: string) {
|
||||
if (url.endsWith("?")) {
|
||||
@@ -345,7 +345,7 @@ export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
|
||||
|
||||
const section = (
|
||||
<FilterSection title="Services">
|
||||
{products.map((product) => (
|
||||
{services.map((product) => (
|
||||
<div className="group flex items-center gap-2" key={product.id}>
|
||||
<Checkbox
|
||||
checked={isServiceActive(mutableSearchParams, product.id)}
|
||||
@@ -393,7 +393,7 @@ export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
|
||||
}
|
||||
|
||||
const firstFilter = allFilters[0];
|
||||
const name = products.find((p) => p.id === firstFilter)?.name;
|
||||
const name = services.find((p) => p.id === firstFilter)?.name;
|
||||
const plus = allFilters.length > 1 ? ` +${allFilters.length - 1}` : "";
|
||||
|
||||
return [`${name}${plus}`, true];
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { products } from "../../../components/server/products";
|
||||
import { services } from "../../../components/server/products";
|
||||
import { getCustomChainMetadata } from "../../../utils";
|
||||
|
||||
type ChainListRowProps = {
|
||||
@@ -83,7 +83,7 @@ export function ChainListRow({
|
||||
<TableCell>
|
||||
<div className="flex w-[520px] flex-row items-center gap-14 ">
|
||||
<div className="z-10 flex items-center gap-4">
|
||||
{products.map((p) => {
|
||||
{services.map((p) => {
|
||||
return (
|
||||
<ProductIcon
|
||||
href={p.link}
|
||||
|
||||
@@ -1,74 +1,62 @@
|
||||
import { ConnectSDKIcon } from "@/icons/ConnectSDKIcon";
|
||||
import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { EngineIcon } from "@/icons/EngineIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { BotIcon, CoinsIcon, FuelIcon, HardDriveIcon } from "lucide-react";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { RPCIcon } from "@/icons/RPCIcon";
|
||||
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
|
||||
export const products = [
|
||||
export const services = [
|
||||
{
|
||||
description: "Create, deploy and manage smart contracts",
|
||||
icon: ContractIcon,
|
||||
icon: CoinsIcon,
|
||||
id: "contracts",
|
||||
link: "https://thirdweb.com/explore",
|
||||
name: "Contracts",
|
||||
link: "https://thirdweb.com/token",
|
||||
name: "Tokens",
|
||||
},
|
||||
{
|
||||
description: "Create and manage crypto wallets",
|
||||
icon: ConnectSDKIcon,
|
||||
icon: WalletProductIcon,
|
||||
id: "connect-sdk",
|
||||
link: "https://thirdweb.com/connect",
|
||||
name: "Wallets",
|
||||
link: "https://thirdweb.com/wallets",
|
||||
name: "User Wallets",
|
||||
},
|
||||
{
|
||||
description: "Performant and scalable RPC service",
|
||||
icon: RPCIcon,
|
||||
id: "rpc-edge",
|
||||
link: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
|
||||
name: "RPC Edge",
|
||||
link: "https://thirdweb.com/rpc",
|
||||
name: "RPC",
|
||||
},
|
||||
{
|
||||
description: "Backend server that reads, writes, and deploys contracts",
|
||||
icon: EngineIcon,
|
||||
icon: HardDriveIcon,
|
||||
id: "engine",
|
||||
link: "https://thirdweb.com/engine",
|
||||
name: "Transactions",
|
||||
link: "https://thirdweb.com/wallets",
|
||||
name: "Server Wallets",
|
||||
},
|
||||
{
|
||||
description: "Enable gas sponsorship for seamless transactions",
|
||||
icon: SmartAccountIcon,
|
||||
icon: FuelIcon,
|
||||
id: "account-abstraction",
|
||||
link: "https://portal.thirdweb.com/wallets/sponsor-gas",
|
||||
name: "Account Abstraction",
|
||||
link: "https://thirdweb.com/wallets",
|
||||
name: "Gas Sponsorship",
|
||||
},
|
||||
{
|
||||
description: "Enable payments in any token on any chain",
|
||||
icon: PayIcon,
|
||||
icon: BridgeIcon,
|
||||
id: "pay",
|
||||
link: "https://portal.thirdweb.com/payments",
|
||||
name: "Payments",
|
||||
link: "https://thirdweb.com/monetize/bridge",
|
||||
name: "Bridge",
|
||||
},
|
||||
{
|
||||
description: "Query and analyze blockchain data",
|
||||
icon: InsightIcon,
|
||||
id: "insight",
|
||||
link: "https://thirdweb.com/insight",
|
||||
name: "Insight",
|
||||
},
|
||||
{
|
||||
description: "The most powerful AI for interacting with the blockchain",
|
||||
icon: NebulaIcon,
|
||||
icon: BotIcon,
|
||||
id: "nebula",
|
||||
link: "https://thirdweb.com/ai",
|
||||
name: "thirdweb AI",
|
||||
name: "AI",
|
||||
},
|
||||
{
|
||||
icon: PayIcon,
|
||||
id: "account-abstraction",
|
||||
link: "https://thirdweb.com/x402",
|
||||
name: "x402",
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string;
|
||||
id: ChainSupportedService;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
description: string;
|
||||
link: string;
|
||||
}>;
|
||||
|
||||
@@ -183,13 +183,64 @@ const STYLUS = {
|
||||
],
|
||||
description:
|
||||
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
|
||||
displayName: "Arbitrum Stylus Contracts",
|
||||
displayName: "Airdrop - Arbitrum Stylus",
|
||||
id: "stylus",
|
||||
name: "Stylus",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const MODULAR_CONTRACTS_STYLUS = {
|
||||
contracts: [
|
||||
// erc721 token
|
||||
[
|
||||
"thirdweb.eth/ERC721CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC721",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC721",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC721",
|
||||
],
|
||||
{
|
||||
description: "ERC721 NFTs that only owners can mint.",
|
||||
title: "Modular NFT Collection",
|
||||
},
|
||||
],
|
||||
// erc1155 token
|
||||
[
|
||||
"thirdweb.eth/ERC1155CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC1155",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC1155",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC1155",
|
||||
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
|
||||
],
|
||||
{
|
||||
description: "ERC1155 NFTs that only owners can mint.",
|
||||
title: "Modular Edition",
|
||||
},
|
||||
],
|
||||
// erc20 token
|
||||
[
|
||||
"thirdweb.eth/ERC20CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC20",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC20",
|
||||
],
|
||||
{
|
||||
description: "ERC20 Tokens that only owners can mint.",
|
||||
title: "Modular Token",
|
||||
},
|
||||
],
|
||||
],
|
||||
description:
|
||||
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
|
||||
displayName: "Modular Contracts - Arbitrum Stylus",
|
||||
id: "modular-contracts",
|
||||
name: "modular",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[POPULAR.id]: POPULAR,
|
||||
[NFTS.id]: NFTS,
|
||||
@@ -203,6 +254,7 @@ const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[COMMERCE.id]: COMMERCE,
|
||||
[STAKING.id]: STAKING,
|
||||
[GOVERNANCE.id]: GOVERNANCE,
|
||||
[MODULAR_CONTRACTS_STYLUS.id]: MODULAR_CONTRACTS_STYLUS,
|
||||
};
|
||||
|
||||
export function getCategory(id: string) {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function TeamSidebarLayout(props: {
|
||||
label: "Documentation",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
href: "https://playground.thirdweb.com/wallets/sign-in/button",
|
||||
icon: BoxIcon,
|
||||
label: "Playground",
|
||||
},
|
||||
|
||||
+6
-21
@@ -26,14 +26,6 @@ const EngineSupportForm = dynamic(
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const ContractSupportForm = dynamic(
|
||||
() =>
|
||||
import("./contact-forms/contracts").then((mod) => mod.ContractSupportForm),
|
||||
{
|
||||
loading: () => <Skeleton className="h-12" />,
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const AccountSupportForm = dynamic(
|
||||
() => import("./contact-forms/account").then((mod) => mod.AccountSupportForm),
|
||||
{
|
||||
@@ -48,14 +40,6 @@ const OtherSupportForm = dynamic(
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const PaymentsSupportForm = dynamic(
|
||||
() =>
|
||||
import("./contact-forms/payments").then((mod) => mod.PaymentsSupportForm),
|
||||
{
|
||||
loading: () => <Skeleton className="h-12" />,
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const TokensMarketplaceSupportForm = dynamic(
|
||||
() =>
|
||||
import("./contact-forms/tokens-marketplace").then(
|
||||
@@ -68,13 +52,14 @@ const TokensMarketplaceSupportForm = dynamic(
|
||||
);
|
||||
|
||||
const productOptions = [
|
||||
{ component: <ConnectSupportForm />, label: "Wallets" },
|
||||
{ component: <EngineSupportForm />, label: "Transactions" },
|
||||
{ component: <PaymentsSupportForm />, label: "Payments" },
|
||||
{ component: <ContractSupportForm />, label: "Contracts" },
|
||||
{ component: <ConnectSupportForm />, label: "Working with User Wallets" },
|
||||
{
|
||||
component: <EngineSupportForm />,
|
||||
label: "Using API/SDK’s, errored transactions",
|
||||
},
|
||||
{
|
||||
component: <TokensMarketplaceSupportForm />,
|
||||
label: "Tokens / Marketplace",
|
||||
label: "Creating, Bridging and swapping tokens",
|
||||
},
|
||||
{ component: <AccountSupportForm />, label: "Account" },
|
||||
{ component: <OtherSupportForm />, label: "Other" },
|
||||
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
|
||||
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
|
||||
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
|
||||
import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
|
||||
|
||||
const AFFECTED_AREAS = ["Dashboard", "Application"];
|
||||
|
||||
export const AffectedAreaInput = () => {
|
||||
const [selectedAffectedArea, setSelectedAffectedArea] = useState<string>("");
|
||||
const [selectedSDK, setSelectedSDK] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [sdkDescription, setSdkDescription] = useState<string>("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SupportForm_SelectInput
|
||||
formLabel="Affected area"
|
||||
name="extraInfo_Affected_Area"
|
||||
onValueChange={setSelectedAffectedArea}
|
||||
options={AFFECTED_AREAS}
|
||||
promptText="Select an affected area"
|
||||
required={true}
|
||||
value={selectedAffectedArea}
|
||||
/>
|
||||
{selectedAffectedArea &&
|
||||
(selectedAffectedArea === "Application" ? (
|
||||
<>
|
||||
<SupportForm_SelectInput
|
||||
formLabel="SDK"
|
||||
name="extraInfo_SDK"
|
||||
onValueChange={setSelectedSDK}
|
||||
options={["TypeScript", "React", "React Native", "Unity"]}
|
||||
promptText="Select SDK"
|
||||
required={true}
|
||||
value={selectedSDK}
|
||||
/>
|
||||
{selectedSDK && (
|
||||
<>
|
||||
{selectedSDK === "Unity" && <UnitySupportForm />}
|
||||
<SupportForm_TextInput
|
||||
formLabel="SDK Version"
|
||||
formValue="extraInfo_SDK_Version"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Application URL"
|
||||
formValue="extraInfo_Application_URL"
|
||||
inputType="url"
|
||||
required={false}
|
||||
/>
|
||||
<DescriptionInput
|
||||
value={sdkDescription}
|
||||
onChange={setSdkDescription}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+61
-86
@@ -1,116 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactElement, useState } from "react";
|
||||
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
|
||||
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
|
||||
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
|
||||
import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
|
||||
import { AffectedAreaInput } from "./AffectedAreaInput";
|
||||
|
||||
type ProblemAreaItem = {
|
||||
label: string;
|
||||
component: ReactElement;
|
||||
};
|
||||
|
||||
const SDKVersionInput = () => (
|
||||
<SupportForm_TextInput
|
||||
formLabel="SDK Version"
|
||||
formValue="extraInfo_SDK_Version"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const OSSelect = () => {
|
||||
const [selectedOS, setSelectedOS] = useState<string>("");
|
||||
const SocialEmailLoginIssuesComponent = () => {
|
||||
const [description, setDescription] = useState<string>("");
|
||||
return (
|
||||
<SupportForm_SelectInput
|
||||
formLabel="OS"
|
||||
name="extraInfo_OS"
|
||||
onValueChange={setSelectedOS}
|
||||
options={["Windows", "MacOS", "Linux", "Other"]}
|
||||
promptText="Select an operating system"
|
||||
required={true}
|
||||
value={selectedOS}
|
||||
/>
|
||||
<>
|
||||
<SupportForm_TextInput
|
||||
formLabel="User ID/Email with issue"
|
||||
formValue="extraInfo_User_ID_Email"
|
||||
inputType="text"
|
||||
placeholder="Enter user ID or email"
|
||||
required={true}
|
||||
/>
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DescriptionInputWrapper = () => {
|
||||
const UserWalletCustomAuthComponent = () => {
|
||||
const [description, setDescription] = useState<string>("");
|
||||
return (
|
||||
<>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Custom auth endpoint"
|
||||
formValue="extraInfo_Custom_Auth_Endpoint"
|
||||
inputType="text"
|
||||
placeholder="Enter custom auth endpoint"
|
||||
required={true}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Support ID"
|
||||
formValue="extraInfo_Support_ID"
|
||||
inputType="text"
|
||||
placeholder="Enter support ID (optional)"
|
||||
required={false}
|
||||
/>
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountAbstractionComponent = () => {
|
||||
const [description, setDescription] = useState<string>("");
|
||||
return (
|
||||
<>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Support ID"
|
||||
formValue="extraInfo_Support_ID"
|
||||
inputType="text"
|
||||
placeholder="Enter support ID (optional)"
|
||||
required={false}
|
||||
/>
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ThirdPartyEOAWalletsComponent = () => {
|
||||
const [description, setDescription] = useState<string>("");
|
||||
return <DescriptionInput value={description} onChange={setDescription} />;
|
||||
};
|
||||
|
||||
const PROBLEM_AREAS: ProblemAreaItem[] = [
|
||||
{
|
||||
component: <AffectedAreaInput />,
|
||||
label: "In-app wallet login issues",
|
||||
component: <SocialEmailLoginIssuesComponent />,
|
||||
label: "Social/Email login issues",
|
||||
},
|
||||
{
|
||||
component: <AffectedAreaInput />,
|
||||
label: "In-app wallet transaction issues",
|
||||
component: <UserWalletCustomAuthComponent />,
|
||||
label: "User wallet with custom auth",
|
||||
},
|
||||
{
|
||||
component: <AffectedAreaInput />,
|
||||
label: "In-app wallet Custom Auth",
|
||||
},
|
||||
{
|
||||
component: <AffectedAreaInput />,
|
||||
component: <AccountAbstractionComponent />,
|
||||
label: "Account Abstraction",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<SDKVersionInput />
|
||||
<SupportForm_TextInput
|
||||
formLabel="Application URL"
|
||||
formValue="extraInfo_Application_URL"
|
||||
inputType="url"
|
||||
required={false}
|
||||
/>
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "thirdweb SDKs",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<UnitySupportForm />
|
||||
<SDKVersionInput />
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "Unity SDK",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<SDKVersionInput />
|
||||
<OSSelect />
|
||||
<SupportForm_TextInput
|
||||
formLabel="Framework"
|
||||
formValue="extraInfo_dotNET_Framework"
|
||||
inputType="text"
|
||||
placeholder="MAUI | Blazor | Godot, etc"
|
||||
required={false}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Target OS"
|
||||
formValue="extraInfo_dotNET_Target_OS"
|
||||
inputType="text"
|
||||
required={false}
|
||||
/>
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: ".NET SDK",
|
||||
},
|
||||
{
|
||||
component: <AffectedAreaInput />,
|
||||
label: "Pay",
|
||||
},
|
||||
{
|
||||
component: <AffectedAreaInput />,
|
||||
label: "Auth",
|
||||
component: <ThirdPartyEOAWalletsComponent />,
|
||||
label: "Third party/EOA wallets",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
import { type ReactElement, useState } from "react";
|
||||
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
|
||||
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
|
||||
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
|
||||
|
||||
type ProblemAreaItem = {
|
||||
label: string;
|
||||
component: ReactElement;
|
||||
};
|
||||
|
||||
const NetworkInput = () => (
|
||||
<SupportForm_TextInput
|
||||
formLabel="Network"
|
||||
formValue="extraInfo_Network"
|
||||
inputType="text"
|
||||
placeholder="Network name or chain ID"
|
||||
required={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const ContractAddressInput = () => (
|
||||
<SupportForm_TextInput
|
||||
formLabel="Contract address"
|
||||
formValue="extraInfo_Contract_Address"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const ContractFunctionInput = () => (
|
||||
<SupportForm_TextInput
|
||||
formLabel="Contract function"
|
||||
formValue="extraInfo_Contract_Function"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const ContractTypeInput = () => (
|
||||
<SupportForm_TextInput
|
||||
formLabel="Contract type"
|
||||
formValue="extraInfo_Contract_Type"
|
||||
inputType="text"
|
||||
placeholder={`"MarketplaceV3 | "NFTDrop" | ...`}
|
||||
required={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const ContractAffectedAreaInput = () => {
|
||||
const [selectedAffectedArea, setSelectedAffectedArea] = useState<string>("");
|
||||
return (
|
||||
<SupportForm_SelectInput
|
||||
formLabel="Contract affected area"
|
||||
name="extraInfo_Affected_Area"
|
||||
onValueChange={setSelectedAffectedArea}
|
||||
options={["Dashboard", "SDK"]}
|
||||
promptText="Select an affected area"
|
||||
required={true}
|
||||
value={selectedAffectedArea}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DescriptionInputWrapper = () => {
|
||||
const [description, setDescription] = useState<string>("");
|
||||
return <DescriptionInput value={description} onChange={setDescription} />;
|
||||
};
|
||||
|
||||
const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<NetworkInput />
|
||||
<ContractTypeInput />
|
||||
<ContractAffectedAreaInput />
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "Deploying a contract",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<NetworkInput />
|
||||
<ContractAddressInput />
|
||||
<ContractTypeInput />
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "Contract verification",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<NetworkInput />
|
||||
<ContractAddressInput />
|
||||
<ContractFunctionInput />
|
||||
<ContractAffectedAreaInput />
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "Calling a function in my contract",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "Developing a custom contract",
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<>
|
||||
<DescriptionInputWrapper />
|
||||
</>
|
||||
),
|
||||
label: "Other",
|
||||
},
|
||||
];
|
||||
|
||||
export function ContractSupportForm() {
|
||||
const [problemArea, setProblemArea] = useState<string>("");
|
||||
return (
|
||||
<>
|
||||
<SupportForm_SelectInput
|
||||
formLabel="Problem area"
|
||||
name="extraInfo_Problem_Area"
|
||||
onValueChange={setProblemArea}
|
||||
options={CONTRACT_PROBLEM_AREAS.map((o) => o.label)}
|
||||
promptText="Select a problem area"
|
||||
required={true}
|
||||
value={problemArea}
|
||||
/>
|
||||
{CONTRACT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+41
-35
@@ -2,56 +2,62 @@ import { useState } from "react";
|
||||
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
|
||||
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
|
||||
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
|
||||
import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
|
||||
|
||||
const ENGINE_TYPES = ["Cloud (V3)", "Dedicated (V2)"];
|
||||
const ENGINE_PROBLEM_AREAS = [
|
||||
"SSL Issues",
|
||||
"Transaction queueing issues",
|
||||
"401 - Unauthorized",
|
||||
"404 - Endpoint Not Found",
|
||||
"Other",
|
||||
const API_SDK_OPTIONS = [
|
||||
"API endpoint",
|
||||
"React/TypeScript",
|
||||
"Unity/.NET",
|
||||
"React Native",
|
||||
];
|
||||
|
||||
export function EngineSupportForm() {
|
||||
const [selectedEngineType, setSelectedEngineType] = useState<string>("");
|
||||
const [problemArea, setProblemArea] = useState<string>("");
|
||||
const [selectedSDK, setSelectedSDK] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SupportForm_SelectInput
|
||||
formLabel="Type"
|
||||
name="extraInfo_Engine_Type"
|
||||
onValueChange={setSelectedEngineType}
|
||||
options={ENGINE_TYPES}
|
||||
promptText="Select Engine type"
|
||||
formLabel="Which API/SDK are you using?"
|
||||
name="extraInfo_API_SDK"
|
||||
onValueChange={setSelectedSDK}
|
||||
options={API_SDK_OPTIONS}
|
||||
promptText="Select a problem area"
|
||||
required={true}
|
||||
value={selectedEngineType}
|
||||
/>{" "}
|
||||
{selectedEngineType && (
|
||||
value={selectedSDK}
|
||||
/>
|
||||
{selectedSDK && (
|
||||
<>
|
||||
<SupportForm_SelectInput
|
||||
formLabel="Problem area"
|
||||
name="extraInfo_Problem_Area"
|
||||
onValueChange={setProblemArea}
|
||||
options={ENGINE_PROBLEM_AREAS}
|
||||
promptText="Select a problem area"
|
||||
required={true}
|
||||
value={problemArea}
|
||||
/>
|
||||
|
||||
{problemArea && (
|
||||
<>
|
||||
{selectedSDK === "Unity/.NET" ? (
|
||||
<UnitySupportForm />
|
||||
) : (
|
||||
selectedSDK !== "API endpoint" && (
|
||||
<SupportForm_TextInput
|
||||
formLabel="Engine URL"
|
||||
formValue="extraInfo_Engine_URL"
|
||||
inputType="url"
|
||||
placeholder="https://your-engine-url.com"
|
||||
formLabel="Version of SDK"
|
||||
formValue="extraInfo_SDK_Version"
|
||||
inputType="text"
|
||||
placeholder="e.g. 3.12.0"
|
||||
required={true}
|
||||
/>
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
</>
|
||||
)
|
||||
)}
|
||||
<SupportForm_TextInput
|
||||
formLabel="Queue/Support ID in logs"
|
||||
formValue="extraInfo_Queue_Support_ID"
|
||||
inputType="text"
|
||||
placeholder="Enter support ID from logs"
|
||||
required={false}
|
||||
/>
|
||||
{selectedSDK === "API endpoint" && (
|
||||
<SupportForm_TextInput
|
||||
formLabel="API endpoint URL with parameters"
|
||||
formValue="extraInfo_API_Endpoint_URL"
|
||||
inputType="url"
|
||||
placeholder="https://api.example.com/endpoint?param=value"
|
||||
required={false}
|
||||
/>
|
||||
)}
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ const OTHER_PROBLEM_AREAS = [
|
||||
"Bug report",
|
||||
"Documentation",
|
||||
"Integration help",
|
||||
"Agents/x402 and AI",
|
||||
"Other",
|
||||
];
|
||||
export function OtherSupportForm() {
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
|
||||
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
|
||||
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
|
||||
|
||||
const PAYMENT_AREAS = ["Dashboard", "Application"];
|
||||
|
||||
export function PaymentsSupportForm() {
|
||||
const [area, setArea] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SupportForm_SelectInput
|
||||
formLabel="Affected Area"
|
||||
name="extraInfo_Payments_Area"
|
||||
onValueChange={setArea}
|
||||
options={PAYMENT_AREAS}
|
||||
promptText="Select affected area"
|
||||
required={true}
|
||||
value={area}
|
||||
/>
|
||||
{area === "Application" && (
|
||||
<>
|
||||
<SupportForm_TextInput
|
||||
formLabel="SDK"
|
||||
formValue="extraInfo_Payments_SDK"
|
||||
inputType="text"
|
||||
placeholder="e.g. React, Unity, iOS, etc."
|
||||
required={true}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
formLabel="SDK Version"
|
||||
formValue="extraInfo_Payments_SDK_Version"
|
||||
inputType="text"
|
||||
placeholder="e.g. 3.12.0"
|
||||
required={true}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Application URL"
|
||||
formValue="extraInfo_Payments_App_URL"
|
||||
inputType="url"
|
||||
placeholder="https://your-app-url.com"
|
||||
required={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(area === "Application" || area === "Dashboard") && (
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+10
-3
@@ -8,10 +8,10 @@ export function TokensMarketplaceSupportForm() {
|
||||
return (
|
||||
<>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Address"
|
||||
formValue="extraInfo_TokensMarketplace_Address"
|
||||
formLabel="Token address or symbol"
|
||||
formValue="extraInfo_Token_Address_Symbol"
|
||||
inputType="text"
|
||||
placeholder="Enter the relevant address"
|
||||
placeholder="Enter token address or symbol (e.g. 0x... or ETH)"
|
||||
required={true}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
@@ -21,6 +21,13 @@ export function TokensMarketplaceSupportForm() {
|
||||
placeholder="e.g. Ethereum, Polygon, etc."
|
||||
required={true}
|
||||
/>
|
||||
<SupportForm_TextInput
|
||||
formLabel="Support ID in logs"
|
||||
formValue="extraInfo_Support_ID_Logs"
|
||||
inputType="text"
|
||||
placeholder="Enter support ID from logs"
|
||||
required={false}
|
||||
/>
|
||||
<DescriptionInput value={description} onChange={setDescription} />
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@ export const SmartWalletsBillingAlert = (props: { teamSlug: string }) => {
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Account Abstraction on Mainnet</AlertTitle>
|
||||
<AlertTitle>Gas Sponsorship on Mainnet</AlertTitle>
|
||||
<AlertDescription>
|
||||
To enable AA on mainnet chains,{" "}
|
||||
To enable Gas Sponsorship on mainnet chains,{" "}
|
||||
<UnderlineLink href={`/team/${props.teamSlug}/~/billing`}>
|
||||
subscribe to a billing plan.
|
||||
</UnderlineLink>
|
||||
|
||||
-8
@@ -14,7 +14,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
|
||||
export function DefaultFactoriesSection() {
|
||||
const data = [
|
||||
@@ -38,13 +37,6 @@ export function DefaultFactoriesSection() {
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Ready to use account factories that are pre-deployed on each chain.{" "}
|
||||
<UnderlineLink
|
||||
href="https://playground.thirdweb.com/connect/account-abstraction/connect"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn how to use these in your apps
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+6
-158
@@ -1,163 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { SearchParams } from "nuqs/server";
|
||||
import { getUserOpUsage } from "@/api/analytics";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import {
|
||||
getLastNDaysRange,
|
||||
type Range,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
|
||||
import { getAbsoluteUrl } from "@/utils/vercel";
|
||||
import { AccountAbstractionSummary } from "./AccountAbstractionAnalytics/AccountAbstractionSummary";
|
||||
import { SmartWalletsBillingAlert } from "./Alerts";
|
||||
import { AccountAbstractionAnalytics } from "./aa-analytics";
|
||||
import { searchParamLoader } from "./search-params";
|
||||
|
||||
interface PageParams {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
// Redirect old Account Abstraction page to new Sponsored Gas Overview
|
||||
export default async function Page(props: {
|
||||
params: Promise<PageParams>;
|
||||
searchParams: Promise<SearchParams>;
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const [params, searchParams, authToken] = await Promise.all([
|
||||
props.params,
|
||||
searchParamLoader(props.searchParams),
|
||||
getAuthToken(),
|
||||
]);
|
||||
|
||||
if (!authToken) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [team, project] = await Promise.all([
|
||||
getTeamBySlug(params.team_slug),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const interval = searchParams.interval ?? "week";
|
||||
const rangeType = searchParams.range || "last-120";
|
||||
|
||||
const range: Range = {
|
||||
from:
|
||||
rangeType === "custom"
|
||||
? searchParams.from
|
||||
: getLastNDaysRange(rangeType).from,
|
||||
to:
|
||||
rangeType === "custom"
|
||||
? searchParams.to
|
||||
: getLastNDaysRange(rangeType).to,
|
||||
type: rangeType,
|
||||
};
|
||||
|
||||
const userOpStats = await getUserOpUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
const isBundlerServiceEnabled = !!project.services.find(
|
||||
(s) => s.name === "bundler",
|
||||
);
|
||||
|
||||
const hasSmartWalletsWithoutBilling =
|
||||
isBundlerServiceEnabled &&
|
||||
team.billingStatus !== "validPayment" &&
|
||||
team.billingStatus !== "pastDue";
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
icon: SmartAccountIcon,
|
||||
client,
|
||||
title: "Account Abstraction",
|
||||
description:
|
||||
"Integrate EIP-7702 and EIP-4337 compliant smart accounts for gasless sponsorships and more.",
|
||||
actions: null,
|
||||
settings: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/settings/account-abstraction`,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/transactions/sponsor",
|
||||
},
|
||||
{
|
||||
type: "playground",
|
||||
href: "https://playground.thirdweb.com/account-abstraction/eip-7702",
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{hasSmartWalletsWithoutBilling && (
|
||||
<>
|
||||
<SmartWalletsBillingAlert teamSlug={params.team_slug} />
|
||||
<div className="h-10" />
|
||||
</>
|
||||
)}
|
||||
<div className="flex grow flex-col gap-10">
|
||||
<AccountAbstractionSummary
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<AccountAbstractionAnalytics
|
||||
client={client}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
teamSlug={params.team_slug}
|
||||
userOpStats={userOpStats}
|
||||
/>
|
||||
</div>
|
||||
</ProjectPage>
|
||||
const params = await props.params;
|
||||
redirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/wallets/sponsored-gas`,
|
||||
);
|
||||
}
|
||||
|
||||
const seo = {
|
||||
desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
|
||||
title: "The Complete Account Abstraction Toolkit | thirdweb",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: seo.desc,
|
||||
openGraph: {
|
||||
description: seo.desc,
|
||||
images: [
|
||||
{
|
||||
alt: seo.title,
|
||||
height: 630,
|
||||
url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`,
|
||||
width: 1200,
|
||||
},
|
||||
],
|
||||
title: seo.title,
|
||||
},
|
||||
title: seo.title,
|
||||
};
|
||||
|
||||
+1
-1
@@ -371,7 +371,7 @@ export function AccountAbstractionSettingsPage(
|
||||
of the{" "}
|
||||
<UnderlineLink
|
||||
className="text-primary-500"
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet/infrastructure#supported-chains"
|
||||
href="https://thirdweb.com/chainlist?service=account-abstraction"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
import { format } from "date-fns";
|
||||
import { BotIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
|
||||
import { DocLink } from "@/components/blocks/DocLink";
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import type { AIUsageStats } from "@/types/analytics";
|
||||
|
||||
type ChartData = Record<string, number> & {
|
||||
@@ -106,7 +106,7 @@ function AiTokenUsageEmptyChartState() {
|
||||
</span>
|
||||
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<DocLink
|
||||
icon={NebulaIcon}
|
||||
icon={BotIcon}
|
||||
label="Get Started"
|
||||
link="https://portal.thirdweb.com/ai/chat"
|
||||
/>
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { BotIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
@@ -6,7 +7,6 @@ import type { DurationId } from "@/components/analytics/date-range-selector";
|
||||
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { AiAnalytics } from "./chart";
|
||||
@@ -58,7 +58,7 @@ export default async function Page(props: {
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<ProjectPage
|
||||
header={{
|
||||
icon: NebulaIcon,
|
||||
icon: BotIcon,
|
||||
client,
|
||||
title: "AI Analytics",
|
||||
description: "Track your AI app usage and performance",
|
||||
|
||||
+2
-2
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import {
|
||||
BotIcon,
|
||||
ChartLineIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
@@ -25,7 +26,6 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TruncatedSessionInfo } from "../api/types";
|
||||
@@ -52,7 +52,7 @@ export function ChatSidebar(props: {
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-start gap-3 p-4 lg:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<NebulaIcon aria-label="AI" className="size-6 text-foreground" />
|
||||
<BotIcon aria-label="AI" className="size-6 text-foreground" />
|
||||
<span className="font-medium text-lg">thirdweb AI</span>
|
||||
</div>
|
||||
|
||||
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
import { MarkdownRenderer } from "@workspace/ui/components/markdown-renderer";
|
||||
import { ScrollShadow } from "@workspace/ui/components/scroll-shadow";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { AlertCircleIcon, BotIcon } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
NebulaSwapData,
|
||||
@@ -238,11 +237,11 @@ function RenderMessage(props: {
|
||||
)}
|
||||
>
|
||||
{message.type === "presence" && (
|
||||
<NebulaIcon className="size-5 text-muted-foreground" />
|
||||
<BotIcon className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
{message.type === "assistant" && (
|
||||
<NebulaIcon className="size-5 text-muted-foreground" />
|
||||
<BotIcon className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
{message.type === "error" && (
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRightIcon, InfoIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon, BotIcon, InfoIcon } from "lucide-react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { NebulaContext, NebulaUserMessage } from "../api/types";
|
||||
import { examplePrompts } from "../data/examplePrompts";
|
||||
@@ -33,7 +32,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
<div className="flex justify-center">
|
||||
<div className="rounded-full border-[1.5px] border-primary-foreground/20 bg-[hsl(var(--primary-foreground)/5%)] p-1">
|
||||
<div className="rounded-full border-[1.5px] border-primary-foreground/40 bg-[hsl(var(--primary-foreground)/5%)] p-2">
|
||||
<NebulaIcon className="size-7 text-primary-foreground" />
|
||||
<BotIcon className="size-7 text-primary-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+20
-21
@@ -1,10 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowRightLeftIcon,
|
||||
BadgeDollarSignIcon,
|
||||
WebhookIcon,
|
||||
} from "lucide-react";
|
||||
import { CodeIcon, WebhookIcon } from "lucide-react";
|
||||
import { FeatureCard } from "../payments/components/FeatureCard.client";
|
||||
|
||||
export function QuickStartSection(props: {
|
||||
@@ -24,32 +20,36 @@ export function QuickStartSection(props: {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<FeatureCard
|
||||
title="Cross-chain Swap Tokens"
|
||||
icon={ArrowRightLeftIcon}
|
||||
setupTime={5}
|
||||
title="Buy Widget Component"
|
||||
icon={CodeIcon}
|
||||
id="swap_tokens"
|
||||
features={["Swap any token", "Cross-chain swap"]}
|
||||
description="Swap tokens cross-chain with dedicated swapping endpoints."
|
||||
features={[
|
||||
"React component",
|
||||
"Cross-chain token swaps across 85+ blockchains",
|
||||
"Fiat onramp support to buy tokens with credit/debit cards",
|
||||
"Customizable UI",
|
||||
]}
|
||||
description={undefined}
|
||||
link={{
|
||||
href: `https://portal.thirdweb.com/bridge/swap`,
|
||||
label: "Setup Swap",
|
||||
label: "Get Started",
|
||||
}}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
title="Earn Fees"
|
||||
description="Setup fees to earn any time a user swaps or bridges funds."
|
||||
icon={BadgeDollarSignIcon}
|
||||
title="Bridge Widget Script"
|
||||
description={undefined}
|
||||
icon={CodeIcon}
|
||||
id="fees"
|
||||
setupTime={1}
|
||||
features={[
|
||||
"Fees on every purchase",
|
||||
"Custom percentage",
|
||||
"Directly to your wallet",
|
||||
"Integrate with a script tag",
|
||||
"Cross-chain token swaps across 85+ blockchains",
|
||||
"Fiat onramp support to buy tokens with credit/debit cards",
|
||||
"Customizable UI",
|
||||
]}
|
||||
link={{
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/settings/payments`,
|
||||
label: "Configure Fees",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
|
||||
label: "Get Started",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -57,7 +57,6 @@ export function QuickStartSection(props: {
|
||||
title="Webhooks"
|
||||
description="Create Webhooks to get notified on each purchase or transaction."
|
||||
icon={WebhookIcon}
|
||||
setupTime={5}
|
||||
id="webhooks"
|
||||
features={["Instant events", "Transaction verification"]}
|
||||
link={{
|
||||
|
||||
+11
-9
@@ -3,9 +3,10 @@ import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import { getFees } from "@/api/universal-bridge/developer";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getProjectWallet } from "@/lib/server/project-wallet";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
|
||||
import { RouteDiscovery } from "../RouteDiscovery";
|
||||
import { PayConfig } from "./PayConfig";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -21,7 +22,7 @@ export default async function Page(props: {
|
||||
]);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${team_slug}/${project_slug}/settings/payments`);
|
||||
loginRedirect(`/team/${team_slug}/${project_slug}/bridge/configuration`);
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
@@ -56,14 +57,13 @@ export default async function Page(props: {
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<ProjectSettingsBreadcrumb
|
||||
teamSlug={team_slug}
|
||||
projectSlug={project_slug}
|
||||
page="Payments"
|
||||
/>
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<PayConfig
|
||||
fees={fees}
|
||||
project={project}
|
||||
@@ -72,6 +72,8 @@ export default async function Page(props: {
|
||||
teamSlug={team_slug}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<RouteDiscovery client={client} project={project} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { WebhookIcon } from "lucide-react";
|
||||
import { redirect } 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";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [params, authToken] = await Promise.all([props.params, getAuthToken()]);
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/bridge`);
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
title: "Bridge",
|
||||
icon: BridgeIcon,
|
||||
description: (
|
||||
<>
|
||||
Bridge lets developers swap and transfer any token across any chain
|
||||
instantly
|
||||
</>
|
||||
),
|
||||
actions: {
|
||||
secondary: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
|
||||
label: "Webhooks",
|
||||
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
|
||||
},
|
||||
},
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/bridge",
|
||||
},
|
||||
{
|
||||
type: "playground",
|
||||
href: "https://playground.thirdweb.com/bridge/swap-widget",
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://api.thirdweb.com/reference#tag/bridge",
|
||||
},
|
||||
],
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
name: "Overview",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/bridge`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Configuration",
|
||||
path: `/team/${params.team_slug}/${params.project_slug}/bridge/configuration`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</ProjectPage>
|
||||
);
|
||||
}
|
||||
+20
-64
@@ -1,17 +1,12 @@
|
||||
import { WebhookIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
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";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { PayAnalytics } from "../payments/components/PayAnalytics";
|
||||
import { getUniversalBridgeFiltersFromSearchParams } from "../payments/components/time";
|
||||
import { QuickStartSection } from "./QuickstartSection.client";
|
||||
import { RouteDiscovery } from "./RouteDiscovery";
|
||||
import { ViewTxStatus } from "./view-tx-status";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
@@ -51,66 +46,27 @@ export default async function Page(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
title: "Bridge",
|
||||
icon: BridgeIcon,
|
||||
description: (
|
||||
<>
|
||||
Bridge lets developers swap and transfer any token across any chain
|
||||
instantly
|
||||
</>
|
||||
),
|
||||
actions: {
|
||||
secondary: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
|
||||
label: "Webhooks",
|
||||
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
|
||||
},
|
||||
},
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/bridge",
|
||||
},
|
||||
{
|
||||
type: "playground",
|
||||
href: "https://playground.thirdweb.com/bridge/swap-widget",
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://api.thirdweb.com/reference#tag/bridge",
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<PayAnalytics
|
||||
client={client}
|
||||
interval={interval}
|
||||
projectClientId={project.publishableKey}
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
<div className="flex flex-col gap-6">
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<PayAnalytics
|
||||
client={client}
|
||||
interval={interval}
|
||||
projectClientId={project.publishableKey}
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
|
||||
<RouteDiscovery client={client} project={project} />
|
||||
|
||||
<ViewTxStatus client={client} />
|
||||
|
||||
<div className="pt-4">
|
||||
<QuickStartSection
|
||||
projectSlug={params.project_slug}
|
||||
teamSlug={params.team_slug}
|
||||
clientId={project.publishableKey}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<QuickStartSection
|
||||
projectSlug={params.project_slug}
|
||||
teamSlug={params.team_slug}
|
||||
clientId={project.publishableKey}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
</div>
|
||||
</ProjectPage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
"use client";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export function ViewTxStatus(props: { client: ThirdwebClient }) {
|
||||
const [txHash, setTxHash] = useState("");
|
||||
const [chainId, setChainId] = useState<number>(1);
|
||||
|
||||
const isValidTxHash = txHash.length === 66 && txHash.startsWith("0x");
|
||||
const isEnabled = !!txHash && !!chainId && isValidTxHash;
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-xl border">
|
||||
<div className="p-4 lg:p-6 mb-3">
|
||||
<h3 className="font-semibold text-xl tracking-tight mb-1">
|
||||
View transaction status
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Select chain and enter the transaction hash to view the transaction
|
||||
status.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 max-w-3xl">
|
||||
<div className="space-y-2">
|
||||
<Label>Chain</Label>
|
||||
<SingleNetworkSelector
|
||||
chainId={chainId}
|
||||
onChange={setChainId}
|
||||
client={props.client}
|
||||
disableChainId
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="view-tx-hash">Transaction Hash</Label>
|
||||
<Input
|
||||
id="view-tx-hash"
|
||||
value={txHash}
|
||||
onChange={(e) => setTxHash(e.target.value)}
|
||||
placeholder="0x.."
|
||||
/>
|
||||
{txHash && !isValidTxHash && (
|
||||
<p className="text-sm text-destructive-text">
|
||||
Invalid transaction hash
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 lg:px-6 py-4 border-t border-dashed justify-end items-center flex">
|
||||
<Button
|
||||
asChild
|
||||
disabled={!isEnabled}
|
||||
size="sm"
|
||||
variant={isEnabled ? "default" : "outline"}
|
||||
className="gap-1.5 rounded-full"
|
||||
>
|
||||
<Link href={`/${chainId.toString()}/tx/${txHash}`} target="_blank">
|
||||
View Transaction Status <ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -3,8 +3,8 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
export function ClientIDSection(props: { clientId: string }) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Client ID</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
<h3 className="mb-1 text-base text-foreground font-medium">Client ID</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
Identifies your application
|
||||
</p>
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export function CodeShowcase(props: {
|
||||
title: string;
|
||||
tabs: Array<{
|
||||
label: string;
|
||||
code: React.ReactNode;
|
||||
}>;
|
||||
}) {
|
||||
const [selectedTab, setSelectedTab] = useState(props.tabs[0]?.label || "");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 justify-between border px-5 py-3 rounded-t-xl bg-card">
|
||||
<h3 className="text-sm font-mono">{props.title}</h3>
|
||||
<Select
|
||||
value={selectedTab}
|
||||
onValueChange={(val) => setSelectedTab(val)}
|
||||
>
|
||||
<SelectTrigger className="w-[160px] rounded-full">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{props.tabs.map((tab) => (
|
||||
<SelectItem key={tab.label} value={tab.label}>
|
||||
{tab.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{props.tabs.find((tab) => tab.label === selectedTab)?.code}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
@@ -24,6 +24,7 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
projectWalletSection: undefined,
|
||||
project: {
|
||||
...projectStub("foo", "bar"),
|
||||
secretKeys: [
|
||||
|
||||
+218
-141
@@ -1,27 +1,40 @@
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { CodeServer } from "@workspace/ui/components/code/code.server";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
ArrowUpRightIcon,
|
||||
BotIcon,
|
||||
ChevronRightIcon,
|
||||
ExternalLinkIcon,
|
||||
CoinsIcon,
|
||||
DoorOpenIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { CodeServer } from "@/components/ui/code/code.server";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
|
||||
import { GithubIcon } from "@/icons/brand-icons/GithubIcon";
|
||||
import { ExpoIcon } from "@/icons/brand-icons/ExpoIcon";
|
||||
import { NextjsIcon } from "@/icons/brand-icons/NextjsIcon";
|
||||
import { NodeJSIcon } from "@/icons/brand-icons/NodeJSIcon";
|
||||
import { ReactIcon } from "@/icons/brand-icons/ReactIcon";
|
||||
import { TypeScriptIcon } from "@/icons/brand-icons/TypeScriptIcon";
|
||||
import { UnityIcon } from "@/icons/brand-icons/UnityIcon";
|
||||
import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
|
||||
import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { ViteIcon } from "@/icons/brand-icons/ViteIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import { ClientIDSection } from "./ClientIDSection";
|
||||
import { CodeShowcase } from "./CodeSelector";
|
||||
import { SecretKeySection } from "./SecretKeySection";
|
||||
|
||||
export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
|
||||
export function ProjectFTUX(props: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
projectWalletSection: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-14">
|
||||
<IntegrateAPIKeySection project={props.project} />
|
||||
{props.projectWalletSection}
|
||||
<GetStartedSection project={props.project} />
|
||||
<ProductsSection
|
||||
projectSlug={props.project.slug}
|
||||
teamSlug={props.teamSlug}
|
||||
@@ -39,12 +52,29 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
|
||||
const clientId = project.publishableKey;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Integrate API key
|
||||
</h2>
|
||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">API Keys</h2>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Your API key is used to authenticate and integrate your application.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-card rounded-lg gap-2"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about API keys
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border bg-card p-4">
|
||||
<div className="rounded-xl border bg-card p-5">
|
||||
<div className="flex flex-col gap-6 ">
|
||||
<ClientIDSection clientId={clientId} />
|
||||
{secretKeyMasked && (
|
||||
@@ -54,23 +84,74 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Run this command in your terminal to send a test transaction using
|
||||
your Project Wallet.
|
||||
</p>
|
||||
<CodeServer
|
||||
className="bg-background"
|
||||
code={curlCodeExample(project)}
|
||||
lang="bash"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function GetStartedSection({ project }: { project: Project }) {
|
||||
return (
|
||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Get Started
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Send a test transaction using your project wallet
|
||||
</p>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-card rounded-lg gap-2"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<Link href="https://portal.thirdweb.com/reference" target="_blank">
|
||||
View API Reference
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CodeShowcase
|
||||
title="POST /v1/transactions"
|
||||
tabs={[
|
||||
{
|
||||
label: "Curl",
|
||||
code: (
|
||||
<CodeServer
|
||||
code={curlCodeExample(project)}
|
||||
lang="bash"
|
||||
className="border-t-0 rounded-t-none"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "JavaScript",
|
||||
code: (
|
||||
<CodeServer
|
||||
code={fetchJSCodeExample(project)}
|
||||
lang="js"
|
||||
className="border-t-0 rounded-t-none"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Python",
|
||||
code: (
|
||||
<CodeServer
|
||||
code={pythonCodeExample(project)}
|
||||
lang="python"
|
||||
className="border-t-0 rounded-t-none"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const curlCodeExample = (project: Project): string => `\
|
||||
curl https://api.thirdweb.com/v1/transactions \\
|
||||
--request POST \\
|
||||
@@ -88,6 +169,40 @@ curl https://api.thirdweb.com/v1/transactions \\
|
||||
}'
|
||||
`;
|
||||
|
||||
const fetchJSCodeExample = (project: Project): string => `\
|
||||
fetch("https://api.thirdweb.com/v1/transactions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": "${project.secretKeys[0]?.masked ?? "<YOUR_SECRET_KEY>"}",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chainId: 421614,
|
||||
transactions: [
|
||||
{ data: "0x", to: "vitalik.eth", value: "0" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
`;
|
||||
|
||||
const pythonCodeExample = (project: Project): string => `\
|
||||
import requests
|
||||
|
||||
url = "https://api.thirdweb.com/v1/transactions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": "${project.secretKeys[0]?.masked ?? "<YOUR_SECRET_KEY>"}",
|
||||
}
|
||||
payload = {
|
||||
"chainId": 421614,
|
||||
"transactions": [
|
||||
{ "data": "0x", "to": "vitalik.eth", "value": "0" },
|
||||
],
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
result = response.json()
|
||||
`;
|
||||
|
||||
// products section ------------------------------------------------------------
|
||||
|
||||
function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
|
||||
@@ -98,56 +213,60 @@ function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
|
||||
icon: React.FC<{ className?: string }>;
|
||||
}> = [
|
||||
{
|
||||
description:
|
||||
"Scale your application with a backend server to read, write, and deploy contracts at production-grade.",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/transactions`,
|
||||
icon: ArrowLeftRightIcon,
|
||||
title: "Transactions",
|
||||
icon: WalletProductIcon,
|
||||
title: "Wallets",
|
||||
description: "Wallets to read, write and transact",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets`,
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Deploy your own contracts or leverage existing solutions for onchain implementation",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/contracts`,
|
||||
icon: ContractIcon,
|
||||
title: "Contracts",
|
||||
icon: BridgeIcon,
|
||||
title: "Bridge",
|
||||
description: "Swap and bridge tokens",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge`,
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Add indexing capabilities to retrieve real-time onchain data",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/insight`,
|
||||
icon: InsightIcon,
|
||||
title: "Insight",
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Bridge, swap, and purchase cryptocurrencies with any fiat options or tokens via cross-chain routing",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/payments`,
|
||||
icon: PayIcon,
|
||||
title: "Payments",
|
||||
title: "x402",
|
||||
description: "Native internet payments",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/x402`,
|
||||
},
|
||||
{
|
||||
icon: DoorOpenIcon,
|
||||
title: "Gateway",
|
||||
description: "Blockchain connectivity and data access",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`,
|
||||
},
|
||||
{
|
||||
icon: CoinsIcon,
|
||||
title: "Tokens",
|
||||
description: "Launch tokens and markets",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/tokens`,
|
||||
},
|
||||
{
|
||||
icon: BotIcon,
|
||||
title: "AI",
|
||||
description: "Read and write onchain via AI agents",
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/ai`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
Complete your full-stack application
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Tools to build frontend, backend, and onchain with built-in
|
||||
infrastructure and analytics.
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">Products</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Everything you need to build full-stack applications, games, and agents.
|
||||
</p>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
{/* Feature Cards */}
|
||||
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
<IconCard
|
||||
description={product.description}
|
||||
href={product.href}
|
||||
icon={product.icon}
|
||||
key={product.title}
|
||||
title={product.title}
|
||||
name={product.title}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
@@ -155,72 +274,51 @@ function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ProductCard(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex flex-col rounded-lg border bg-card p-4 hover:border-active-border">
|
||||
<div className="mb-3 flex size-9 items-center justify-center rounded-full border p-1">
|
||||
<props.icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
|
||||
<Link className="before:absolute before:inset-0" href={props.href}>
|
||||
{props.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// sdk section ------------------------------------------------------------
|
||||
|
||||
type SDKCardProps = {
|
||||
type IconCardProps = {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
href: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
trackingLabel: string;
|
||||
};
|
||||
|
||||
const sdks: SDKCardProps[] = [
|
||||
const sdks: IconCardProps[] = [
|
||||
{
|
||||
href: "https://portal.thirdweb.com/sdk/typescript",
|
||||
icon: TypeScriptIcon,
|
||||
name: "TypeScript",
|
||||
trackingLabel: "typescript",
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5",
|
||||
icon: ReactIcon,
|
||||
name: "React",
|
||||
trackingLabel: "react",
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react-native/v5",
|
||||
icon: ReactIcon,
|
||||
name: "React Native",
|
||||
trackingLabel: "react_native",
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/unity/v5",
|
||||
icon: UnityIcon,
|
||||
name: "Unity",
|
||||
trackingLabel: "unity",
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/unreal-engine",
|
||||
icon: UnrealIcon,
|
||||
name: "Unreal Engine",
|
||||
trackingLabel: "unreal",
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/dotnet",
|
||||
icon: DotNetIcon,
|
||||
name: ".NET",
|
||||
trackingLabel: "dotnet",
|
||||
description: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -228,14 +326,14 @@ function SDKSection() {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">Client SDKs</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{sdks.map((sdk) => (
|
||||
<SDKCard
|
||||
<IconCard
|
||||
href={sdk.href}
|
||||
icon={sdk.icon}
|
||||
key={sdk.name}
|
||||
name={sdk.name}
|
||||
trackingLabel={sdk.trackingLabel}
|
||||
description={sdk.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -243,10 +341,10 @@ function SDKSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function SDKCard(props: SDKCardProps) {
|
||||
function IconCard(props: IconCardProps) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-3 rounded-lg border bg-card p-4 hover:border-active-border">
|
||||
<div className="flex size-9 items-center justify-center rounded-full border">
|
||||
<div className="relative flex items-center gap-3 rounded-xl border bg-card p-4 hover:border-active-border">
|
||||
<div className="flex items-center justify-center rounded-full border p-2 bg-background">
|
||||
<props.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
@@ -254,16 +352,14 @@ function SDKCard(props: SDKCardProps) {
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
>
|
||||
{props.name}
|
||||
</Link>
|
||||
</p>
|
||||
<p className="inline-flex items-center gap-1.5 text-muted-foreground text-xs">
|
||||
View Docs
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</p>
|
||||
{props.description && (
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -271,42 +367,47 @@ function SDKCard(props: SDKCardProps) {
|
||||
|
||||
// starter kits section ------------------------------------------------------------
|
||||
|
||||
type StartedKitCardProps = {
|
||||
name: string;
|
||||
href: string;
|
||||
trackingLabel: string;
|
||||
};
|
||||
|
||||
const startedKits: StartedKitCardProps[] = [
|
||||
const startedKits: IconCardProps[] = [
|
||||
{
|
||||
href: "https://github.com/thirdweb-example/next-starter",
|
||||
name: "Next Starter",
|
||||
trackingLabel: "next_starter",
|
||||
icon: NextjsIcon,
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://github.com/thirdweb-example/vite-starter",
|
||||
name: "Vite Starter",
|
||||
trackingLabel: "vite_starter",
|
||||
icon: ViteIcon,
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://github.com/thirdweb-example/expo-starter",
|
||||
name: "Expo Starter",
|
||||
trackingLabel: "expo_starter",
|
||||
icon: ExpoIcon,
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
href: "https://github.com/thirdweb-example/node-starter",
|
||||
name: "Node Starter",
|
||||
trackingLabel: "node_starter",
|
||||
icon: NodeJSIcon,
|
||||
description: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
function StarterKitsSection() {
|
||||
return (
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<h2 className="font-semibold text-xl tracking-tight">Starter Kits</h2>
|
||||
<div className="mb-3 flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="font-semibold text-xl tracking-tight mb-1">
|
||||
Starters
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Kickstart your development process with ready-to-ship repositories.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
className="inline-flex items-center gap-1 text-muted-foreground text-sm hover:text-foreground"
|
||||
className="inline-flex items-center gap-1 text-muted-foreground text-sm hover:text-foreground shrink-0"
|
||||
href="https://github.com/thirdweb-example"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
@@ -315,41 +416,17 @@ function StarterKitsSection() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{startedKits.map((kit) => (
|
||||
<StarterKitCard
|
||||
<IconCard
|
||||
href={kit.href}
|
||||
icon={kit.icon}
|
||||
key={kit.name}
|
||||
name={kit.name}
|
||||
trackingLabel={kit.trackingLabel}
|
||||
description={kit.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StarterKitCard(props: StartedKitCardProps) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-3 rounded-lg border bg-card p-4 hover:border-active-border">
|
||||
<div className="flex size-9 items-center justify-center rounded-full border">
|
||||
<GithubIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{props.name}
|
||||
</Link>
|
||||
<p className="inline-flex items-center gap-1.5 text-muted-foreground text-xs">
|
||||
View Repo
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-5
@@ -13,15 +13,15 @@ export function SecretKeySection(props: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Secret Key</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
<h3 className="mb-1 text-base text-foreground font-medium">Secret Key</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
Identifies and authenticates your application from a backend. <br />{" "}
|
||||
This is not the full secret key, Refer to your saved secret key at the
|
||||
This is not the full secret key. Refer to your saved secret key at the
|
||||
time of creation for the full secret key.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm lg:w-[350px]">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:w-[400px]">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm grow">
|
||||
{secretKeyMasked}
|
||||
</div>
|
||||
|
||||
|
||||
+116
-138
@@ -1,160 +1,138 @@
|
||||
"use client";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
BotIcon,
|
||||
DatabaseIcon,
|
||||
DoorOpenIcon,
|
||||
HomeIcon,
|
||||
LockIcon,
|
||||
RssIcon,
|
||||
Settings2Icon,
|
||||
WebhookIcon,
|
||||
} from "lucide-react";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
FullWidthSidebarLayout,
|
||||
type ShadcnSidebarLink,
|
||||
} from "@/components/blocks/full-width-sidebar-layout";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
|
||||
import { TokenIcon } from "@/icons/TokenIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
|
||||
export function ProjectSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
children: React.ReactNode;
|
||||
hasEngines: boolean;
|
||||
}) {
|
||||
const contentSidebarLinks = [
|
||||
{
|
||||
exactMatch: true,
|
||||
href: props.layoutPath,
|
||||
icon: HomeIcon,
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
subMenu: {
|
||||
icon: WalletProductIcon,
|
||||
label: "Wallets",
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: `${props.layoutPath}/wallets/user-wallets`,
|
||||
label: "User Wallets",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/wallets/server-wallets`,
|
||||
label: "Server Wallets",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/wallets/sponsored-gas`,
|
||||
label: "Gas Sponsorship",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/contracts`,
|
||||
icon: ContractIcon,
|
||||
label: "Contracts",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/x402`,
|
||||
icon: PayIcon,
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
x402 <Badge>New</Badge>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/bridge`,
|
||||
icon: BridgeIcon,
|
||||
label: "Bridge",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/tokens`,
|
||||
icon: TokenIcon,
|
||||
label: "Tokens",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/ai`,
|
||||
icon: BotIcon,
|
||||
label: "AI",
|
||||
},
|
||||
{
|
||||
subMenu: {
|
||||
icon: DoorOpenIcon,
|
||||
label: "Gateway",
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: `${props.layoutPath}/gateway/rpc`,
|
||||
label: "RPC",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/gateway/indexer`,
|
||||
label: "Indexer",
|
||||
},
|
||||
],
|
||||
},
|
||||
// only show engine link if there the user already has an engine instance
|
||||
...(props.hasEngines
|
||||
? [
|
||||
{
|
||||
href: `${props.layoutPath}/engine`,
|
||||
icon: DatabaseIcon,
|
||||
label: "Engine",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] satisfies ShadcnSidebarLink[];
|
||||
|
||||
const footerSidebarLinks = [
|
||||
{
|
||||
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,
|
||||
label: "Project Settings",
|
||||
},
|
||||
] satisfies ShadcnSidebarLink[];
|
||||
|
||||
return (
|
||||
<FullWidthSidebarLayout
|
||||
contentSidebarLinks={[
|
||||
{
|
||||
exactMatch: true,
|
||||
href: props.layoutPath,
|
||||
icon: HomeIcon,
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
group: "Build",
|
||||
links: [
|
||||
{
|
||||
href: `${props.layoutPath}/wallets`,
|
||||
icon: WalletProductIcon,
|
||||
label: "Wallets",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/transactions`,
|
||||
icon: ArrowLeftRightIcon,
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
Transactions <Badge>New</Badge>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/contracts`,
|
||||
icon: ContractIcon,
|
||||
label: "Contracts",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/ai`,
|
||||
icon: NebulaIcon,
|
||||
label: "AI",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
group: "Monetize",
|
||||
links: [
|
||||
{
|
||||
href: `${props.layoutPath}/payments`,
|
||||
icon: PayIcon,
|
||||
label: "Payments",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/bridge`,
|
||||
icon: BridgeIcon,
|
||||
label: "Bridge",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/tokens`,
|
||||
icon: TokenIcon,
|
||||
label: "Tokens",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
group: "Scale",
|
||||
links: [
|
||||
{
|
||||
href: `${props.layoutPath}/insight`,
|
||||
icon: InsightIcon,
|
||||
label: "Insight",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/account-abstraction`,
|
||||
icon: SmartAccountIcon,
|
||||
label: "Account Abstraction",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/rpc`,
|
||||
icon: RssIcon,
|
||||
label: "RPC",
|
||||
},
|
||||
{
|
||||
href: `${props.layoutPath}/vault`,
|
||||
icon: LockIcon,
|
||||
label: "Vault",
|
||||
},
|
||||
// linkely want to move this to `team` level eventually
|
||||
{
|
||||
href: `${props.layoutPath}/engine`,
|
||||
icon: DatabaseIcon,
|
||||
label: "Engine",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
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,
|
||||
label: "Project Settings",
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com",
|
||||
icon: BookTextIcon,
|
||||
label: "Documentation",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
icon: BoxIcon,
|
||||
label: "Playground",
|
||||
},
|
||||
]}
|
||||
contentSidebarLinks={contentSidebarLinks}
|
||||
footerSidebarLinks={footerSidebarLinks}
|
||||
>
|
||||
{props.children}
|
||||
</FullWidthSidebarLayout>
|
||||
|
||||
+2
-2
@@ -177,7 +177,7 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<div className="p-4 relative flex flex-col gap-5">
|
||||
<div className="p-5 relative flex flex-col gap-5">
|
||||
<div>
|
||||
<p className="text-sm text-foreground mb-1">Wallet Address</p>
|
||||
<CopyTextButton
|
||||
@@ -200,7 +200,7 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-dashed flex justify-between items-center">
|
||||
<div className="p-5 border-t border-dashed flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-sm text-foreground mb-1">Balance</p>
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
+6
@@ -49,6 +49,7 @@ const projectWallet2: ProjectWalletSummary = {
|
||||
|
||||
export const NoProjectWalletSetNoManagedAccessToken: Story = {
|
||||
args: {
|
||||
layout: "column",
|
||||
project: projectWithoutManagedAccessToken,
|
||||
client: storybookThirdwebClient,
|
||||
teamSlug: "bar",
|
||||
@@ -62,6 +63,7 @@ export const NoProjectWalletSetNoManagedAccessToken: Story = {
|
||||
|
||||
export const NoProjectWalletSetWithManagedAccessToken: Story = {
|
||||
args: {
|
||||
layout: "column",
|
||||
project: projectWithManagedAccessToken,
|
||||
client: storybookThirdwebClient,
|
||||
teamSlug: "bar",
|
||||
@@ -75,6 +77,7 @@ export const NoProjectWalletSetWithManagedAccessToken: Story = {
|
||||
|
||||
export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = {
|
||||
args: {
|
||||
layout: "column",
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
client: storybookThirdwebClient,
|
||||
@@ -88,6 +91,7 @@ export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = {
|
||||
|
||||
export const NoProjectWalletSetLoading: Story = {
|
||||
args: {
|
||||
layout: "column",
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
client: storybookThirdwebClient,
|
||||
@@ -101,6 +105,7 @@ export const NoProjectWalletSetLoading: Story = {
|
||||
|
||||
export const ProjectWalletSetMultipleServerWallets: Story = {
|
||||
args: {
|
||||
layout: "column",
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
client: storybookThirdwebClient,
|
||||
@@ -114,6 +119,7 @@ export const ProjectWalletSetMultipleServerWallets: Story = {
|
||||
|
||||
export const ProjectWalletSetSingleServerWallet: Story = {
|
||||
args: {
|
||||
layout: "column",
|
||||
project: projectWithManagedAccessToken,
|
||||
teamSlug: "bar",
|
||||
projectWallet: projectWallet1,
|
||||
|
||||
+35
-6
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
|
||||
import { ChevronDownIcon, XIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -29,6 +30,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
|
||||
import { CreateServerWallet } from "../../transactions/server-wallets/components/create-server-wallet.client";
|
||||
import { ProjectWalletDetailsSection } from "./project-wallet-details";
|
||||
@@ -131,11 +133,11 @@ function CreateProjectWalletSection(props: {
|
||||
Boolean(managementAccessToken) && serverWallets.length > 0;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border p-4 bg-card">
|
||||
<div className="rounded-xl border p-5 bg-card">
|
||||
<div className="rounded-full p-2 border bg-background inline-flex mb-4">
|
||||
<XIcon className="size-3.5 text-muted-foreground" />
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-foreground mb-0.5">
|
||||
<h2 className="text-base font-medium text-foreground mb-0.5">
|
||||
No Project Wallet set
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -276,10 +278,17 @@ export function ProjectWalletSectionUI(props: {
|
||||
projectWallet: ProjectWalletSummary | undefined;
|
||||
getProjectServerWallets: GetProjectServerWallets;
|
||||
client: ThirdwebClient;
|
||||
layout: "row" | "column";
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className={cn(
|
||||
props.layout === "row"
|
||||
? "grid grid-cols-1 gap-4 lg:grid-cols-2"
|
||||
: "flex flex-col gap-4",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Project Wallet
|
||||
</h2>
|
||||
@@ -292,6 +301,24 @@ export function ProjectWalletSectionUI(props: {
|
||||
thirdweb API
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
{props.layout === "row" && (
|
||||
<div className="mt-4 hidden lg:block">
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="rounded-lg gap-2 bg-card"
|
||||
size="sm"
|
||||
>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/reference"
|
||||
target="_blank"
|
||||
>
|
||||
View API Reference
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.projectWallet ? (
|
||||
@@ -318,6 +345,7 @@ export function ProjectWalletSection(props: {
|
||||
teamSlug: string;
|
||||
projectWallet: ProjectWalletSummary | undefined;
|
||||
client: ThirdwebClient;
|
||||
layout: "row" | "column";
|
||||
}) {
|
||||
return (
|
||||
<ProjectWalletSectionUI
|
||||
@@ -326,6 +354,7 @@ export function ProjectWalletSection(props: {
|
||||
client={props.client}
|
||||
projectWallet={props.projectWallet}
|
||||
getProjectServerWallets={listProjectServerWallets}
|
||||
layout={props.layout}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+19
-172
@@ -6,15 +6,10 @@ import {
|
||||
ArrowRightIcon,
|
||||
CheckIcon,
|
||||
CircleAlertIcon,
|
||||
DatabaseIcon,
|
||||
InfinityIcon,
|
||||
InfoIcon,
|
||||
MoreHorizontalIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
ShieldCheckIcon,
|
||||
Trash2Icon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
@@ -22,7 +17,6 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { CheckoutButton } from "@/components/billing/billing";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Badge, type BadgeProps } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -31,20 +25,10 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -72,7 +56,6 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import {
|
||||
type DeleteCloudHostedEngineParams,
|
||||
deleteCloudHostedEngine,
|
||||
@@ -84,7 +67,6 @@ import {
|
||||
} from "@/hooks/useEngine";
|
||||
import { EngineIcon } from "@/icons/EngineIcon";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DeletedCloudHostedEngine = (
|
||||
params: DeleteCloudHostedEngineParams,
|
||||
@@ -126,96 +108,6 @@ export function EngineInstancesTable(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function DedicatedEngineSubscriptionButton(props: { team: Team }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const trigger = (
|
||||
<Button className="gap-2 rounded-full" size="sm">
|
||||
<PlusIcon className="size-3.5" />
|
||||
Deploy Dedicated Engine
|
||||
</Button>
|
||||
);
|
||||
|
||||
const title = (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="grid size-8 place-items-center rounded-full bg-warning-text">
|
||||
<DatabaseIcon className="size-4 text-white" />
|
||||
</div>
|
||||
<span className="font-semibold text-lg">Dedicated Engine</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div className="flex flex-col items-start justify-center gap-4 px-4 md:px-0">
|
||||
<ul className="flex flex-col gap-1 self-start text-muted-foreground text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<ShieldCheckIcon className="size-4 text-foreground" />
|
||||
Isolated environment
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<WalletIcon className="size-4 text-foreground" />
|
||||
EOA or Smart Wallets
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<InfinityIcon className="size-4 text-foreground" />
|
||||
No transaction usage limits or charges
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer onOpenChange={setOpen} open={open}>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[90vh]">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{title}</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Instantly deploy a dedicated engine instance for your team.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{content}
|
||||
<DrawerFooter>
|
||||
<CheckoutButton
|
||||
billingStatus={props.team.billingStatus}
|
||||
sku="product:engine_standard"
|
||||
teamSlug={props.team.slug}
|
||||
>
|
||||
Deploy Now · $299 / month
|
||||
</CheckoutButton>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={setOpen} open={open}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Instantly deploy a dedicated engine instance for your team.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{content}
|
||||
<DialogFooter>
|
||||
<CheckoutButton
|
||||
billingStatus={props.team.billingStatus}
|
||||
sku="product:engine_standard"
|
||||
teamSlug={props.team.slug}
|
||||
>
|
||||
Deploy Now · $299 / month
|
||||
</CheckoutButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function EngineInstancesTableUI(props: {
|
||||
instances: EngineInstance[];
|
||||
engineLinkPrefix: string;
|
||||
@@ -896,10 +788,6 @@ function DeleteEngineSubscriptionModalContent(props: {
|
||||
}
|
||||
|
||||
function EmptyEngineState(props: { team: Team; projectSlug: string }) {
|
||||
const [selectedTab, setSelectedTab] = useState<
|
||||
"self-hosted" | "cloud-hosted"
|
||||
>("cloud-hosted");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border bg-card px-4 py-12 lg:px-6">
|
||||
<div className="mb-4 rounded-full border bg-card p-2">
|
||||
@@ -908,75 +796,34 @@ function EmptyEngineState(props: { team: Team; projectSlug: string }) {
|
||||
|
||||
<h2 className="mb-1 font-semibold text-xl">No Engine instances found</h2>
|
||||
<p className="mb-6 max-w-lg text-center text-muted-foreground text-sm">
|
||||
Get started with one of the options below to add your first engine
|
||||
instance.
|
||||
Import an Engine instance running on your own infrastructure.
|
||||
</p>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mx-auto max-w-sm">
|
||||
<div className="grid grid-cols-2 gap-1 rounded-lg border p-1">
|
||||
<Button
|
||||
className={cn(selectedTab === "cloud-hosted" && "bg-accent")}
|
||||
onClick={() => setSelectedTab("cloud-hosted")}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
Managed
|
||||
</Button>
|
||||
<div className="h-5" />
|
||||
|
||||
<div className="flex flex-col text-center">
|
||||
<h3 className="mb-0.5 font-semibold text-base">Self-Hosted Engine</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Add engine instance running on your own infrastructure.
|
||||
</p>
|
||||
<div className="h-4" />
|
||||
<div className="mt-auto">
|
||||
<Button
|
||||
className={cn(selectedTab === "self-hosted" && "bg-accent")}
|
||||
onClick={() => setSelectedTab("self-hosted")}
|
||||
asChild
|
||||
className="w-full gap-2"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
variant="default"
|
||||
>
|
||||
Self-hosted
|
||||
<Link
|
||||
href={`/team/${props.team.slug}/${props.projectSlug}/engine`}
|
||||
>
|
||||
Import self-hosted Engine
|
||||
<ArrowRightIcon size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
{selectedTab === "self-hosted" && (
|
||||
<div className="flex flex-col text-center">
|
||||
<h3 className="mb-0.5 font-semibold text-base">
|
||||
Self-Hosted Engine
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Add engine instance running on your own infrastructure.
|
||||
</p>
|
||||
<div className="h-4" />
|
||||
<div className="mt-auto">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full gap-2"
|
||||
size="sm"
|
||||
variant="default"
|
||||
>
|
||||
<Link
|
||||
href={`/team/${props.team.slug}/${props.projectSlug}/engine`}
|
||||
>
|
||||
Import self-hosted Engine
|
||||
<ArrowRightIcon size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTab === "cloud-hosted" && (
|
||||
<div className="flex flex-col text-center">
|
||||
<h3 className="mb-0.5 font-semibold text-base">Managed Engine</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Deploy a managed engine instance to your team. <br /> We recommend
|
||||
using Engine Cloud in most cases.
|
||||
</p>
|
||||
<div className="h-4" />
|
||||
<div className="mt-auto">
|
||||
<DedicatedEngineSubscriptionButton team={props.team} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center text-muted-foreground text-sm">
|
||||
|
||||
-4
@@ -7,7 +7,6 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { getEngineInstances } from "../_utils/getEngineInstances";
|
||||
import { ImportEngineButton } from "./import/import-engine-dialog";
|
||||
import { DedicatedEngineSubscriptionButton } from "./overview/engine-instances-table";
|
||||
import { EngineInstancesList } from "./overview/engine-list";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -61,9 +60,6 @@ export default async function Page(props: {
|
||||
title: "Engine",
|
||||
description: "Manage your deployed Engine instances.",
|
||||
actions: {
|
||||
primary: {
|
||||
component: <DedicatedEngineSubscriptionButton team={team} />,
|
||||
},
|
||||
secondary: {
|
||||
component: (
|
||||
<ImportEngineButton
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { CardHeading } from "../../payments/components/common";
|
||||
import { CardHeading } from "../../../payments/components/common";
|
||||
|
||||
export function TopInsightChainsTable(props: {
|
||||
data: InsightChainStats[];
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { CardHeading } from "../../payments/components/common";
|
||||
import { CardHeading } from "../../../payments/components/common";
|
||||
|
||||
export function TopInsightEndpointsTable(props: {
|
||||
data: InsightEndpointStats[];
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
export function InsightFTUX(_props: { clientId: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="border-b px-4 py-4 lg:px-6">
|
||||
<h2 className="font-semibold text-xl tracking-tight">Indexer</h2>
|
||||
</div>
|
||||
<div className="px-4 py-6 lg:p-6">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Indexed data is automatically used when using the thirdweb SDKs and
|
||||
API where appropriate.
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Once you have integrated thirdweb into your application you will start
|
||||
seeing usage patterns here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 border-t p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
|
||||
<div className="flex gap-3"></div>
|
||||
|
||||
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
|
||||
<span className="!pointer-events-auto relative flex size-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
Waiting for integration
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
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";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { InsightAnalytics } from "./components/InsightAnalytics";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
}>;
|
||||
}) {
|
||||
const [params, authToken] = await Promise.all([props.params, getAuthToken()]);
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/gateway/indexer`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const searchParams = await props.searchParams;
|
||||
const { range, interval } = getFiltersFromSearchParams({
|
||||
defaultRange: "last-30",
|
||||
from: searchParams.from,
|
||||
interval: searchParams.interval,
|
||||
to: searchParams.to,
|
||||
});
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
icon: InsightIcon,
|
||||
title: "Indexer",
|
||||
description:
|
||||
"Indexed data for EVM chains, available automatically via the thirdweb SDKs and API.",
|
||||
actions: null,
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/contracts/events",
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://portal.thirdweb.com/reference",
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<InsightAnalytics
|
||||
client={client}
|
||||
interval={interval}
|
||||
projectClientId={project.publishableKey}
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
</ProjectPage>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { CardHeading } from "../../payments/components/common";
|
||||
import { CardHeading } from "../../../payments/components/common";
|
||||
|
||||
export function TopRPCMethodsTable(props: {
|
||||
data: RpcMethodStats[];
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { CodeServer } from "@/components/ui/code/code.server";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { ClientIDSection } from "../../components/ProjectFTUX/ClientIDSection";
|
||||
import { WaitingForIntegrationCard } from "../../components/WaitingForIntegrationCard/WaitingForIntegrationCard";
|
||||
import { ClientIDSection } from "../../../components/ProjectFTUX/ClientIDSection";
|
||||
import { WaitingForIntegrationCard } from "../../../components/WaitingForIntegrationCard/WaitingForIntegrationCard";
|
||||
|
||||
export function RpcFTUX(props: { clientId: string }) {
|
||||
return (
|
||||
+3
-1
@@ -25,7 +25,9 @@ export default async function Page(props: {
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/rpc`);
|
||||
loginRedirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/gateway/rpc`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import { CodeServer } from "@/components/ui/code/code.server";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { ClientIDSection } from "../../components/ProjectFTUX/ClientIDSection";
|
||||
import { WaitingForIntegrationCard } from "../../components/WaitingForIntegrationCard/WaitingForIntegrationCard";
|
||||
|
||||
export function InsightFTUX(props: { clientId: string }) {
|
||||
return (
|
||||
<WaitingForIntegrationCard
|
||||
codeTabs={[
|
||||
{
|
||||
code: (
|
||||
<CodeServer
|
||||
className="bg-background"
|
||||
code={jsCode(props.clientId)}
|
||||
lang="ts"
|
||||
/>
|
||||
),
|
||||
label: "JavaScript",
|
||||
},
|
||||
{
|
||||
code: (
|
||||
<CodeServer
|
||||
className="bg-background"
|
||||
code={pythonCode(props.clientId)}
|
||||
lang="python"
|
||||
/>
|
||||
),
|
||||
label: "Python",
|
||||
},
|
||||
{
|
||||
code: (
|
||||
<CodeServer
|
||||
className="bg-background"
|
||||
code={curlCode(props.clientId)}
|
||||
lang="bash"
|
||||
/>
|
||||
),
|
||||
label: "Curl",
|
||||
},
|
||||
]}
|
||||
ctas={[
|
||||
{
|
||||
href: "https://portal.thirdweb.com/insight",
|
||||
label: "View Docs",
|
||||
},
|
||||
]}
|
||||
title="Integrate Insight"
|
||||
>
|
||||
<ClientIDSection clientId={props.clientId} />
|
||||
<div className="h-4" />
|
||||
</WaitingForIntegrationCard>
|
||||
);
|
||||
}
|
||||
|
||||
const twDomain = isProd ? "thirdweb" : "thirdweb-dev";
|
||||
|
||||
const jsCode = (clientId: string) => `\
|
||||
// Example: Get latest 10 transactions on Ethereum
|
||||
const res = await fetch("https://insight.${twDomain}.com/v1/transactions?chain=1&limit=10&clientId=${clientId}");
|
||||
const data = await res.json();
|
||||
`;
|
||||
|
||||
const curlCode = (clientId: string) => `\
|
||||
# Example: Get latest 10 transactions on Ethereum
|
||||
curl -X GET "https://insight.${twDomain}.com/v1/transactions?chain=1&limit=10&clientId=${clientId}"
|
||||
`;
|
||||
|
||||
const pythonCode = (clientId: string) => `\
|
||||
# Example: Get latest 10 transactions on Ethereum
|
||||
import requests
|
||||
|
||||
response = requests.get("https://insight.${twDomain}.com/v1/transactions?chain=1&limit=10&clientId=${clientId}")
|
||||
data = response.json()
|
||||
`;
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
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";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { InsightAnalytics } from "./components/InsightAnalytics";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
}>;
|
||||
}) {
|
||||
const [params, authToken] = await Promise.all([props.params, getAuthToken()]);
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/insight`);
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const searchParams = await props.searchParams;
|
||||
const { range, interval } = getFiltersFromSearchParams({
|
||||
defaultRange: "last-30",
|
||||
from: searchParams.from,
|
||||
interval: searchParams.interval,
|
||||
to: searchParams.to,
|
||||
});
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
client,
|
||||
icon: InsightIcon,
|
||||
title: "Insight",
|
||||
description:
|
||||
"APIs to retrieve blockchain data from any EVM chain, enrich it with metadata, and transform it using custom logic.",
|
||||
actions: null,
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/insight",
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
href: "https://insight-api.thirdweb.com/reference",
|
||||
},
|
||||
{
|
||||
type: "webhooks",
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<ResponsiveSearchParamsProvider value={searchParams}>
|
||||
<div>
|
||||
<InsightAnalytics
|
||||
client={client}
|
||||
interval={interval}
|
||||
projectClientId={project.publishableKey}
|
||||
projectId={project.id}
|
||||
range={range}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
|
||||
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-medium text-lg">
|
||||
Get Started with Insight
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
A cross-chain API for historic blockchain data.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
|
||||
href="https://portal.thirdweb.com/insight"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn More
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
</ProjectPage>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { TeamHeaderLoggedIn } from "../../../components/TeamHeader/team-header-l
|
||||
import { StaffModeNotice } from "../../(team)/_components/StaffModeNotice";
|
||||
import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout";
|
||||
import { SaveLastUsedProject } from "./components/SaveLastUsedProject";
|
||||
import { getEngineInstances } from "./engine/_utils/getEngineInstances";
|
||||
|
||||
export default async function ProjectLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -20,15 +21,34 @@ export default async function ProjectLayout(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const [accountAddress, teams, account, authToken, team, project] =
|
||||
await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getTeams(),
|
||||
getValidAccount(`/team/${params.team_slug}/${params.project_slug}`),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
]);
|
||||
const [
|
||||
accountAddress,
|
||||
teams,
|
||||
account,
|
||||
authToken,
|
||||
team,
|
||||
project,
|
||||
hasLegacyDedicatedEngines,
|
||||
] = await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getTeams(),
|
||||
getValidAccount(`/team/${params.team_slug}/${params.project_slug}`),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
(async () => {
|
||||
const authToken = await getAuthToken();
|
||||
if (!authToken) {
|
||||
return false;
|
||||
}
|
||||
const instances = await getEngineInstances({
|
||||
authToken,
|
||||
teamIdOrSlug: params.team_slug,
|
||||
});
|
||||
// if there are any engine instances, return true, otherwise false
|
||||
return !!instances?.data?.length;
|
||||
})(),
|
||||
]);
|
||||
|
||||
if (!teams || !accountAddress || !authToken) {
|
||||
redirect("/login");
|
||||
@@ -73,7 +93,10 @@ export default async function ProjectLayout(props: {
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
/>
|
||||
</div>
|
||||
<ProjectSidebarLayout layoutPath={layoutPath}>
|
||||
<ProjectSidebarLayout
|
||||
layoutPath={layoutPath}
|
||||
hasEngines={hasLegacyDedicatedEngines}
|
||||
>
|
||||
{props.children}
|
||||
</ProjectSidebarLayout>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
isProjectIcon: true,
|
||||
icon: () => (
|
||||
<ProjectAvatar
|
||||
className="size-12"
|
||||
className="size-11"
|
||||
client={client}
|
||||
src={project.image ?? ""}
|
||||
/>
|
||||
@@ -130,30 +130,44 @@ export default async function ProjectOverviewPage(props: PageProps) {
|
||||
actions: null,
|
||||
}}
|
||||
>
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
projectWallet={projectWallet}
|
||||
client={client}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
{isActive ? (
|
||||
<div className="flex flex-col gap-4 md:gap-6">
|
||||
<ResponsiveTimeFilters defaultRange={defaultRange} />
|
||||
<ProjectAnalytics
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
interval={interval}
|
||||
params={params}
|
||||
<div className="flex flex-col gap-4 md:gap-10">
|
||||
<ProjectWalletSection
|
||||
project={project}
|
||||
range={range}
|
||||
searchParams={searchParams}
|
||||
teamSlug={params.team_slug}
|
||||
projectWallet={projectWallet}
|
||||
client={client}
|
||||
layout="column"
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ResponsiveTimeFilters defaultRange={defaultRange} />
|
||||
<ProjectAnalytics
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
interval={interval}
|
||||
params={params}
|
||||
project={project}
|
||||
range={range}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ProjectFTUX project={project} teamSlug={params.team_slug} />
|
||||
<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>
|
||||
|
||||
+12
-8
@@ -9,7 +9,7 @@ import { Card } from "@/components/ui/card";
|
||||
|
||||
type FeatureCardProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
description: string | undefined;
|
||||
badge?: { label: string; variant?: "outline" | "success" | "default" };
|
||||
id: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
@@ -49,14 +49,18 @@ export function FeatureCard(props: FeatureCardProps) {
|
||||
</div>
|
||||
|
||||
{/* title and description */}
|
||||
<h3 className="font-semibold text-lg text-foreground mb-1">
|
||||
{props.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm font-medium text-pretty">
|
||||
{props.description}
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold text-lg text-foreground">
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.description && (
|
||||
<p className="text-muted-foreground text-sm font-medium text-pretty">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
<div className="h-3" />
|
||||
|
||||
<div className="space-y-3">
|
||||
{props.setupTime && (
|
||||
|
||||
+35
-26
@@ -15,14 +15,16 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow as TableRowComponent,
|
||||
} from "@/components/ui/table";
|
||||
import { TableData, TableHeading, TableHeadingRow } from "./common";
|
||||
import { formatTokenAmount } from "./format";
|
||||
import { TableRow } from "./PaymentsTableRow";
|
||||
import { PurchaseTableRow } from "./PaymentsTableRow";
|
||||
|
||||
const pageSize = 20;
|
||||
const pageSize = 10;
|
||||
|
||||
export function PaymentHistory(props: {
|
||||
client: ThirdwebClient;
|
||||
@@ -79,14 +81,15 @@ export function PaymentHistory(props: {
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHeadingRow>
|
||||
<TableHeading> Sent </TableHeading>
|
||||
<TableHeading> Received </TableHeading>
|
||||
<TableHeading>Type</TableHeading>
|
||||
<TableHeading>Status</TableHeading>
|
||||
<TableHeading>Recipient</TableHeading>
|
||||
<TableHeading>Date</TableHeading>
|
||||
</TableHeadingRow>
|
||||
<TableRowComponent>
|
||||
<TableHead> Sent </TableHead>
|
||||
<TableHead> Received </TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Recipient</TableHead>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead>Transactions</TableHead>
|
||||
</TableRowComponent>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(!isEmpty || isLoading) &&
|
||||
@@ -95,7 +98,7 @@ export function PaymentHistory(props: {
|
||||
.filter(isBridgePayment)
|
||||
.map((purchase) => {
|
||||
return (
|
||||
<TableRow
|
||||
<PurchaseTableRow
|
||||
client={props.client}
|
||||
key={purchase.id}
|
||||
purchase={purchase}
|
||||
@@ -174,25 +177,31 @@ function getCSVData(data: BridgePayment[]) {
|
||||
|
||||
function SkeletonTableRow() {
|
||||
return (
|
||||
<tr className="border-border border-b">
|
||||
<TableData>
|
||||
<TableRowComponent className="h-[73px]">
|
||||
<TableCell>
|
||||
<Skeleton className="h-7 w-20" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-7 w-20" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-7 w-20 rounded-2xl" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-7 w-20 rounded-2xl" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-7 w-[140px]" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-7 w-[200px]" />
|
||||
</TableData>
|
||||
</tr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-[100px]" />
|
||||
<Skeleton className="h-5 w-[100px]" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRowComponent>
|
||||
);
|
||||
}
|
||||
|
||||
+26
-10
@@ -3,14 +3,12 @@ import { type ThirdwebClient, toTokens } from "thirdweb";
|
||||
import type { BridgePayment } from "@/api/universal-bridge/developer";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
TableCell,
|
||||
TableRow as TableRowComponent,
|
||||
} from "@/components/ui/table";
|
||||
import { LinkWithCopyButton } from "@/components/ui/link-with-copy-button";
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatTokenAmount } from "./format";
|
||||
|
||||
export function TableRow(props: {
|
||||
export function PurchaseTableRow(props: {
|
||||
purchase: BridgePayment;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
@@ -34,10 +32,7 @@ export function TableRow(props: {
|
||||
})();
|
||||
|
||||
return (
|
||||
<TableRowComponent
|
||||
className="fade-in-0 border-border border-b duration-300"
|
||||
key={purchase.id}
|
||||
>
|
||||
<TableRow key={purchase.id}>
|
||||
{/* Paid */}
|
||||
<TableCell>{`${formatTokenAmount(originAmount)} ${purchase.originToken.symbol}`}</TableCell>
|
||||
|
||||
@@ -88,6 +83,27 @@ export function TableRow(props: {
|
||||
{format(new Date(purchase.createdAt), "LLL dd, y h:mm a")}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRowComponent>
|
||||
|
||||
{/* tx Hash */}
|
||||
<TableCell>
|
||||
{purchase.transactions.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{purchase.transactions.map((tx) => (
|
||||
<LinkWithCopyButton
|
||||
key={tx.transactionHash}
|
||||
className="-translate-x-1"
|
||||
href={`/${tx.chainId}/tx/${tx.transactionHash}`}
|
||||
textToShow={`${tx.transactionHash.slice(0, 6)}...${tx.transactionHash.slice(-4)}`}
|
||||
textToCopy={tx.transactionHash}
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
linkIconClassName="hidden"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">N/A</p>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ export function QuickStartSection(props: {
|
||||
"Directly to your wallet",
|
||||
]}
|
||||
link={{
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/settings/payments`,
|
||||
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
|
||||
label: "Configure Fees",
|
||||
}}
|
||||
/>
|
||||
|
||||
+10
-10
@@ -72,7 +72,7 @@ export default async function Page(props: {
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/settings/payments`,
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/bridge/configuration`,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
@@ -81,7 +81,7 @@ export default async function Page(props: {
|
||||
},
|
||||
{
|
||||
type: "playground",
|
||||
href: "https://playground.thirdweb.com/payments/ui-components",
|
||||
href: "https://playground.thirdweb.com/bridge",
|
||||
},
|
||||
{
|
||||
type: "api",
|
||||
@@ -93,20 +93,20 @@ export default async function Page(props: {
|
||||
center: {
|
||||
links: [
|
||||
{
|
||||
href: "https://playground.thirdweb.com/payments/ui-components",
|
||||
label: "UI Component",
|
||||
href: "https://playground.thirdweb.com/bridge/buy-widget",
|
||||
label: "Buy Widget",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/payments/fund-wallet",
|
||||
label: "Buy Crypto",
|
||||
href: "https://playground.thirdweb.com/bridge/checkout-widget",
|
||||
label: "Checkout Widget",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/payments/commerce",
|
||||
label: "Checkout",
|
||||
href: "https://playground.thirdweb.com/bridge/transaction-widget",
|
||||
label: "Transaction Widget",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/payments/transactions",
|
||||
label: "Transactions",
|
||||
href: "https://playground.thirdweb.com/bridge/swap-widget",
|
||||
label: "Swap Widget",
|
||||
},
|
||||
],
|
||||
title: "Demos",
|
||||
|
||||
+43
-22
@@ -2,9 +2,11 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
DotIcon,
|
||||
InfoIcon,
|
||||
MoreVerticalIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
updateWebhook,
|
||||
} from "@/api/universal-bridge/developer";
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
@@ -435,28 +438,30 @@ function BridgeWebhookModalContent(
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="v2" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2">v2</SelectItem>
|
||||
<SelectItem value="1">v1</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select the data format of the webhook payload (v2 recommended,
|
||||
v1 for legacy users).
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{props.type === "edit" && props.webhook.version === 1 && (
|
||||
<FormField
|
||||
name="version"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Version</FormLabel>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="v2" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2">v2</SelectItem>
|
||||
<SelectItem value="1">v1</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select the data format of the webhook payload (v2 recommended,
|
||||
v1 for legacy users).
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.type === "create" && (
|
||||
<section>
|
||||
@@ -486,6 +491,22 @@ function BridgeWebhookModalContent(
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Alert variant="warning">
|
||||
<InfoIcon className="size-4" />
|
||||
<AlertTitle> Verify payload before processing </AlertTitle>
|
||||
<AlertDescription>
|
||||
Make sure to verify receiver, destination chain, token address and
|
||||
amount to ensure that it represents the expected state of the
|
||||
transaction before processing it in your backend.{" "}
|
||||
<UnderlineLink
|
||||
href="https://portal.thirdweb.com/bridge/webhooks#verify-payload"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</UnderlineLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="gap-2"
|
||||
|
||||
+3
-3
@@ -220,10 +220,10 @@ export function ProjectGeneralSettingsPageUI(props: {
|
||||
const projectLayout = `/team/${props.teamSlug}/${props.project.slug}`;
|
||||
|
||||
const paths = {
|
||||
aaConfig: `${projectLayout}/settings/account-abstraction`,
|
||||
aaConfig: `${projectLayout}/wallets/sponsored-gas/configuration`,
|
||||
afterDeleteRedirectTo: `/team/${props.teamSlug}`,
|
||||
inAppConfig: `${projectLayout}/settings/wallets`,
|
||||
payConfig: `${projectLayout}/settings/payments`,
|
||||
inAppConfig: `${projectLayout}/wallets/user-wallets/configuration`,
|
||||
payConfig: `${projectLayout}/bridge/configuration`,
|
||||
};
|
||||
|
||||
const { project } = props;
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
|
||||
export function ProjectSettingsBreadcrumb(props: {
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
page: string;
|
||||
}) {
|
||||
const { teamSlug, projectSlug } = props;
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/team/${teamSlug}/${projectSlug}/settings`}>
|
||||
Project Settings
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{props.page}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { DefaultFactoriesSection } from "../../account-abstraction/factories/AccountFactories";
|
||||
import { YourFactoriesSection } from "../../account-abstraction/factories/AccountFactories/your-factories";
|
||||
import { AccountAbstractionSettingsPage } from "../../account-abstraction/settings/SponsorshipPolicies";
|
||||
import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const { team_slug, project_slug } = await props.params;
|
||||
|
||||
const [team, project, authToken] = await Promise.all([
|
||||
getTeamBySlug(team_slug),
|
||||
getProject(team_slug, project_slug),
|
||||
|
||||
getAuthToken(),
|
||||
]);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${team_slug}/settings/account-abstraction`);
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${team_slug}`);
|
||||
}
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const bundlerService = project.services.find((s) => s.name === "bundler");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProjectSettingsBreadcrumb
|
||||
teamSlug={team_slug}
|
||||
projectSlug={project_slug}
|
||||
page="Account Abstraction"
|
||||
/>
|
||||
|
||||
<div className="space-y-8 mt-6">
|
||||
{!bundlerService ? (
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Account Abstraction service is disabled</AlertTitle>
|
||||
<AlertDescription>
|
||||
Enable Account Abstraction service in{" "}
|
||||
<UnderlineLink
|
||||
href={`/team/${team_slug}/${project_slug}/settings`}
|
||||
>
|
||||
project settings
|
||||
</UnderlineLink>{" "}
|
||||
to configure the sponsorship rules
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<AccountAbstractionSettingsPage
|
||||
bundlerService={bundlerService}
|
||||
client={client}
|
||||
project={project}
|
||||
teamId={team.id}
|
||||
teamSlug={team.slug}
|
||||
validTeamPlan={getValidTeamPlan(team)}
|
||||
/>
|
||||
|
||||
<DefaultFactoriesSection />
|
||||
<YourFactoriesSection
|
||||
authToken={authToken}
|
||||
clientThirdwebClient={client}
|
||||
projectId={project.id}
|
||||
projectSlug={project.slug}
|
||||
teamId={team.id}
|
||||
teamSlug={team.slug}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-51
@@ -1,6 +1,4 @@
|
||||
import "server-only";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "@/constants/server-envs";
|
||||
|
||||
export type SMSCountryTiers = {
|
||||
tier1: string[];
|
||||
@@ -10,52 +8,3 @@ export type SMSCountryTiers = {
|
||||
tier5: string[];
|
||||
tier6: string[];
|
||||
};
|
||||
|
||||
export async function getSMSCountryTiers() {
|
||||
if (!API_SERVER_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/sms/list-country-tiers`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": API_SERVER_SECRET,
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Failed to fetch sms country tiers",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
res.body?.cancel();
|
||||
return {
|
||||
tier1: [],
|
||||
tier2: [],
|
||||
tier3: [],
|
||||
tier4: [],
|
||||
tier5: [],
|
||||
tier6: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return (await res.json()).data as SMSCountryTiers;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse sms country tiers", e);
|
||||
return {
|
||||
tier1: [],
|
||||
tier2: [],
|
||||
tier3: [],
|
||||
tier4: [],
|
||||
tier5: [],
|
||||
tier6: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+3
-81
@@ -2,7 +2,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils";
|
||||
import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
|
||||
@@ -13,7 +13,6 @@ import type { Project } from "@/api/project/projects";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { FileInput } from "@/components/blocks/FileInput";
|
||||
import { GatedSwitch } from "@/components/blocks/GatedSwitch";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import {
|
||||
@@ -31,7 +30,6 @@ import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { planToTierRecordForGating } from "@/constants/planToTierRecord";
|
||||
import { updateProjectClient } from "@/hooks/useApi";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type ApiKeyEmbeddedWalletsValidationSchema,
|
||||
@@ -57,82 +55,6 @@ type UpdateAPIKeyTrackingData = {
|
||||
hasCustomAuthEndpoint: boolean;
|
||||
};
|
||||
|
||||
export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
|
||||
const updateProject = useMutation({
|
||||
mutationFn: async (projectValues: Partial<Project>) => {
|
||||
await updateProjectClient(
|
||||
{
|
||||
projectId: props.project.id,
|
||||
teamId: props.teamId,
|
||||
},
|
||||
projectValues,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function handleUpdateProject(projectValues: Partial<Project>) {
|
||||
updateProject.mutate(projectValues, {
|
||||
onError: (err) => {
|
||||
toast.error("Failed to update an API Key");
|
||||
console.error(err);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("In-App Wallet API Key configuration updated");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<InAppWalletSettingsPageUI
|
||||
{...props}
|
||||
isUpdating={updateProject.isPending}
|
||||
smsCountryTiers={props.smsCountryTiers}
|
||||
updateApiKey={handleUpdateProject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const InAppWalletSettingsPageUI: React.FC<
|
||||
InAppWalletSettingsPageProps & {
|
||||
updateApiKey: (
|
||||
projectValues: Partial<Project>,
|
||||
trackingData: UpdateAPIKeyTrackingData,
|
||||
) => void;
|
||||
isUpdating: boolean;
|
||||
smsCountryTiers: SMSCountryTiers;
|
||||
}
|
||||
> = (props) => {
|
||||
const embeddedWalletService = props.project.services.find(
|
||||
(service) => service.name === "embeddedWallets",
|
||||
);
|
||||
|
||||
if (!embeddedWalletService) {
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>In-App wallets service is disabled</AlertTitle>
|
||||
<AlertDescription>
|
||||
Enable In-App wallets service in the{" "}
|
||||
<UnderlineLink
|
||||
href={`/team/${props.teamSlug}/${props.project.slug}/settings`}
|
||||
>
|
||||
project settings
|
||||
</UnderlineLink>{" "}
|
||||
to configure settings
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InAppWalletSettingsUI
|
||||
{...props}
|
||||
client={props.client}
|
||||
embeddedWalletService={embeddedWalletService}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InAppWalletSettingsUI: React.FC<
|
||||
InAppWalletSettingsPageProps & {
|
||||
updateApiKey: (
|
||||
@@ -191,7 +113,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
) {
|
||||
return toast.error("Custom JSON Web Token configuration is invalid", {
|
||||
description:
|
||||
"To use In-App Wallets with Custom JSON Web Token, provide JWKS URI and AUD.",
|
||||
"To use User Wallets with Custom JSON Web Token, provide JWKS URI and AUD.",
|
||||
dismissible: true,
|
||||
duration: 9000,
|
||||
});
|
||||
@@ -202,7 +124,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
"Custom Authentication Endpoint configuration is invalid",
|
||||
{
|
||||
description:
|
||||
"To use In-App Wallets with Custom Authentication Endpoint, provide a valid URL.",
|
||||
"To use User Wallets with Custom Authentication Endpoint, provide a valid URL.",
|
||||
dismissible: true,
|
||||
duration: 9000,
|
||||
},
|
||||
|
||||
+88
-31
@@ -1,45 +1,102 @@
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { getLastNDaysRange } from "@/components/analytics/date-range-selector";
|
||||
import { normalizeTimeISOString } from "@/lib/time";
|
||||
import { UnifiedTransactionsTable } from "../components/transactions-table.client";
|
||||
import { getTransactionAnalyticsSummary } from "../lib/analytics-summary.client";
|
||||
import type { Wallet } from "../server-wallets/wallet-table/types";
|
||||
import { TransactionAnalyticsFilter } from "./filter";
|
||||
import { TransactionsChartCard } from "./tx-chart/tx-chart";
|
||||
import type { SolanaWallet } from "../solana-wallets/wallet-table/types";
|
||||
import { EngineChecklist } from "./ftux.client";
|
||||
import { TransactionsAnalytics } from "./tx-chart/tx-chart";
|
||||
|
||||
export function TransactionsAnalyticsPageContent(props: {
|
||||
searchParams: {
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
};
|
||||
project: Project;
|
||||
showAnalytics: boolean;
|
||||
wallets?: Wallet[];
|
||||
wallets: Wallet[];
|
||||
teamSlug: string;
|
||||
client: ThirdwebClient;
|
||||
authToken: string;
|
||||
teamId: string;
|
||||
isManagedVault: boolean;
|
||||
testTxWithWallet: string | undefined;
|
||||
testSolanaTxWithWallet: string | undefined;
|
||||
solanaWallets: SolanaWallet[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveSearchParamsProvider value={props.searchParams}>
|
||||
<div className="flex grow flex-col gap-6">
|
||||
{props.showAnalytics && (
|
||||
<>
|
||||
<div className="flex justify-end">
|
||||
<TransactionAnalyticsFilter />
|
||||
</div>
|
||||
<TransactionsChartCard
|
||||
project={props.project}
|
||||
searchParams={props.searchParams}
|
||||
teamSlug={props.teamSlug}
|
||||
wallets={props.wallets ?? []}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<UnifiedTransactionsTable
|
||||
client={props.client}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
const defaultRange = useMemo(() => {
|
||||
const range = getLastNDaysRange("last-30");
|
||||
return {
|
||||
from: normalizeTimeISOString(range.from),
|
||||
to: normalizeTimeISOString(range.to),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const engineTxSummaryQuery = useQuery({
|
||||
queryKey: [
|
||||
"engine-tx-analytics-summary",
|
||||
props.teamId,
|
||||
props.project.publishableKey,
|
||||
props.authToken,
|
||||
defaultRange.from,
|
||||
defaultRange.to,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const data = await getTransactionAnalyticsSummary({
|
||||
clientId: props.project.publishableKey,
|
||||
teamId: props.teamId,
|
||||
authToken: props.authToken,
|
||||
startDate: defaultRange.from,
|
||||
endDate: defaultRange.to,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (engineTxSummaryQuery.isPending) {
|
||||
return (
|
||||
<div className="flex h-[642px] grow items-center justify-center bg-card rounded-xl border">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const hasTransactions = engineTxSummaryQuery.data
|
||||
? engineTxSummaryQuery.data.totalCount > 0
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-10">
|
||||
<EngineChecklist
|
||||
isManagedVault={props.isManagedVault}
|
||||
client={props.client}
|
||||
hasTransactions={hasTransactions}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
testTxWithWallet={props.testTxWithWallet}
|
||||
testSolanaTxWithWallet={props.testSolanaTxWithWallet}
|
||||
wallets={props.wallets}
|
||||
solanaWallets={props.solanaWallets}
|
||||
/>
|
||||
|
||||
{props.showAnalytics && hasTransactions && (
|
||||
<TransactionsAnalytics
|
||||
project={props.project}
|
||||
authToken={props.authToken}
|
||||
teamSlug={props.teamSlug}
|
||||
wallets={props.wallets ?? []}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UnifiedTransactionsTable
|
||||
client={props.client}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useResponsiveSearchParams,
|
||||
useSetResponsiveSearchParams,
|
||||
} from "responsive-rsc";
|
||||
import { DateRangeSelector } from "@/components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "@/components/analytics/interval-selector";
|
||||
import { normalizeTimeISOString } from "@/lib/time";
|
||||
import { getTxAnalyticsFiltersFromSearchParams } from "../lib/utils";
|
||||
|
||||
export function TransactionAnalyticsFilter() {
|
||||
const responsiveSearchParams = useResponsiveSearchParams();
|
||||
const setResponsiveSearchParams = useSetResponsiveSearchParams();
|
||||
|
||||
const { range, interval } = getTxAnalyticsFiltersFromSearchParams({
|
||||
from: responsiveSearchParams.from,
|
||||
interval: responsiveSearchParams.interval,
|
||||
to: responsiveSearchParams.to,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
|
||||
<DateRangeSelector
|
||||
popoverAlign="end"
|
||||
range={range}
|
||||
setRange={(newRange) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
return {
|
||||
...v,
|
||||
from: normalizeTimeISOString(newRange.from),
|
||||
to: normalizeTimeISOString(newRange.to),
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<IntervalSelector
|
||||
intervalType={interval}
|
||||
setIntervalType={(newInterval) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
return {
|
||||
...v,
|
||||
interval: newInterval,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+36
-3
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ActivityIcon, CoinsIcon } from "lucide-react";
|
||||
import { toEther } from "thirdweb/utils";
|
||||
import { StatCard } from "@/components/analytics/stat"; // Assuming correct path
|
||||
import type { TransactionSummaryData } from "../lib/analytics";
|
||||
import {
|
||||
getTransactionAnalyticsSummary,
|
||||
type TransactionSummaryData,
|
||||
} from "../lib/analytics-summary.client";
|
||||
|
||||
// Renders the UI based on fetched data or pending state
|
||||
function TransactionAnalyticsSummaryUI(props: {
|
||||
@@ -88,9 +93,37 @@ function TransactionAnalyticsSummaryUI(props: {
|
||||
export function TransactionAnalyticsSummary(props: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
initialData: TransactionSummaryData | undefined;
|
||||
authToken: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const engineTxSummaryQuery = useQuery({
|
||||
queryKey: [
|
||||
"engine-tx-analytics-summary",
|
||||
props.teamId,
|
||||
props.clientId,
|
||||
props.authToken,
|
||||
props.startDate,
|
||||
props.endDate,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const data = await getTransactionAnalyticsSummary({
|
||||
clientId: props.clientId,
|
||||
teamId: props.teamId,
|
||||
authToken: props.authToken,
|
||||
startDate: props.startDate,
|
||||
endDate: props.endDate,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<TransactionAnalyticsSummaryUI data={props.initialData} isPending={false} />
|
||||
<TransactionAnalyticsSummaryUI
|
||||
data={engineTxSummaryQuery.data}
|
||||
isPending={engineTxSummaryQuery.isPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
|
||||
import type { TransactionStats } from "@/types/analytics";
|
||||
|
||||
export async function getTransactionsChartData({
|
||||
teamId,
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
interval,
|
||||
authToken,
|
||||
}: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
interval: "day" | "week";
|
||||
authToken: string;
|
||||
}): Promise<TransactionStats[]> {
|
||||
const filters = {
|
||||
endDate: to,
|
||||
resolution: interval,
|
||||
startDate: from,
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`,
|
||||
{
|
||||
body: JSON.stringify(filters),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
"x-team-id": teamId,
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 400) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO - need to handle this error state, like we do with the connect charts
|
||||
throw new Error(
|
||||
`Error fetching transactions chart data: ${response.status} ${
|
||||
response.statusText
|
||||
} - ${await response.text().catch(() => "Unknown error")}`,
|
||||
);
|
||||
}
|
||||
|
||||
type TransactionsChartResponse = {
|
||||
result: {
|
||||
analytics: Array<{
|
||||
timeBucket: string;
|
||||
chainId: string;
|
||||
count: number;
|
||||
}>;
|
||||
metadata: {
|
||||
resolution: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const data = (await response.json()) as TransactionsChartResponse;
|
||||
|
||||
return data.result.analytics.map((stat) => ({
|
||||
chainId: Number(stat.chainId),
|
||||
count: stat.count,
|
||||
date: stat.timeBucket,
|
||||
}));
|
||||
}
|
||||
+4
-10
@@ -108,10 +108,10 @@ export function TransactionsChartCardUI(props: {
|
||||
customHeader={
|
||||
<div className="relative px-6 pt-6">
|
||||
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
Daily Transactions
|
||||
Transactions
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Amount of daily transactions by chain.
|
||||
transactions broken down by chain
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-8 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
@@ -184,7 +184,7 @@ function EmptyChartContent(props: {
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/team/${props.teamSlug}/${props.project.slug}/vault`,
|
||||
`/team/${props.teamSlug}/${props.project.slug}/wallets/server-wallets/configuration`,
|
||||
);
|
||||
}}
|
||||
variant="primary"
|
||||
@@ -194,13 +194,7 @@ function EmptyChartContent(props: {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
|
||||
<span className="!pointer-events-auto relative flex size-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
Waiting for transactions...
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">No transactions found</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+59
-53
@@ -1,73 +1,79 @@
|
||||
import { ResponsiveSuspense } from "responsive-rsc";
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { getTransactionsChart } from "../../lib/analytics";
|
||||
import { getTxAnalyticsFiltersFromSearchParams } from "../../lib/utils";
|
||||
import {
|
||||
DateRangeSelector,
|
||||
getLastNDaysRange,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "@/components/analytics/interval-selector";
|
||||
import { normalizeTimeISOString } from "@/lib/time";
|
||||
import type { Wallet } from "../../server-wallets/wallet-table/types";
|
||||
import { TransactionAnalyticsSummary } from "../summary";
|
||||
import { getTransactionsChartData } from "./data";
|
||||
import { TransactionsChartCardUI } from "./tx-chart-ui";
|
||||
|
||||
async function AsyncTransactionsChartCard(props: {
|
||||
from: string;
|
||||
to: string;
|
||||
interval: "day" | "week";
|
||||
export function TransactionsAnalytics(props: {
|
||||
project: Project;
|
||||
wallets: Wallet[];
|
||||
authToken: string;
|
||||
teamSlug: string;
|
||||
}) {
|
||||
const data = await getTransactionsChart({
|
||||
const [range, setRange] = useState(() => getLastNDaysRange("last-30"));
|
||||
const [interval, setInterval] = useState<"day" | "week">("day");
|
||||
|
||||
const normalizedFrom = normalizeTimeISOString(range.from);
|
||||
const normalizedTo = normalizeTimeISOString(range.to);
|
||||
|
||||
const params = {
|
||||
clientId: props.project.publishableKey,
|
||||
from: props.from,
|
||||
interval: props.interval,
|
||||
from: normalizedFrom,
|
||||
interval: interval,
|
||||
teamId: props.project.teamId,
|
||||
to: props.to,
|
||||
to: normalizedTo,
|
||||
authToken: props.authToken,
|
||||
};
|
||||
|
||||
const engineTxAnalytics = useQuery({
|
||||
queryKey: ["engine-tx-analytics", params],
|
||||
queryFn: async () => {
|
||||
const data = await getTransactionsChartData(params);
|
||||
return data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<TransactionsChartCardUI
|
||||
isPending={false}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
userOpStats={data}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-start">
|
||||
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
|
||||
<DateRangeSelector
|
||||
popoverAlign="end"
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
/>
|
||||
|
||||
export function TransactionsChartCard(props: {
|
||||
searchParams: {
|
||||
from?: string | undefined | string[];
|
||||
to?: string | undefined | string[];
|
||||
interval?: string | undefined | string[];
|
||||
};
|
||||
project: Project;
|
||||
wallets: Wallet[];
|
||||
teamSlug: string;
|
||||
}) {
|
||||
const { range, interval } = getTxAnalyticsFiltersFromSearchParams(
|
||||
props.searchParams,
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveSuspense
|
||||
// TODO - change this if this component does not end up using these params
|
||||
fallback={
|
||||
<TransactionsChartCardUI
|
||||
isPending={true}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
userOpStats={[]}
|
||||
wallets={[]}
|
||||
/>
|
||||
}
|
||||
searchParamsUsed={["from", "to", "interval"]}
|
||||
>
|
||||
<AsyncTransactionsChartCard
|
||||
from={range.from.toISOString()}
|
||||
interval={interval}
|
||||
<IntervalSelector
|
||||
intervalType={interval}
|
||||
setIntervalType={setInterval}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TransactionAnalyticsSummary
|
||||
authToken={props.authToken}
|
||||
clientId={props.project.publishableKey}
|
||||
teamId={props.project.teamId}
|
||||
startDate={normalizedFrom}
|
||||
endDate={normalizedTo}
|
||||
/>
|
||||
<TransactionsChartCardUI
|
||||
isPending={engineTxAnalytics.isPending}
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
to={range.to.toISOString()}
|
||||
userOpStats={engineTxAnalytics.data ?? []}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
</ResponsiveSuspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+56
-69
@@ -10,6 +10,7 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { FundWalletModal } from "@/components/blocks/fund-wallets-modal";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
|
||||
import { SolanaAddress } from "@/components/blocks/solana-address";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -32,14 +34,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -88,6 +82,7 @@ interface ServerWalletsTableProps {
|
||||
client: ThirdwebClient;
|
||||
solanaPermissionError?: boolean;
|
||||
authToken: string;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
@@ -105,6 +100,7 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
client,
|
||||
solanaPermissionError,
|
||||
authToken,
|
||||
pageSize,
|
||||
} = props;
|
||||
|
||||
const [activeChain, setActiveChain] = useState<WalletChain>("evm");
|
||||
@@ -333,13 +329,13 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
</TableContainer>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<WalletsPagination
|
||||
<ServerWalletsPagination
|
||||
activeChain={activeChain}
|
||||
currentPage={currentPage}
|
||||
currentCount={wallets.length}
|
||||
pageSize={pageSize}
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
teamSlug={teamSlug}
|
||||
projectSlug={project.slug}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -349,78 +345,69 @@ export function ServerWalletsTable(props: ServerWalletsTableProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Wallets Pagination Component
|
||||
function WalletsPagination({
|
||||
function ServerWalletsPagination({
|
||||
activeChain,
|
||||
currentPage,
|
||||
currentCount,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
teamSlug,
|
||||
projectSlug,
|
||||
}: {
|
||||
activeChain: WalletChain;
|
||||
currentPage: number;
|
||||
currentCount: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalRecords: number;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const pageParam = activeChain === "evm" ? "page" : "solana_page";
|
||||
|
||||
const effectivePageSize = pageSize > 0 ? pageSize : currentCount;
|
||||
const rangeStart =
|
||||
totalRecords && effectivePageSize
|
||||
? Math.min((currentPage - 1) * effectivePageSize + 1, totalRecords)
|
||||
: 0;
|
||||
const computedCount = currentCount
|
||||
? currentCount
|
||||
: totalRecords && effectivePageSize
|
||||
? Math.min(effectivePageSize, totalRecords - rangeStart + 1)
|
||||
: 0;
|
||||
const rangeEnd = totalRecords
|
||||
? Math.min(rangeStart + Math.max(computedCount - 1, 0), totalRecords)
|
||||
: 0;
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
const params = new URLSearchParams(searchParams?.toString() ?? "");
|
||||
if (page <= 1) {
|
||||
params.delete(pageParam);
|
||||
} else {
|
||||
params.set(pageParam, page.toString());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center border-t p-6">
|
||||
<div className="mb-4 text-muted-foreground text-sm">
|
||||
Found {totalRecords} {activeChain === "evm" ? "EVM" : "Solana"} wallets
|
||||
<div className="border-t px-6 py-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{totalRecords
|
||||
? `Showing ${rangeStart.toLocaleString()}-${rangeEnd.toLocaleString()} of ${totalRecords.toLocaleString()} ` +
|
||||
(activeChain === "evm" ? "EVM" : "Solana") +
|
||||
" wallets"
|
||||
: `No ${activeChain === "evm" ? "EVM" : "Solana"} wallets found`}
|
||||
</span>
|
||||
<PaginationButtons
|
||||
activePage={currentPage}
|
||||
className="justify-start lg:justify-end"
|
||||
onPageClick={handlePageChange}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${projectSlug}/transactions?${pageParam}=${
|
||||
currentPage > 1 ? currentPage - 1 : 1
|
||||
}`}
|
||||
legacyBehavior
|
||||
passHref
|
||||
>
|
||||
<PaginationPrevious
|
||||
className={
|
||||
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</PaginationItem>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(pageNumber) => (
|
||||
<PaginationItem key={`page-${pageNumber}`}>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${projectSlug}/transactions?${pageParam}=${pageNumber}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationLink isActive={currentPage === pageNumber}>
|
||||
{pageNumber}
|
||||
</PaginationLink>
|
||||
</Link>
|
||||
</PaginationItem>
|
||||
),
|
||||
)}
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${projectSlug}/transactions?${pageParam}=${
|
||||
currentPage < totalPages ? currentPage + 1 : totalPages
|
||||
}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationNext
|
||||
className={
|
||||
currentPage >= totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs";
|
||||
|
||||
export type TransactionSummaryData = {
|
||||
totalCount: number;
|
||||
totalGasCostWei: string;
|
||||
totalGasUnitsUsed: string; // Keep fetched data structure
|
||||
};
|
||||
|
||||
type AnalyticsSummaryApiResponse = {
|
||||
result: {
|
||||
summary: {
|
||||
totalCount: number;
|
||||
totalGasCostWei: string;
|
||||
totalGasUnitsUsed: string;
|
||||
};
|
||||
metadata: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Fetches data from the /analytics-summary endpoint
|
||||
export async function getTransactionAnalyticsSummary(props: {
|
||||
teamId: string;
|
||||
clientId: string;
|
||||
authToken: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<TransactionSummaryData> {
|
||||
const body: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
} = {};
|
||||
|
||||
if (props.startDate) {
|
||||
body.startDate = props.startDate;
|
||||
}
|
||||
|
||||
if (props.endDate) {
|
||||
body.endDate = props.endDate;
|
||||
}
|
||||
const defaultData: TransactionSummaryData = {
|
||||
totalCount: 0,
|
||||
totalGasCostWei: "0",
|
||||
totalGasUnitsUsed: "0",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`,
|
||||
{
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
Authorization: `Bearer ${props.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 400) {
|
||||
console.error("Unauthorized fetching transaction summary");
|
||||
return defaultData;
|
||||
}
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as AnalyticsSummaryApiResponse;
|
||||
|
||||
return {
|
||||
totalCount: data.result.summary.totalCount ?? 0,
|
||||
totalGasCostWei: data.result.summary.totalGasCostWei ?? "0",
|
||||
totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch transaction summary:", error);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user