Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60629e3cf3 | ||
|
|
178d407e6f | ||
|
|
7e1cb6d8d8 | ||
|
|
c2a90542d7 | ||
|
|
802d3bfd4b | ||
|
|
09b1912eb9 | ||
|
|
975cd0b384 | ||
|
|
436f6b79b5 | ||
|
|
df8b2f6f90 | ||
|
|
950ce60ec2 | ||
|
|
3658b3fece | ||
|
|
686d0c3c05 | ||
|
|
9425e9ef42 | ||
|
|
556e11373c | ||
|
|
6e87a866b2 | ||
|
|
043ee92598 | ||
|
|
89df3b5fa9 | ||
|
|
c9f52e9239 | ||
|
|
519349b5cf | ||
|
|
71a21e185e | ||
|
|
c8b5780627 | ||
|
|
68ce724ff6 | ||
|
|
ad4af68d1d |
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "../../lib/useIsomorphicLayoutEffect";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type imgElementProps = React.DetailedHTMLProps<
|
||||
@@ -25,19 +26,39 @@ export function Img(props: imgElementProps) {
|
||||
const { className, fallback, skeleton, ...restProps } = props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-muted" />;
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const imgEl = imgRef.current;
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
if (imgEl.complete) {
|
||||
setStatus("loaded");
|
||||
} else {
|
||||
function handleLoad() {
|
||||
setStatus("loaded");
|
||||
}
|
||||
imgEl.addEventListener("load", handleLoad);
|
||||
return () => {
|
||||
imgEl.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<img
|
||||
{...restProps}
|
||||
onLoad={() => {
|
||||
setStatus("loaded");
|
||||
}}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
src={restProps.src || undefined}
|
||||
ref={imgRef}
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
style={{
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
...restProps.style,
|
||||
}}
|
||||
alt={restProps.alt || ""}
|
||||
className={cn(
|
||||
@@ -48,6 +69,7 @@ export function Img(props: imgElementProps) {
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
style={restProps.style}
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
|
||||
export function GenericLoadingPage() {
|
||||
return (
|
||||
<div className="flex min-h-[500px] grow items-center justify-center rounded-lg border border-border">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -63,8 +63,11 @@ export const embeddedWalletsKeys = {
|
||||
all: ["embeddedWallets"] as const,
|
||||
wallet: (walletAddress: string) =>
|
||||
[...embeddedWalletsKeys.all, walletAddress] as const,
|
||||
embeddedWallets: (walletAddress: string, clientId: string | undefined) =>
|
||||
[...embeddedWalletsKeys.wallet(walletAddress), clientId] as const,
|
||||
embeddedWallets: (
|
||||
walletAddress: string,
|
||||
clientId: string | undefined,
|
||||
page: number,
|
||||
) => [...embeddedWalletsKeys.wallet(walletAddress), clientId, page] as const,
|
||||
};
|
||||
|
||||
export const engineKeys = {
|
||||
|
||||
@@ -12,21 +12,24 @@ import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
// FIXME: We keep repeating types, API server should provide them
|
||||
|
||||
export enum AccountStatus {
|
||||
NoCustomer = "noCustomer",
|
||||
NoPayment = "noPayment",
|
||||
PaymentVerification = "paymentVerification",
|
||||
ValidPayment = "validPayment",
|
||||
InvalidPayment = "invalidPayment",
|
||||
InvalidPaymentMethod = "invalidPaymentMethod",
|
||||
}
|
||||
export const accountStatus = {
|
||||
noCustomer: "noCustomer",
|
||||
noPayment: "noPayment",
|
||||
paymentVerification: "paymentVerification",
|
||||
validPayment: "validPayment",
|
||||
invalidPayment: "invalidPayment",
|
||||
invalidPaymentMethod: "invalidPaymentMethod",
|
||||
} as const;
|
||||
|
||||
export enum AccountPlan {
|
||||
Free = "free",
|
||||
Growth = "growth",
|
||||
Pro = "pro",
|
||||
Enterprise = "enterprise",
|
||||
}
|
||||
export const accountPlan = {
|
||||
free: "free",
|
||||
growth: "growth",
|
||||
pro: "pro",
|
||||
enterprise: "enterprise",
|
||||
} as const;
|
||||
|
||||
export type AccountStatus = (typeof accountStatus)[keyof typeof accountStatus];
|
||||
export type AccountPlan = (typeof accountPlan)[keyof typeof accountPlan];
|
||||
|
||||
export type AuthorizedWallet = {
|
||||
id: string;
|
||||
@@ -235,6 +238,12 @@ export interface WalletStats {
|
||||
walletType: string;
|
||||
}
|
||||
|
||||
export interface InAppWalletStats {
|
||||
date: string;
|
||||
authenticationMethod: string;
|
||||
uniqueWalletsConnected: number;
|
||||
}
|
||||
|
||||
export interface UserOpStats {
|
||||
date: string;
|
||||
successful: number;
|
||||
@@ -436,6 +445,43 @@ async function getUserOpUsage(args: {
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export async function getInAppWalletUsage(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const { clientId, from, to, period } = args;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("clientId", clientId);
|
||||
if (from) {
|
||||
searchParams.append("from", from.toISOString());
|
||||
}
|
||||
if (to) {
|
||||
searchParams.append("to", to.toISOString());
|
||||
}
|
||||
if (period) {
|
||||
searchParams.append("period", period);
|
||||
}
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets/in-app?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await res?.json();
|
||||
|
||||
if (!res || res.status !== 200) {
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export function useUserOpUsageAggregate(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { THIRDWEB_EWS_API_HOST } from "constants/urls";
|
||||
import type { WalletUser } from "thirdweb/wallets";
|
||||
import { embeddedWalletsKeys } from "../cache-keys";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
// FIXME: Make API to return camelCase or transform
|
||||
export type EmbeddedWalletUser = {
|
||||
id: string;
|
||||
client_id: string;
|
||||
created_at: string;
|
||||
last_accessed_at: string;
|
||||
embedded_wallet?: {
|
||||
id: string;
|
||||
address: string;
|
||||
chain: string;
|
||||
wallet_user_id: string;
|
||||
}[];
|
||||
ews_authed_user: {
|
||||
id: string;
|
||||
authed_user_id: string;
|
||||
email: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function useEmbeddedWallets(clientId: string) {
|
||||
export function useEmbeddedWallets(clientId: string, page: number) {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: embeddedWalletsKeys.embeddedWallets(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
clientId,
|
||||
page,
|
||||
),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user?.jwt}`,
|
||||
},
|
||||
},
|
||||
const url = new URL(
|
||||
`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`,
|
||||
);
|
||||
url.searchParams.append("clientId", clientId);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
const res = await fetch(url.href, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user?.jwt}`,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.walletUsers as EmbeddedWalletUser[];
|
||||
return json as {
|
||||
users: WalletUser[];
|
||||
totalPages: number;
|
||||
};
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: !!user?.address && isLoggedIn && !!clientId,
|
||||
});
|
||||
}
|
||||
|
||||
+1
@@ -208,6 +208,7 @@ export function FaucetButton({
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Turnstile
|
||||
className="mt-4"
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
onSuccess={(token) => field.onChange(token)}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export default async function Page(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(contract).then(() => undefined);
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
|
||||
@@ -23,7 +23,6 @@ import plumeCTA from "./temp-assets/plumeCTA.png";
|
||||
import rootStockBanner from "./temp-assets/rootstock-banner.png";
|
||||
import rootStockCTABG from "./temp-assets/rootstock-cta.png";
|
||||
import soneiumBanner from "./temp-assets/soneiumBanner.png";
|
||||
import soneiumCTA from "./temp-assets/soneiumCTA.png";
|
||||
import superpositionBanner from "./temp-assets/superpositionBanner.png";
|
||||
import superpositionCTA from "./temp-assets/superpositionCTA.png";
|
||||
import thirdwebBanner from "./temp-assets/thirdweb-banner.png";
|
||||
@@ -404,13 +403,7 @@ const chainMetaRecord = {
|
||||
headerImgUrl: soneiumBanner.src,
|
||||
about:
|
||||
"Soneium, an Ethereum layer-2 developed by Sony Block Solutions Labs. This versatile chain is a general-purpose blockchain platform that aims to evoke emotion, empower creativity, and meet diverse needs to go mainstream. Soneium will be simplifying blockchain experiences while empowering developers, creators, and communities. Built on accessibility, scalability, and efficiency, it aims to solve real-world problems across industries globally. Soneium will change the way we interact with the internet, opening up a world of innovative applications and unlimited potential for users worldwide.",
|
||||
cta: {
|
||||
backgroundImageUrl: soneiumCTA.src,
|
||||
title: "Begin your journey with Soneium Spark",
|
||||
buttonLink:
|
||||
"https://soneium.org/en/blog/soneium-minato-testnet-is-live-along-with-soneium-spark-incubation-program/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
cta: OP_CTA,
|
||||
},
|
||||
} satisfies Record<number, ExtraChainMetadata>;
|
||||
// END TEMPORARY
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { AccountStatus } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountStatus } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
|
||||
import { ConnectSDKCard } from "components/shared/ConnectSDKCard";
|
||||
import { SmartWallets } from "components/smart-wallets";
|
||||
@@ -38,7 +38,7 @@ export default async function Page(props: {
|
||||
const hasSmartWalletsWithoutBilling = apiKeys.find((k) =>
|
||||
k.services?.find(
|
||||
(s) =>
|
||||
dashboardAccount.status !== AccountStatus.ValidPayment &&
|
||||
dashboardAccount.status !== accountStatus.validPayment &&
|
||||
s.name === "bundler",
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+2
-2
@@ -16,7 +16,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -164,7 +164,7 @@ export function CreateEcosystemForm(props: {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{billingAccountInfo?.status !== AccountStatus.ValidPayment ? (
|
||||
{billingAccountInfo?.status !== accountStatus.validPayment ? (
|
||||
<ToolTipLabel label="Please update your payment method to create an ecosystem">
|
||||
{/* Allows the button to be disabled but the tooltip still works */}
|
||||
<div className="w-full">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function Tabs({ clientId }: { clientId: string }) {
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
href: `/dashboard/connect/in-app-wallets/${clientId}/analytics`,
|
||||
isActive: path?.endsWith("/analytics") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
href: `/dashboard/connect/in-app-wallets/${clientId}/users`,
|
||||
isActive: path?.endsWith("/users") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "Configuration",
|
||||
href: `/dashboard/connect/in-app-wallets/${clientId}/config`,
|
||||
isActive: path?.endsWith("/config") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const TRACKING_CATEGORY = "embedded-wallet";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: { clientId: string };
|
||||
searchParams: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
type?: string;
|
||||
interval?: string;
|
||||
};
|
||||
}) {
|
||||
const range =
|
||||
searchParams.from && searchParams.to
|
||||
? {
|
||||
type: searchParams.type ?? "last-120",
|
||||
from: new Date(searchParams.from),
|
||||
to: new Date(searchParams.to),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const interval: "day" | "week" = ["day", "week"].includes(
|
||||
searchParams.interval ?? "",
|
||||
)
|
||||
? (searchParams.interval as "day" | "week")
|
||||
: "week";
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
clientId={params.clientId}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { InAppWalletSettingsPage } from "components/embedded-wallets/Configure";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getInAppWalletSupportedAPIKeys } from "../../getInAppWalletSupportedAPIKeys";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export default async function Page({
|
||||
params: { clientId },
|
||||
}: { params: { clientId: string } }) {
|
||||
const apiKeys = await getInAppWalletSupportedAPIKeys();
|
||||
const apiKey = apiKeys.find((key) => key.key === clientId);
|
||||
|
||||
if (!apiKey) {
|
||||
redirect("/dashboard/connect/in-app-wallets");
|
||||
}
|
||||
return (
|
||||
<InAppWalletSettingsPage
|
||||
apiKey={apiKey}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
|
||||
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { InAppWaletFooterSection } from "../../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
|
||||
import { PageHeader } from "../PageHeader";
|
||||
import { getInAppWalletSupportedAPIKeys } from "../getInAppWalletSupportedAPIKeys";
|
||||
import { InAppWalletsAPIKeysMenu } from "../inAppWalletsAPIKeysMenu";
|
||||
import { Tabs } from "./_components/tabs";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
clientId: string;
|
||||
};
|
||||
searchParams: {
|
||||
tab?: string;
|
||||
};
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const authToken = getAuthToken();
|
||||
const { clientId } = props.params;
|
||||
|
||||
if (!authToken) {
|
||||
redirect(
|
||||
`/login?next=${encodeURIComponent(`/dashboard/connect/in-app-wallets/${clientId}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = await getInAppWalletSupportedAPIKeys();
|
||||
const apiKey = apiKeys.find((key) => key.key === clientId);
|
||||
|
||||
if (!apiKey) {
|
||||
redirect("/dashboard/connect/in-app-wallets");
|
||||
}
|
||||
|
||||
const allTimeStatsPromise = getInAppWalletUsage({
|
||||
clientId,
|
||||
from: new Date(2022, 0, 1),
|
||||
to: new Date(),
|
||||
period: "all",
|
||||
});
|
||||
|
||||
const monthlyStatsPromise = getInAppWalletUsage({
|
||||
clientId,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
});
|
||||
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
allTimeStatsPromise,
|
||||
monthlyStatsPromise,
|
||||
]).catch(() => [null, null]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* header */}
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:justify-between">
|
||||
<PageHeader />
|
||||
<div>
|
||||
<InAppWalletsAPIKeysMenu
|
||||
apiKeys={apiKeys.map((x) => ({
|
||||
name: x.name,
|
||||
key: x.key,
|
||||
}))}
|
||||
selectedAPIKey={apiKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<InAppWalletsSummary
|
||||
allTimeStats={allTimeStats || []}
|
||||
monthlyStats={monthlyStats || []}
|
||||
/>
|
||||
|
||||
<div className="h-8" />
|
||||
<Tabs clientId={clientId} />
|
||||
<div className="h-8" />
|
||||
{props.children}
|
||||
<div className="h-16" />
|
||||
<InAppWaletFooterSection trackingCategory="embedded-wallet" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+1
-43
@@ -1,9 +1,4 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { EmbeddedWallets } from "../../../../../../components/embedded-wallets";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { PageHeader } from "../PageHeader";
|
||||
import { getInAppWalletSupportedAPIKeys } from "../getInAppWalletSupportedAPIKeys";
|
||||
import { InAppWalletsAPIKeysMenu } from "../inAppWalletsAPIKeysMenu";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -13,44 +8,7 @@ export default async function Page(props: {
|
||||
tab?: string;
|
||||
};
|
||||
}) {
|
||||
const authToken = getAuthToken();
|
||||
const { clientId } = props.params;
|
||||
|
||||
if (!authToken) {
|
||||
redirect(
|
||||
`/login?next=${encodeURIComponent(`/dashboard/connect/in-app-wallets/${clientId}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = await getInAppWalletSupportedAPIKeys();
|
||||
const apiKey = apiKeys.find((key) => key.key === clientId);
|
||||
|
||||
if (!apiKey) {
|
||||
redirect("/dashboard/connect/in-app-wallets");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* header */}
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:justify-between">
|
||||
<PageHeader />
|
||||
<div>
|
||||
<InAppWalletsAPIKeysMenu
|
||||
apiKeys={apiKeys.map((x) => ({
|
||||
name: x.name,
|
||||
key: x.key,
|
||||
}))}
|
||||
selectedAPIKey={apiKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
<EmbeddedWallets
|
||||
apiKey={apiKey}
|
||||
trackingCategory="embedded-wallet"
|
||||
defaultTab={props.searchParams.tab === "1" ? 1 : 0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
redirect(`/dashboard/connect/in-app-wallets/${clientId}/analytics`);
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { InAppWalletUsersPageContent } from "components/embedded-wallets/Users";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getInAppWalletSupportedAPIKeys } from "../../getInAppWalletSupportedAPIKeys";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
clientId: string;
|
||||
};
|
||||
}) {
|
||||
const { clientId } = props.params;
|
||||
const apiKeys = await getInAppWalletSupportedAPIKeys();
|
||||
const apiKey = apiKeys.find((key) => key.key === clientId);
|
||||
|
||||
if (!apiKey) {
|
||||
redirect("/dashboard/connect/in-app-wallets");
|
||||
}
|
||||
return (
|
||||
<InAppWalletUsersPageContent
|
||||
clientId={apiKey.key}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { AnalyticsCallout } from "../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/AnalyticsCallout";
|
||||
import { InAppWaletFooterSection } from "../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
|
||||
|
||||
export default function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{props.children}
|
||||
<div className="h-16" />
|
||||
{/* Footer */}
|
||||
<AnalyticsCallout trackingCategory="embedded-wallet" />
|
||||
<div className="h-5" />
|
||||
<InAppWaletFooterSection trackingCategory="embedded-wallet" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { NoApiKeys } from "../../../../../components/settings/ApiKeys/NoApiKeys";
|
||||
import { getAuthToken } from "../../../../api/lib/getAuthToken";
|
||||
import { InAppWaletFooterSection } from "../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { getInAppWalletSupportedAPIKeys } from "./getInAppWalletSupportedAPIKeys";
|
||||
|
||||
@@ -25,6 +26,8 @@ export default async function Page() {
|
||||
<PageHeader />
|
||||
<div className="h-8" />
|
||||
<NoApiKeys service="in-app wallets" />
|
||||
<div className="h-16" />
|
||||
<InAppWaletFooterSection trackingCategory="embedded-wallet" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+2
-2
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -47,6 +46,7 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
import { z } from "zod";
|
||||
import GenericLoadingPage from "../loading";
|
||||
|
||||
type Webhook = {
|
||||
url: string;
|
||||
@@ -79,7 +79,7 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
|
||||
});
|
||||
|
||||
if (webhooksQuery.isPending) {
|
||||
return <Spinner className="mx-auto size-8" />;
|
||||
return <GenericLoadingPage />;
|
||||
}
|
||||
|
||||
if (!webhooksQuery.data?.length) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+5
-1
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { ImageResponse } from "next/og";
|
||||
import { isAddress } from "thirdweb";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
|
||||
const OgBrandIcon: React.FC = () => (
|
||||
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
|
||||
@@ -255,7 +257,9 @@ export async function publishedContractOGImageTemplate(params: {
|
||||
)}
|
||||
|
||||
<h2 tw="text-2xl text-white font-medium max-w-full">
|
||||
{params.publisher}
|
||||
{isAddress(params.publisher)
|
||||
? shortenAddress(params.publisher)
|
||||
: params.publisher}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function GlobalError({
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body>
|
||||
<h2>Something went wrong!</h2>
|
||||
<button type="button" onClick={() => reset()}>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<PlausibleProvider
|
||||
domain="thirdweb.com"
|
||||
|
||||
+9
-8
@@ -1,20 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Billing } from "components/settings/Account/Billing";
|
||||
|
||||
export const SettingsBillingPage = (props: {
|
||||
teamId: string | undefined;
|
||||
}) => {
|
||||
const meQuery = useAccount({
|
||||
refetchInterval: (query) =>
|
||||
[
|
||||
AccountStatus.InvalidPayment,
|
||||
AccountStatus.InvalidPaymentMethod,
|
||||
].includes(query.state?.status as AccountStatus)
|
||||
? 1000
|
||||
: false,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state?.status as string;
|
||||
const isInvalidPayment =
|
||||
status === accountStatus.invalidPayment ||
|
||||
status === accountStatus.invalidPaymentMethod;
|
||||
|
||||
return isInvalidPayment ? 1000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: account } = meQuery;
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import {
|
||||
AccountStatus,
|
||||
type ApiKeyService,
|
||||
accountStatus,
|
||||
useAccount,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
@@ -40,7 +40,7 @@ export function AccountAbstractionPage(props: {
|
||||
|
||||
return apiKeyServices.find(
|
||||
(s) =>
|
||||
accountQuery.data.status !== AccountStatus.ValidPayment &&
|
||||
accountQuery.data.status !== accountStatus.validPayment &&
|
||||
s.name === "bundler",
|
||||
);
|
||||
}, [apiKeyServices, accountQuery.data]);
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+28
-17
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
import {
|
||||
getInAppWalletUsage,
|
||||
useUserOpUsageAggregate,
|
||||
useWalletUsageAggregate,
|
||||
useWalletUsagePeriod,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
DateRangeSelector,
|
||||
type Range,
|
||||
getLastNDaysRange,
|
||||
} from "components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "components/analytics/interval-selector";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { ConnectAnalyticsDashboardUI } from "./ConnectAnalyticsDashboardUI";
|
||||
@@ -45,31 +45,42 @@ export function ConnectAnalyticsDashboard(props: {
|
||||
clientId: props.clientId,
|
||||
});
|
||||
|
||||
const inAppAggregateQuery = useQuery({
|
||||
queryKey: ["in-app-usage-aggregate", props.clientId],
|
||||
queryFn: async () => {
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
getInAppWalletUsage({
|
||||
clientId: props.clientId,
|
||||
from: new Date(2022, 0, 1),
|
||||
to: new Date(),
|
||||
period: "all",
|
||||
}),
|
||||
getInAppWalletUsage({
|
||||
clientId: props.clientId,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
}),
|
||||
]);
|
||||
return { allTimeStats, monthlyStats };
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-3">
|
||||
<DateRangeSelector
|
||||
range={range}
|
||||
setRange={(newRange) => {
|
||||
setRange(newRange);
|
||||
const days = differenceInDays(newRange.to, newRange.from);
|
||||
setIntervalType(days > 30 ? "week" : "day");
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
intervalType={intervalType}
|
||||
setIntervalType={setIntervalType}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-4" />
|
||||
<ConnectAnalyticsDashboardUI
|
||||
walletUsage={walletUsageQuery.data || []}
|
||||
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
|
||||
aggregateUserOpUsageQuery={userOpAggregateQuery.data}
|
||||
connectLayoutSlug={props.connectLayoutSlug}
|
||||
inAppAggregateQuery={inAppAggregateQuery.data}
|
||||
isPending={
|
||||
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
|
||||
}
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
intervalType={intervalType}
|
||||
setIntervalType={setIntervalType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+91
-17
@@ -1,10 +1,19 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { UserOpStats, WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type {
|
||||
InAppWalletStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { Stat } from "components/analytics/stat";
|
||||
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
|
||||
import { AccountAbstractionSummary } from "components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
|
||||
import { CableIcon, WalletCardsIcon } from "lucide-react";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { ArrowRightIcon, CableIcon, WalletCardsIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { DateRangeSelector } from "../../../../../../components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "../../../../../../components/analytics/interval-selector";
|
||||
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
|
||||
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
|
||||
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";
|
||||
@@ -15,6 +24,14 @@ export function ConnectAnalyticsDashboardUI(props: {
|
||||
aggregateUserOpUsageQuery?: UserOpStats;
|
||||
connectLayoutSlug: string;
|
||||
isPending: boolean;
|
||||
inAppAggregateQuery?: {
|
||||
allTimeStats: InAppWalletStats[];
|
||||
monthlyStats: InAppWalletStats[];
|
||||
};
|
||||
setRange: (range: Range) => void;
|
||||
range: Range;
|
||||
intervalType: "day" | "week";
|
||||
setIntervalType: (intervalType: "day" | "week") => void;
|
||||
}) {
|
||||
const { totalWallets, uniqueWallets } = useMemo(() => {
|
||||
return props.aggregateWalletUsage.reduce(
|
||||
@@ -29,35 +46,77 @@ export function ConnectAnalyticsDashboardUI(props: {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 lg:gap-6">
|
||||
{/* Connections */}
|
||||
<div className="grid grid-cols-2 gap-4 lg:gap-6">
|
||||
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
|
||||
<Stat
|
||||
label="Unique Wallets"
|
||||
value={uniqueWallets}
|
||||
icon={WalletCardsIcon}
|
||||
/>
|
||||
<div>
|
||||
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Connections
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
View how many users are connecting to your app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 lg:gap-6">
|
||||
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
|
||||
<Stat
|
||||
label="Unique Wallets"
|
||||
value={uniqueWallets}
|
||||
icon={WalletCardsIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-4">
|
||||
<div className="w-full border-muted border-b" />
|
||||
<div className="w-full border-separate border-b" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="font-medium text-xl tracking-tight">
|
||||
Account Abstraction
|
||||
</h1>
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
In-App Wallets
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Sponsor and batch transactions for your users.
|
||||
View metrics for your in-app wallets.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button asChild variant="primary" size="sm" className="w-full">
|
||||
<Link href={`${props.connectLayoutSlug}/in-app-wallets`}>
|
||||
In-App Wallet Metrics
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<InAppWalletsSummary
|
||||
allTimeStats={props.inAppAggregateQuery?.allTimeStats}
|
||||
monthlyStats={props.inAppAggregateQuery?.monthlyStats}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4">
|
||||
<div className="w-full border-separate border-b" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Account Abstraction
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
View how smart wallets are used in your app.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button asChild variant="primary" size="sm" className="w-full">
|
||||
<Link href={`${props.connectLayoutSlug}/account-abstraction`}>
|
||||
View More Stats
|
||||
Account Abstraction Metrics
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -68,7 +127,22 @@ export function ConnectAnalyticsDashboardUI(props: {
|
||||
</div>
|
||||
|
||||
<div className="h-4">
|
||||
<div className="w-full border-muted border-b" />
|
||||
<div className="w-full border-separate border-b" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<DateRangeSelector
|
||||
range={props.range}
|
||||
setRange={(newRange) => {
|
||||
props.setRange(newRange);
|
||||
const days = differenceInDays(newRange.to, newRange.from);
|
||||
props.setIntervalType(days > 30 ? "week" : "day");
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
intervalType={props.intervalType}
|
||||
setIntervalType={props.setIntervalType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DailyConnectionsChartCard
|
||||
|
||||
+5
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createUserOpStatsStub } from "components/smart-wallets/AccountAbstractionAnalytics/storyUtils";
|
||||
import { getLastNDaysRange } from "../../../../../../../components/analytics/date-range-selector";
|
||||
import { mobileViewport } from "../../../../../../../stories/utils";
|
||||
import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI";
|
||||
import { createWalletStatsStub } from "./storyUtils";
|
||||
@@ -30,6 +31,10 @@ function Component() {
|
||||
return (
|
||||
<div className="container max-w-[1150px] py-8">
|
||||
<ConnectAnalyticsDashboardUI
|
||||
setRange={() => {}}
|
||||
range={getLastNDaysRange("last-120")}
|
||||
intervalType="day"
|
||||
setIntervalType={() => {}}
|
||||
walletUsage={createWalletStatsStub(30)}
|
||||
aggregateWalletUsage={createWalletStatsStub(30)}
|
||||
aggregateUserOpUsageQuery={createUserOpStatsStub(1)?.[0]}
|
||||
|
||||
+25
-2
@@ -27,7 +27,15 @@ import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
LabelList,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { formatTickerNumber } from "../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartToShow = "uniqueWallets" | "totalWallets";
|
||||
|
||||
@@ -194,7 +202,21 @@ export function DailyConnectionsChartCard(props: {
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<ChartTooltip cursor={true} content={<ChartTooltipContent />} />
|
||||
<YAxis
|
||||
dataKey={(data) => data[chartToShow]}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Bar
|
||||
dataKey={chartToShow}
|
||||
@@ -207,6 +229,7 @@ export function DailyConnectionsChartCard(props: {
|
||||
offset={12}
|
||||
className="invisible fill-foreground sm:visible"
|
||||
fontSize={12}
|
||||
formatter={(value: number) => formatTickerNumber(value)}
|
||||
/>
|
||||
)}
|
||||
</Bar>
|
||||
|
||||
+27
-3
@@ -26,7 +26,11 @@ import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import {
|
||||
formatTickerNumber,
|
||||
formatWalletType,
|
||||
} from "../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartToShow = "uniqueWalletsConnected" | "totalConnections";
|
||||
|
||||
@@ -60,7 +64,8 @@ export function WalletConnectorsChartCard(props: {
|
||||
// for each stat, add it in _chartDataMap
|
||||
for (const stat of walletStats) {
|
||||
const chartData = _chartDataMap.get(stat.date);
|
||||
const { walletType } = stat;
|
||||
const { walletType: rawWalletType } = stat;
|
||||
const walletType = formatWalletType(rawWalletType);
|
||||
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
@@ -213,7 +218,26 @@ export function WalletConnectorsChartCard(props: {
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<ChartTooltip cursor={true} content={<ChartTooltipContent />} />
|
||||
<YAxis
|
||||
dataKey={(data) =>
|
||||
Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
.reduce((acc, current) => Number(acc) + Number(current), 0)
|
||||
}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{uniqueWalletTypes.map((walletType) => {
|
||||
return (
|
||||
|
||||
+7
-2
@@ -26,6 +26,10 @@ import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Pie, PieChart } from "recharts";
|
||||
import {
|
||||
formatTickerNumber,
|
||||
formatWalletType,
|
||||
} from "../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartToShow = "totalConnections" | "uniqueWalletsConnected";
|
||||
|
||||
@@ -63,7 +67,8 @@ export function WalletDistributionChartCard(props: {
|
||||
let _uniqueConnections = 0;
|
||||
|
||||
for (const stat of walletStats) {
|
||||
const { walletType } = stat;
|
||||
const { walletType: rawWalletType } = stat;
|
||||
const walletType = formatWalletType(rawWalletType);
|
||||
const chartData = _chartDataMap.get(walletType);
|
||||
|
||||
_totalConnections += stat.totalConnections;
|
||||
@@ -235,7 +240,7 @@ export function WalletDistributionChartCard(props: {
|
||||
? uniqueConnections
|
||||
: totalConnections;
|
||||
const percentageValue = ((v / sumValue) * 100).toFixed(2);
|
||||
return `${percentageValue}% - ${v}`;
|
||||
return `${percentageValue}% - ${formatTickerNumber(v)}`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { UsageCard } from "components/settings/Account/UsageCard";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { toNumber, toPercent } from "utils/number";
|
||||
|
||||
type AnalyticsCalloutProps = {
|
||||
trackingCategory: string;
|
||||
};
|
||||
|
||||
export const AnalyticsCallout: React.FC<AnalyticsCalloutProps> = ({
|
||||
trackingCategory,
|
||||
}) => {
|
||||
const usageQuery = useAccountUsage();
|
||||
|
||||
const walletsMetrics = useMemo(() => {
|
||||
if (!usageQuery?.data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const usageData = usageQuery.data;
|
||||
|
||||
const numOfWallets = usageData.usage.embeddedWallets.countWalletAddresses;
|
||||
const limitWallets = usageData.limits.embeddedWallets;
|
||||
const percent = toPercent(numOfWallets, limitWallets);
|
||||
|
||||
return {
|
||||
total: `${toNumber(numOfWallets)} / ${toNumber(
|
||||
limitWallets,
|
||||
)} (${percent}%)`,
|
||||
progress: percent,
|
||||
...(usageData.billableUsd.embeddedWallets > 0
|
||||
? {
|
||||
overage: usageData.billableUsd.embeddedWallets,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}, [usageQuery]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 rounded-lg border border-border bg-muted/50 p-4 md:px-10 md:py-12 lg:flex-row lg:justify-between">
|
||||
{/* Left */}
|
||||
<div>
|
||||
<p className="mb-3 text-muted-foreground text-xs">Analytics</p>
|
||||
<h2 className="mb-5 max-w-[500px] font-semibold text-2xl tracking-tight">
|
||||
View more insights about how users are interacting with your
|
||||
application
|
||||
</h2>
|
||||
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<TrackedLinkTW
|
||||
href="/dashboard/connect/analytics"
|
||||
category={trackingCategory}
|
||||
label="view-analytics"
|
||||
className="mt-auto min-w-[150px] gap-2"
|
||||
>
|
||||
View Analytics
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Right */}
|
||||
<div className="min-w-[280px]">
|
||||
{walletsMetrics && (
|
||||
<UsageCard
|
||||
{...walletsMetrics}
|
||||
name="Monthly Active Users"
|
||||
tooltip="Email wallet (with managed recovery code) usage is calculated by monthly active wallets (i.e. active as defined by at least 1 user log-in via email or social within the billing period month)."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
|
||||
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export async function InAppWalletsHeader({ clientId }: { clientId: string }) {
|
||||
const allTimeStatsPromise = getInAppWalletUsage({
|
||||
clientId,
|
||||
from: new Date(2022, 0, 1),
|
||||
to: new Date(),
|
||||
period: "all",
|
||||
});
|
||||
|
||||
const monthlyStatsPromise = getInAppWalletUsage({
|
||||
clientId,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
});
|
||||
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
allTimeStatsPromise,
|
||||
monthlyStatsPromise,
|
||||
]).catch(() => [null, null]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-3 font-semibold text-2xl tracking-tight md:text-3xl">
|
||||
In-App Wallets
|
||||
</h1>
|
||||
<p className="mt-3 mb-7 max-w-[700px] text-muted-foreground">
|
||||
A wallet infrastructure that enables apps to create, manage, and control
|
||||
their users wallets. Email login, social login, and bring-your-own auth
|
||||
supported.{" "}
|
||||
<TrackedLinkTW
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
|
||||
label="learn-more"
|
||||
category={TRACKING_CATEGORY}
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more
|
||||
</TrackedLinkTW>
|
||||
</p>
|
||||
<InAppWalletsSummary
|
||||
allTimeStats={allTimeStats || []}
|
||||
monthlyStats={monthlyStats || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function Tabs({
|
||||
team_slug,
|
||||
project_slug,
|
||||
}: { team_slug: string; project_slug: string }) {
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/analytics`,
|
||||
isActive: path?.endsWith("/analytics") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/users`,
|
||||
isActive: path?.endsWith("/users") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "Configuration",
|
||||
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/config`,
|
||||
isActive: path?.endsWith("/config") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const TRACKING_CATEGORY = "team/in-app-wallets";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: { team_slug: string; project_slug: string };
|
||||
searchParams: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
type?: string;
|
||||
interval?: string;
|
||||
};
|
||||
}) {
|
||||
const range =
|
||||
searchParams.from && searchParams.to
|
||||
? {
|
||||
type: searchParams.type ?? "last-120",
|
||||
from: new Date(searchParams.from),
|
||||
to: new Date(searchParams.to),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const interval: "day" | "week" = ["day", "week"].includes(
|
||||
searchParams.interval ?? "",
|
||||
)
|
||||
? (searchParams.interval as "day" | "week")
|
||||
: "week";
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
clientId={params.project_slug}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { getProject } from "@/api/projects";
|
||||
import { getAPIKeyForProjectId } from "app/api/lib/getAPIKeys";
|
||||
import { notFound } from "next/navigation";
|
||||
import { InAppWalletSettingsPage } from "../../../../../../../components/embedded-wallets/Configure";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: { params: { team_slug: string; project_slug: string } }) {
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const apiKey = await getAPIKeyForProjectId(project.id);
|
||||
|
||||
if (!apiKey) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InAppWalletSettingsPage
|
||||
apiKey={apiKey}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { getProject } from "@/api/projects";
|
||||
import { getAPIKeyForProjectId } from "app/api/lib/getAPIKeys";
|
||||
import { notFound } from "next/navigation";
|
||||
import { InAppWaletFooterSection } from "./_components/footer";
|
||||
import { InAppWalletsHeader } from "./_components/header";
|
||||
import { Tabs } from "./_components/tabs";
|
||||
import { TRACKING_CATEGORY } from "./_constants";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
};
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const project = await getProject(
|
||||
props.params.team_slug,
|
||||
props.params.project_slug,
|
||||
);
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const apiKey = await getAPIKeyForProjectId(project.id);
|
||||
if (!apiKey) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InAppWalletsHeader clientId={apiKey.key} />
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<Tabs
|
||||
team_slug={props.params.team_slug}
|
||||
project_slug={props.params.project_slug}
|
||||
/>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
{props.children}
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<InAppWaletFooterSection trackingCategory={TRACKING_CATEGORY} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+4
-49
@@ -1,9 +1,4 @@
|
||||
import { getProject } from "@/api/projects";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { InAppWalletUsersPageContent } from "../../../../../../components/embedded-wallets/Users";
|
||||
import { AnalyticsCallout } from "./_components/AnalyticsCallout";
|
||||
import { InAppWaletFooterSection } from "./_components/footer";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -11,48 +6,8 @@ export default async function Page(props: {
|
||||
project_slug: string;
|
||||
};
|
||||
}) {
|
||||
const project = await getProject(
|
||||
props.params.team_slug,
|
||||
props.params.project_slug,
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const TRACKING_CATEGORY = "team/in-app-wallets";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-3 font-semibold text-2xl tracking-tight md:text-3xl">
|
||||
In-App Wallets
|
||||
</h1>
|
||||
|
||||
<p className="mt-3 mb-7 max-w-[700px] text-muted-foreground">
|
||||
A wallet infrastructure that enables apps to create, manage, and control
|
||||
their users wallets. Email login, social login, and bring-your-own auth
|
||||
supported.{" "}
|
||||
<TrackedLinkTW
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
|
||||
label="learn-more"
|
||||
category={TRACKING_CATEGORY}
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more
|
||||
</TrackedLinkTW>
|
||||
</p>
|
||||
|
||||
<InAppWalletUsersPageContent
|
||||
clientId={project.publishableKey}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
/>
|
||||
|
||||
<div className="h-16" />
|
||||
<AnalyticsCallout trackingCategory={TRACKING_CATEGORY} />
|
||||
<div className="h-5" />
|
||||
|
||||
<InAppWaletFooterSection trackingCategory={TRACKING_CATEGORY} />
|
||||
</div>
|
||||
// Default to the users tab
|
||||
redirect(
|
||||
`/team/${props.params.team_slug}/${props.params.project_slug}/connect/in-app-wallets/analytics`,
|
||||
);
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { InAppWalletUsersPageContent } from "components/embedded-wallets/Users";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
}: { params: { team_slug: string; project_slug: string } }) {
|
||||
return (
|
||||
<>
|
||||
<InAppWalletUsersPageContent
|
||||
clientId={params.project_slug}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -54,7 +54,7 @@ export const PublisherHeader: React.FC<PublisherHeaderProps> = ({
|
||||
>
|
||||
<PublisherAvatar
|
||||
alt="Publisher avatar"
|
||||
boxSize={14}
|
||||
className="size-14"
|
||||
address={ensQuery.data?.ensName || wallet}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
"use client";
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { InAppWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
|
||||
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
|
||||
import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
|
||||
import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import { formatTickerNumber } from "../../../lib/format-utils";
|
||||
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
};
|
||||
|
||||
export function InAppWalletUsersChartCard(props: {
|
||||
inAppWalletStats: InAppWalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
const { inAppWalletStats } = props;
|
||||
const topChainsToShow = 10;
|
||||
|
||||
const { chartConfig, chartData } = useMemo(() => {
|
||||
const _chartConfig: ChartConfig = {};
|
||||
const _chartDataMap: Map<string, ChartData> = new Map();
|
||||
const authMethodToVolumeMap: Map<string, number> = new Map();
|
||||
// for each stat, add it in _chartDataMap
|
||||
for (const stat of inAppWalletStats) {
|
||||
const chartData = _chartDataMap.get(stat.date);
|
||||
const { authenticationMethod } = stat;
|
||||
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData && stat.uniqueWalletsConnected > 0) {
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[authenticationMethod || "Unknown"]: stat.uniqueWalletsConnected,
|
||||
} as ChartData);
|
||||
} else if (chartData) {
|
||||
chartData[authenticationMethod || "Unknown"] =
|
||||
(chartData[authenticationMethod || "Unknown"] || 0) +
|
||||
stat.uniqueWalletsConnected;
|
||||
}
|
||||
|
||||
authMethodToVolumeMap.set(
|
||||
authenticationMethod || "Unknown",
|
||||
stat.uniqueWalletsConnected +
|
||||
(authMethodToVolumeMap.get(authenticationMethod || "Unknown") || 0),
|
||||
);
|
||||
}
|
||||
|
||||
const authMethodsSorted = Array.from(authMethodToVolumeMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const authMethodsToShow = authMethodsSorted.slice(0, topChainsToShow);
|
||||
const authMethodsAsOther = authMethodsSorted.slice(topChainsToShow);
|
||||
|
||||
// replace chainIdsToTagAsOther chainId with "other"
|
||||
for (const data of _chartDataMap.values()) {
|
||||
for (const authMethod in data) {
|
||||
if (authMethodsAsOther.includes(authMethod)) {
|
||||
data.others = (data.others || 0) + (data[authMethod] || 0);
|
||||
delete data[authMethod];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authMethodsToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: authMethodsToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
});
|
||||
|
||||
// Add Other
|
||||
authMethodsToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
|
||||
return {
|
||||
chartData: Array.from(_chartDataMap.values()).sort(
|
||||
(a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(),
|
||||
),
|
||||
chartConfig: _chartConfig,
|
||||
};
|
||||
}, [inAppWalletStats]);
|
||||
|
||||
const uniqueAuthMethods = Object.keys(chartConfig);
|
||||
const disableActions =
|
||||
props.isPending ||
|
||||
chartData.length === 0 ||
|
||||
chartData.every((data) => data.sponsoredUsd === 0);
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-muted/50 p-4 md:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Unique Users
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
The total number of active in-app wallet users on your project.
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<ExportToCSVButton
|
||||
className="bg-background"
|
||||
fileName="Connect Wallets"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
// Shows the number of each type of wallet connected on all dates
|
||||
const header = ["Date", ...uniqueAuthMethods];
|
||||
const rows = chartData.map((data) => {
|
||||
const { time, ...rest } = data;
|
||||
return [
|
||||
time,
|
||||
...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()),
|
||||
];
|
||||
});
|
||||
return { header, rows };
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[250px] w-full md:h-[350px]"
|
||||
>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : chartData.length === 0 ||
|
||||
chartData.every((data) => data.sponsoredUsd === 0) ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="mb-6 text-lg">
|
||||
Connect users to your app with social logins
|
||||
</span>
|
||||
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/inAppWallet"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/in-app-wallet/get-started"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/in-app-wallet/get-started"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/unity/v5/wallets/in-app-wallet"
|
||||
label="Unity"
|
||||
icon={UnityIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/unreal-engine/getting-started"
|
||||
label="Unreal Engine"
|
||||
icon={UnrealIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EmptyChartState>
|
||||
) : (
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
dataKey={(data) =>
|
||||
Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
.reduce((acc, current) => Number(acc) + Number(current), 0)
|
||||
}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{uniqueAuthMethods.map((authMethod) => {
|
||||
return (
|
||||
<Bar
|
||||
key={authMethod}
|
||||
dataKey={authMethod}
|
||||
fill={chartConfig[authMethod]?.color}
|
||||
radius={4}
|
||||
stackId="a"
|
||||
strokeWidth={1.5}
|
||||
className="stroke-muted"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { DateRangeSelector } from "components/analytics/date-range-selector";
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "components/analytics/interval-selector";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export function RangeSelector({
|
||||
range,
|
||||
interval,
|
||||
}: { range: Range; interval: "day" | "week" }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-3">
|
||||
<DateRangeSelector
|
||||
range={range}
|
||||
setRange={(newRange) => {
|
||||
const days = differenceInDays(newRange.to, newRange.from);
|
||||
const interval = days > 30 ? "week" : "day";
|
||||
const newSearchParams = new URLSearchParams(searchParams || {});
|
||||
newSearchParams.set("from", newRange.from.toISOString());
|
||||
newSearchParams.set("to", newRange.to.toISOString());
|
||||
newSearchParams.set("type", newRange.type);
|
||||
newSearchParams.set("interval", interval);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
intervalType={interval}
|
||||
setIntervalType={(newInterval) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams || {});
|
||||
newSearchParams.set("interval", newInterval);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { InAppWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Stat } from "components/analytics/stat";
|
||||
import { ActivityIcon, UserIcon } from "lucide-react";
|
||||
|
||||
export function InAppWalletsSummary(props: {
|
||||
allTimeStats: InAppWalletStats[] | undefined;
|
||||
monthlyStats: InAppWalletStats[] | undefined;
|
||||
}) {
|
||||
const allTimeStats = props.allTimeStats?.reduce(
|
||||
(acc, curr) => {
|
||||
acc.uniqueWalletsConnected += curr.uniqueWalletsConnected;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
uniqueWalletsConnected: 0,
|
||||
},
|
||||
);
|
||||
|
||||
const monthlyStats = props.monthlyStats?.reduce(
|
||||
(acc, curr) => {
|
||||
acc.uniqueWalletsConnected += curr.uniqueWalletsConnected;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
uniqueWalletsConnected: 0,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 lg:gap-6">
|
||||
<Stat
|
||||
label="Total Users"
|
||||
value={allTimeStats?.uniqueWalletsConnected || 0}
|
||||
icon={ActivityIcon}
|
||||
/>
|
||||
<Stat
|
||||
label="Monthly Active Users"
|
||||
value={monthlyStats?.uniqueWalletsConnected || 0}
|
||||
icon={UserIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
type Range,
|
||||
getLastNDaysRange,
|
||||
} from "components/analytics/date-range-selector";
|
||||
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
|
||||
import { InAppWalletUsersChartCard } from "./InAppWalletUsersChartCard";
|
||||
import { RangeSelector } from "./RangeSelector";
|
||||
|
||||
export async function InAppWalletAnalytics({
|
||||
clientId,
|
||||
interval,
|
||||
range,
|
||||
}: { clientId: string; interval: "day" | "week"; range?: Range }) {
|
||||
if (!range) {
|
||||
range = getLastNDaysRange("last-120");
|
||||
}
|
||||
|
||||
const stats = await getInAppWalletUsage({
|
||||
clientId,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}).catch(() => null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RangeSelector range={range} interval={interval} />
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="flex flex-col gap-4 lg:gap-6">
|
||||
<InAppWalletUsersChartCard
|
||||
inAppWalletStats={stats || []}
|
||||
isPending={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -230,7 +229,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
canEditAdvancedFeatures={canEditAdvancedFeatures}
|
||||
/>
|
||||
|
||||
<Separator className="my-5" />
|
||||
<div className="h-5" />
|
||||
|
||||
<AuthEndpointFields
|
||||
form={form}
|
||||
|
||||
@@ -3,41 +3,54 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
type EmbeddedWalletUser,
|
||||
useEmbeddedWallets,
|
||||
} from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useEmbeddedWallets } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
|
||||
import { Spinner } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { format } from "date-fns/format";
|
||||
import Papa from "papaparse";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { withinDays } from "utils/date-utils";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { WalletUser } from "thirdweb/wallets";
|
||||
|
||||
const ACTIVE_THRESHOLD_DAYS = 30;
|
||||
|
||||
const columnHelper = createColumnHelper<EmbeddedWalletUser>();
|
||||
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const mainDetail = accounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id
|
||||
);
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<WalletUser>();
|
||||
const columns = [
|
||||
columnHelper.accessor("ews_authed_user", {
|
||||
header: "Email",
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
header: "User Identifier",
|
||||
enableColumnFilter: true,
|
||||
cell: (cell) => (
|
||||
<span className="text-sm">{cell.getValue()?.[0]?.email}</span>
|
||||
),
|
||||
cell: (cell) => {
|
||||
const identifier = getUserIdentifier(cell.getValue());
|
||||
return <span className="text-sm">{identifier}</span>;
|
||||
},
|
||||
id: "user_identifier",
|
||||
}),
|
||||
columnHelper.accessor("embedded_wallet", {
|
||||
columnHelper.accessor("wallets", {
|
||||
header: "Address",
|
||||
cell: (cell) => {
|
||||
const address = cell.getValue()?.[0]?.address;
|
||||
const address = cell.getValue()[0]?.address;
|
||||
return address ? <WalletAddress address={address} /> : null;
|
||||
},
|
||||
id: "address",
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
columnHelper.accessor("wallets", {
|
||||
header: "Created",
|
||||
cell: (cell) => {
|
||||
const value = cell.getValue();
|
||||
const value = cell.getValue()[0]?.createdAt;
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
@@ -48,21 +61,36 @@ const columns = [
|
||||
</span>
|
||||
);
|
||||
},
|
||||
id: "created_at",
|
||||
}),
|
||||
columnHelper.accessor("last_accessed_at", {
|
||||
header: "Last login",
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
header: "Login Methods",
|
||||
cell: (cell) => {
|
||||
const value = cell.getValue();
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const loginMethodsDisplay = value.reduce((acc, curr) => {
|
||||
if (acc.length === 2) {
|
||||
acc.push("...");
|
||||
}
|
||||
if (acc.length < 2) {
|
||||
acc.push(curr.type);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
const loginMethods = value.map((v) => v.type).join(", ");
|
||||
return (
|
||||
<span className="text-sm">
|
||||
{format(new Date(value), "MMM dd, yyyy")}
|
||||
</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="text-sm">
|
||||
{loginMethodsDisplay.join(", ")}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span className="text-sm">{loginMethods}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
id: "login_methods",
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -70,37 +98,27 @@ export const InAppWalletUsersPageContent = (props: {
|
||||
clientId: string;
|
||||
trackingCategory: string;
|
||||
}) => {
|
||||
const [onlyActive, setOnlyActive] = useState(true);
|
||||
const walletsQuery = useEmbeddedWallets(props.clientId);
|
||||
const wallets = walletsQuery?.data || [];
|
||||
|
||||
const activeWallets = useMemo(() => {
|
||||
if (!wallets) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return wallets.filter((w) => {
|
||||
const lastAccessedAt = w.last_accessed_at;
|
||||
return (
|
||||
lastAccessedAt && withinDays(lastAccessedAt, ACTIVE_THRESHOLD_DAYS)
|
||||
);
|
||||
});
|
||||
}, [wallets]);
|
||||
|
||||
const theWalletsWeWant = useMemo(() => {
|
||||
return (onlyActive ? activeWallets : wallets) ?? [];
|
||||
}, [activeWallets, onlyActive, wallets]);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const walletsQuery = useEmbeddedWallets(props.clientId, activePage);
|
||||
const { users: wallets, totalPages } = walletsQuery?.data || {
|
||||
users: [],
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
// TODO: Make the download CSV grab all data instead of merely what's on the current page
|
||||
const downloadCSV = useCallback(() => {
|
||||
if (theWalletsWeWant.length === 0) {
|
||||
if (wallets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const csv = Papa.unparse(
|
||||
theWalletsWeWant.map((row) => ({
|
||||
email: row.ews_authed_user[0]?.email,
|
||||
address: row.embedded_wallet?.[0]?.address || "",
|
||||
created: format(new Date(row.created_at), "MMM dd, yyyy"),
|
||||
last_login: format(new Date(row.last_accessed_at), "MMM dd, yyyy"),
|
||||
wallets.map((row) => ({
|
||||
user_identifier: getUserIdentifier(row.linkedAccounts),
|
||||
address: row.wallets[0]?.address || "",
|
||||
created: format(
|
||||
new Date(row.wallets[0]?.createdAt ?? ""),
|
||||
"MMM dd, yyyy",
|
||||
),
|
||||
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
|
||||
})),
|
||||
);
|
||||
const csvUrl = URL.createObjectURL(
|
||||
@@ -110,20 +128,7 @@ export const InAppWalletUsersPageContent = (props: {
|
||||
tempLink.href = csvUrl;
|
||||
tempLink.setAttribute("download", "download.csv");
|
||||
tempLink.click();
|
||||
}, [theWalletsWeWant]);
|
||||
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const itemsPerPage = 20;
|
||||
const totalPages =
|
||||
theWalletsWeWant.length <= itemsPerPage
|
||||
? 1
|
||||
: Math.ceil(theWalletsWeWant.length / itemsPerPage);
|
||||
|
||||
const itemsToShow = useMemo(() => {
|
||||
const startIndex = (activePage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return theWalletsWeWant.slice(startIndex, endIndex);
|
||||
}, [activePage, theWalletsWeWant]);
|
||||
}, [wallets]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -131,7 +136,7 @@ export const InAppWalletUsersPageContent = (props: {
|
||||
{/* Top section */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
disabled={theWalletsWeWant.length === 0}
|
||||
disabled={wallets.length === 0}
|
||||
variant="outline"
|
||||
onClick={downloadCSV}
|
||||
size="sm"
|
||||
@@ -140,20 +145,20 @@ export const InAppWalletUsersPageContent = (props: {
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Active last {ACTIVE_THRESHOLD_DAYS} days
|
||||
</p>
|
||||
<Switch
|
||||
checked={onlyActive}
|
||||
onCheckedChange={(v) => setOnlyActive(v)}
|
||||
disabled={wallets.length === 0}
|
||||
/>
|
||||
{walletsQuery.isPlaceholderData && (
|
||||
<>
|
||||
<Spinner className="size-4" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Loading page {activePage} of {totalPages}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TWTable
|
||||
title="active in-app wallets"
|
||||
data={itemsToShow}
|
||||
title="in-app wallets"
|
||||
data={wallets}
|
||||
columns={columns}
|
||||
isPending={walletsQuery.isPending}
|
||||
isFetched={walletsQuery.isFetched}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useState } from "react";
|
||||
import { InAppWalletSettingsPage } from "./Configure";
|
||||
import { InAppWalletUsersPageContent } from "./Users";
|
||||
|
||||
interface EmbeddedWalletsProps {
|
||||
apiKey: Pick<
|
||||
ApiKey,
|
||||
| "id"
|
||||
| "name"
|
||||
| "domains"
|
||||
| "bundleIds"
|
||||
| "services"
|
||||
| "redirectUrls"
|
||||
| "key"
|
||||
>;
|
||||
trackingCategory: string;
|
||||
defaultTab: 0 | 1;
|
||||
}
|
||||
|
||||
export const EmbeddedWallets: React.FC<EmbeddedWalletsProps> = ({
|
||||
apiKey,
|
||||
trackingCategory,
|
||||
defaultTab,
|
||||
}) => {
|
||||
const [selectedTab, setSelectedTab] = useState<"users" | "config">(
|
||||
defaultTab === 0 ? "users" : "config",
|
||||
);
|
||||
|
||||
function updateSearchParams(value: string) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("tab", value);
|
||||
window.history.pushState(null, "", url.toString());
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
name: "Users",
|
||||
onClick: () => {
|
||||
setSelectedTab("users");
|
||||
updateSearchParams("0");
|
||||
},
|
||||
isActive: selectedTab === "users",
|
||||
isEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "Configuration",
|
||||
onClick: () => {
|
||||
setSelectedTab("config");
|
||||
updateSearchParams("1");
|
||||
},
|
||||
isActive: selectedTab === "config",
|
||||
isEnabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
{selectedTab === "users" && (
|
||||
<InAppWalletUsersPageContent
|
||||
clientId={apiKey.key}
|
||||
trackingCategory={trackingCategory}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedTab === "config" && (
|
||||
<InAppWalletSettingsPage
|
||||
apiKey={apiKey}
|
||||
trackingCategory={trackingCategory}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ConfirmEngineTierDialog } from "../../../components/engine/create/ConfirmEngineTierDialog";
|
||||
@@ -79,7 +79,7 @@ export const CreateEnginePage = () => {
|
||||
setIsConfirmationDialogOpen(false);
|
||||
|
||||
// If Payment is already setup, deploy the Engine
|
||||
if (accountQuery.data?.status === AccountStatus.ValidPayment) {
|
||||
if (accountQuery.data?.status === accountStatus.validPayment) {
|
||||
await createEngineInstance(selectedTier);
|
||||
} else {
|
||||
trackEvent({
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ContractPublisher: React.FC<ContractPublisherProps> = async ({
|
||||
>
|
||||
<PublisherAvatar
|
||||
isPending={false}
|
||||
boxSize={5}
|
||||
className="size-5"
|
||||
address={addressOrEns || ""}
|
||||
/>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Judges = ({ TRACKING_CATEGORY }: JudgesProps) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-8">
|
||||
<div className="container relative flex max-w-[1200px] flex-col gap-8">
|
||||
<Heading size="title.2xl">Judges</Heading>
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, lg: 4 }}
|
||||
@@ -46,10 +46,9 @@ export const Judges = ({ TRACKING_CATEGORY }: JudgesProps) => {
|
||||
{judges.map((judge) => (
|
||||
<Flex key={judge.name} flexDir="column" gap={2} alignItems="center">
|
||||
<MaskedAvatar
|
||||
boxSize={40}
|
||||
src={judge.image}
|
||||
alt={judge.name}
|
||||
className="rounded-full"
|
||||
className="size-52"
|
||||
/>
|
||||
<Heading size="title.sm" mt={4} textAlign="center">
|
||||
{judge.name}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const JudgesEarn = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-8">
|
||||
<div className="container relative flex max-w-[1200px] flex-col gap-8">
|
||||
<Heading size="title.2xl">Judges</Heading>
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, lg: 4 }}
|
||||
@@ -38,10 +38,9 @@ export const JudgesEarn = () => {
|
||||
{judges.map((judge) => (
|
||||
<Flex key={judge.name} flexDir="column" gap={2} alignItems="center">
|
||||
<MaskedAvatar
|
||||
boxSize={40}
|
||||
src={judge.image}
|
||||
alt={judge.name}
|
||||
className="rounded-full"
|
||||
className="size-52"
|
||||
/>
|
||||
<Heading size="title.sm" mt={4} textAlign="center">
|
||||
{judge.name}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccountPlan } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { type AccountPlan, accountPlan } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Box, type CardProps, Flex } from "@chakra-ui/react";
|
||||
import {
|
||||
Badge,
|
||||
@@ -133,7 +133,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
<FeatureItem key={Array.isArray(f) ? f[0] : f} text={f} />
|
||||
))}
|
||||
</Flex>
|
||||
{name === AccountPlan.Growth && onDashboard ? (
|
||||
{name === accountPlan.growth && onDashboard ? (
|
||||
<UpgradeModal
|
||||
name={name}
|
||||
ctaProps={ctaProps}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccountPlan } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountPlan } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Box, Container, Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { Heading, Text, TrackedLink } from "tw-components";
|
||||
import { CONTACT_US_URL } from "utils/pricing";
|
||||
@@ -48,7 +48,7 @@ export const PricingSection: React.FC<PricingSectionProps> = ({
|
||||
|
||||
<SimpleGrid columns={{ base: 1, xl: 3 }} gap={{ base: 6, xl: 8 }}>
|
||||
<PricingCard
|
||||
name={AccountPlan.Free}
|
||||
name={accountPlan.free}
|
||||
ctaTitle="Get started for free"
|
||||
ctaProps={{
|
||||
category: trackingCategory,
|
||||
@@ -61,7 +61,7 @@ export const PricingSection: React.FC<PricingSectionProps> = ({
|
||||
ctaTitle={
|
||||
canTrialGrowth ? "Claim your 1-month free" : "Get started"
|
||||
}
|
||||
name={AccountPlan.Growth}
|
||||
name={accountPlan.growth}
|
||||
ctaHint={
|
||||
canTrialGrowth
|
||||
? "Your free trial will end after 30 days."
|
||||
@@ -82,7 +82,7 @@ export const PricingSection: React.FC<PricingSectionProps> = ({
|
||||
/>
|
||||
|
||||
<PricingCard
|
||||
name={AccountPlan.Pro}
|
||||
name={accountPlan.pro}
|
||||
ctaTitle="Contact us"
|
||||
ctaProps={{
|
||||
category: trackingCategory,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AccountPlan, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
type AccountPlan,
|
||||
accountPlan,
|
||||
useAccount,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
@@ -44,7 +48,7 @@ export const UpgradeModal: React.FC<UpgradeModalProps> = ({
|
||||
// FIXME: this needs to be re-worked
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (account.data?.plan === AccountPlan.Growth) {
|
||||
if (account.data?.plan === accountPlan.growth) {
|
||||
onClose();
|
||||
}
|
||||
}, [account.data?.plan, onClose]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccountPlan, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountPlan, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Textarea } from "@chakra-ui/react";
|
||||
import { Select as ChakraSelect } from "chakra-react-select";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
@@ -41,7 +41,7 @@ export const ApplyForOpCreditsForm: React.FC<ApplyForOpCreditsFormProps> = ({
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
thirdweb_account_id: account?.id || "",
|
||||
plan_type: PlanToCreditsRecord[account?.plan || AccountPlan.Free].title,
|
||||
plan_type: PlanToCreditsRecord[account?.plan || accountPlan.free].title,
|
||||
email: account?.email || "",
|
||||
company: "",
|
||||
website: "",
|
||||
@@ -208,6 +208,7 @@ export const ApplyForOpCreditsForm: React.FC<ApplyForOpCreditsFormProps> = ({
|
||||
"Ancient8",
|
||||
"Donatuz",
|
||||
"Mantle",
|
||||
"Soneium",
|
||||
].map((chain) => ({
|
||||
label: chain === "Optimism" ? "OP Mainnet" : chain,
|
||||
value: chain,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AccountPlan,
|
||||
AccountStatus,
|
||||
type AccountPlan,
|
||||
accountPlan,
|
||||
accountStatus,
|
||||
useAccount,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
@@ -32,13 +33,13 @@ export type CreditsRecord = {
|
||||
};
|
||||
|
||||
export const PlanToCreditsRecord: Record<AccountPlan, CreditsRecord> = {
|
||||
[AccountPlan.Free]: {
|
||||
[accountPlan.free]: {
|
||||
title: "Starter",
|
||||
upTo: true,
|
||||
credits: "$250",
|
||||
color: "#3b394b",
|
||||
},
|
||||
[AccountPlan.Growth]: {
|
||||
[accountPlan.growth]: {
|
||||
title: "Growth",
|
||||
upTo: true,
|
||||
credits: "$2,500",
|
||||
@@ -52,7 +53,7 @@ export const PlanToCreditsRecord: Record<AccountPlan, CreditsRecord> = {
|
||||
ctaTitle: "Upgrade for $99",
|
||||
ctaHref: "/dashboard/settings/billing",
|
||||
},
|
||||
[AccountPlan.Pro]: {
|
||||
[accountPlan.pro]: {
|
||||
title: "Pro",
|
||||
credits: "$3,000+",
|
||||
color: "#282B6F",
|
||||
@@ -64,7 +65,7 @@ export const PlanToCreditsRecord: Record<AccountPlan, CreditsRecord> = {
|
||||
ctaTitle: "Contact Us",
|
||||
ctaHref: "https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro",
|
||||
},
|
||||
[AccountPlan.Enterprise]: {
|
||||
[accountPlan.enterprise]: {
|
||||
title: "Enterprise",
|
||||
credits: "Custom",
|
||||
color: "#000000",
|
||||
@@ -94,16 +95,16 @@ export const ApplyForOpCreditsModal: React.FC = () => {
|
||||
|
||||
const hasValidPayment = useMemo(() => {
|
||||
return (
|
||||
!!(account?.data?.status === AccountStatus.ValidPayment) ||
|
||||
!!(account?.data?.status === accountStatus.validPayment) ||
|
||||
hasAddedPaymentMethod
|
||||
);
|
||||
}, [account?.data?.status, hasAddedPaymentMethod]);
|
||||
|
||||
const isFreePlan = account.data?.plan === AccountPlan.Free;
|
||||
const isProPlan = account.data?.plan === AccountPlan.Pro;
|
||||
const isFreePlan = account.data?.plan === accountPlan.free;
|
||||
const isProPlan = account.data?.plan === accountPlan.pro;
|
||||
|
||||
const creditsRecord =
|
||||
PlanToCreditsRecord[account.data?.plan || AccountPlan.Free];
|
||||
PlanToCreditsRecord[account.data?.plan || accountPlan.free];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -193,11 +194,11 @@ export const ApplyForOpCreditsModal: React.FC = () => {
|
||||
>
|
||||
{isFreePlan && (
|
||||
<PlanCard
|
||||
creditsRecord={PlanToCreditsRecord[AccountPlan.Growth]}
|
||||
creditsRecord={PlanToCreditsRecord[accountPlan.growth]}
|
||||
/>
|
||||
)}
|
||||
<PlanCard
|
||||
creditsRecord={PlanToCreditsRecord[AccountPlan.Pro]}
|
||||
creditsRecord={PlanToCreditsRecord[accountPlan.pro]}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AccountPlan,
|
||||
type AccountPlan,
|
||||
accountPlan,
|
||||
useUpdateAccountPlan,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { PricingCard } from "components/homepage/sections/PricingCard";
|
||||
@@ -24,7 +25,7 @@ const OnboardingChoosePlan: React.FC<OnboardingChoosePlanProps> = ({
|
||||
});
|
||||
|
||||
// free is default, so no need to update account
|
||||
if (plan === AccountPlan.Free) {
|
||||
if (plan === accountPlan.free) {
|
||||
trackEvent({
|
||||
category: "account",
|
||||
action: "choosePlan",
|
||||
@@ -76,13 +77,13 @@ const OnboardingChoosePlan: React.FC<OnboardingChoosePlanProps> = ({
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<PricingCard
|
||||
size="sm"
|
||||
name={AccountPlan.Free}
|
||||
name={accountPlan.free}
|
||||
ctaTitle="Get started for free"
|
||||
ctaProps={{
|
||||
category: "account",
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
handleSave(AccountPlan.Free);
|
||||
handleSave(accountPlan.free);
|
||||
},
|
||||
label: "freePlan",
|
||||
href: "/",
|
||||
@@ -93,7 +94,7 @@ const OnboardingChoosePlan: React.FC<OnboardingChoosePlanProps> = ({
|
||||
<PricingCard
|
||||
size="sm"
|
||||
ctaTitle="Claim your 1-month free"
|
||||
name={AccountPlan.Growth}
|
||||
name={accountPlan.growth}
|
||||
ctaHint="Your free trial will end after 30 days."
|
||||
canTrialGrowth={true}
|
||||
ctaProps={{
|
||||
@@ -101,7 +102,7 @@ const OnboardingChoosePlan: React.FC<OnboardingChoosePlanProps> = ({
|
||||
label: "growthPlan",
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
handleSave(AccountPlan.Growth);
|
||||
handleSave(accountPlan.growth);
|
||||
},
|
||||
href: "/",
|
||||
variant: "solid",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import {
|
||||
AccountStatus,
|
||||
accountStatus,
|
||||
useAccount,
|
||||
useAccountCredits,
|
||||
useApiKeys,
|
||||
@@ -76,7 +76,7 @@ export const OnboardingSteps: React.FC<OnboardingStepsProps> = ({
|
||||
);
|
||||
|
||||
const hasValidPayment = useMemo(() => {
|
||||
return meQuery?.data?.status === AccountStatus.ValidPayment;
|
||||
return meQuery?.data?.status === accountStatus.validPayment;
|
||||
}, [meQuery?.data?.status]);
|
||||
|
||||
const hasApiKeys = useMemo(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
type Account,
|
||||
AccountStatus,
|
||||
accountStatus,
|
||||
useAccount,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
@@ -26,9 +26,9 @@ function Loading() {
|
||||
|
||||
const skipBilling = (account: Account) => {
|
||||
return (
|
||||
[AccountStatus.ValidPayment, AccountStatus.PaymentVerification].includes(
|
||||
account.status,
|
||||
) || account.onboardSkipped
|
||||
account.status === accountStatus.validPayment ||
|
||||
account.status === accountStatus.paymentVerification ||
|
||||
account.onboardSkipped
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import {
|
||||
type Account,
|
||||
AccountStatus,
|
||||
accountStatus,
|
||||
useCreateBillingSession,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useMemo } from "react";
|
||||
@@ -26,14 +26,14 @@ export const ManageBillingButton: React.FC<ManageBillingButtonProps> = ({
|
||||
|
||||
const [buttonLabel, buttonText] = useMemo(() => {
|
||||
switch (account.status) {
|
||||
case AccountStatus.InvalidPayment:
|
||||
case AccountStatus.ValidPayment: {
|
||||
case accountStatus.invalidPayment:
|
||||
case accountStatus.validPayment: {
|
||||
return ["manage", "Manage billing"];
|
||||
}
|
||||
case AccountStatus.PaymentVerification: {
|
||||
case accountStatus.paymentVerification: {
|
||||
return ["verifyPaymentMethod", "Verify your payment method →"];
|
||||
}
|
||||
case AccountStatus.InvalidPaymentMethod: {
|
||||
case accountStatus.invalidPaymentMethod: {
|
||||
return ["addAnotherPayment", "Add another payment method →"];
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccountPlan } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { type AccountPlan, accountPlan } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { SimpleGrid } from "@chakra-ui/react";
|
||||
import { PricingCard } from "components/homepage/sections/PricingCard";
|
||||
import { useMemo } from "react";
|
||||
@@ -25,15 +25,13 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
loading,
|
||||
onSelect,
|
||||
}) => {
|
||||
const isPro = [AccountPlan.Pro, AccountPlan.Enterprise].includes(
|
||||
plan as AccountPlan,
|
||||
);
|
||||
const isPro = plan === accountPlan.pro || plan === accountPlan.enterprise;
|
||||
|
||||
const freeCtaTitle = useMemo(() => {
|
||||
if (!validPayment) {
|
||||
return "Get started for free";
|
||||
}
|
||||
if (plan !== AccountPlan.Free) {
|
||||
if (plan !== accountPlan.free) {
|
||||
return "Downgrade";
|
||||
}
|
||||
|
||||
@@ -51,7 +49,7 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
return "Contact us";
|
||||
}
|
||||
|
||||
if (plan === AccountPlan.Free) {
|
||||
if (plan === accountPlan.free) {
|
||||
return canTrialGrowth ? trialTitle : "Upgrade";
|
||||
}
|
||||
|
||||
@@ -65,14 +63,14 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, xl: 3 }} gap={{ base: 6, xl: 8 }}>
|
||||
<PricingCard
|
||||
current={plan === AccountPlan.Free}
|
||||
current={plan === accountPlan.free}
|
||||
size="sm"
|
||||
name={AccountPlan.Free}
|
||||
name={accountPlan.free}
|
||||
ctaTitle={freeCtaTitle}
|
||||
ctaProps={{
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
handleSelect(AccountPlan.Free);
|
||||
handleSelect(accountPlan.free);
|
||||
},
|
||||
isLoading: loading,
|
||||
isDisabled: loading || invalidPayment || paymentVerification,
|
||||
@@ -85,18 +83,18 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
|
||||
<PricingCard
|
||||
activeTrialEndsAt={
|
||||
plan === AccountPlan.Growth ? trialPeriodEndedAt : undefined
|
||||
plan === accountPlan.growth ? trialPeriodEndedAt : undefined
|
||||
}
|
||||
current={plan === AccountPlan.Growth}
|
||||
current={plan === accountPlan.growth}
|
||||
size="sm"
|
||||
name={AccountPlan.Growth}
|
||||
name={accountPlan.growth}
|
||||
ctaTitle={growthCtaTitle}
|
||||
ctaHint="Your free trial will end after 30 days."
|
||||
canTrialGrowth={canTrialGrowth}
|
||||
ctaProps={{
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
handleSelect(AccountPlan.Growth);
|
||||
handleSelect(accountPlan.growth);
|
||||
},
|
||||
isLoading: loading,
|
||||
isDisabled: loading || invalidPayment || paymentVerification,
|
||||
@@ -112,7 +110,7 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
<PricingCard
|
||||
current={isPro}
|
||||
size="sm"
|
||||
name={AccountPlan.Pro}
|
||||
name={accountPlan.pro}
|
||||
ctaTitle="Contact us"
|
||||
ctaProps={{
|
||||
category: "account",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { NavLink } from "@/components/ui/NavLink";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AccountPlan, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { accountPlan, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
@@ -16,7 +16,7 @@ export const UpgradeButton = () => {
|
||||
meQuery.isPending ||
|
||||
!meQuery.data ||
|
||||
pathname?.startsWith("/dashboard/settings/billing") ||
|
||||
meQuery.data?.plan !== AccountPlan.Free
|
||||
meQuery.data?.plan !== accountPlan.free
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type Account,
|
||||
AccountStatus,
|
||||
type UsageBillableByService,
|
||||
accountStatus,
|
||||
useAccount,
|
||||
useAccountUsage,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { OnboardingModal } from "components/onboarding/Modal";
|
||||
import { format } from "date-fns";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { ExternalLinkIcon, XIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import { LazyOnboardingBilling } from "../../../../onboarding/LazyOnboardingBilling";
|
||||
import { ManageBillingButton } from "../ManageButton";
|
||||
import { RecurringPaymentFailureAlert } from "./RecurringPaymentFailureAlert";
|
||||
@@ -45,11 +45,9 @@ export const BillingAlerts = (props: {
|
||||
const usageQuery = useAccountUsage();
|
||||
const meQuery = useAccount({
|
||||
refetchInterval: (account) =>
|
||||
[
|
||||
AccountStatus.InvalidPayment,
|
||||
AccountStatus.InvalidPaymentMethod,
|
||||
AccountStatus.PaymentVerification,
|
||||
].includes(account.state.data?.status as AccountStatus)
|
||||
account.state.data?.status === accountStatus.invalidPayment ||
|
||||
account.state.data?.status === accountStatus.invalidPaymentMethod ||
|
||||
account.state.data?.status === accountStatus.paymentVerification
|
||||
? 1000
|
||||
: false,
|
||||
});
|
||||
@@ -112,7 +110,7 @@ export function BillingAlertsUI(props: {
|
||||
const paymentFailureAlerts: AlertConditionType[] = [
|
||||
{
|
||||
shouldShowAlert:
|
||||
dashboardAccount.status === AccountStatus.PaymentVerification,
|
||||
dashboardAccount.status === accountStatus.paymentVerification,
|
||||
key: "verifyPaymentAlert",
|
||||
title: "Your payment method requires verification",
|
||||
description:
|
||||
@@ -122,7 +120,7 @@ export function BillingAlertsUI(props: {
|
||||
},
|
||||
{
|
||||
shouldShowAlert:
|
||||
dashboardAccount.status === AccountStatus.InvalidPaymentMethod,
|
||||
dashboardAccount.status === accountStatus.invalidPaymentMethod,
|
||||
key: "invalidPaymentMethodAlert",
|
||||
title: "Your payment method is invalid",
|
||||
description:
|
||||
@@ -162,7 +160,7 @@ export function BillingAlertsUI(props: {
|
||||
usage.storage.sumFileSizeBytes >= limits.storage;
|
||||
|
||||
const hasHardLimits =
|
||||
dashboardAccount.status !== AccountStatus.ValidPayment;
|
||||
dashboardAccount.status !== accountStatus.validPayment;
|
||||
const isFreePlan = dashboardAccount.plan === "free";
|
||||
const isGrowthPlan = dashboardAccount.plan === "growth";
|
||||
const usageAlerts: AlertConditionType[] = [
|
||||
@@ -301,7 +299,9 @@ export function BillingAlertsUI(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-4", props.className)}>{alerts}</div>
|
||||
<ErrorBoundary FallbackComponent={BillingAlertsErrorBoundary}>
|
||||
<div className={cn("flex flex-col gap-4", props.className)}>{alerts}</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -331,16 +331,11 @@ const AddPaymentNotification: React.FC<AddPaymentNotificationProps> = ({
|
||||
// TODO: We should find a way to move this deeper into the
|
||||
// TODO: ManageBillingButton component and set an optional field to override
|
||||
const [paymentMethodSaving, setPaymentMethodSaving] = useState(false);
|
||||
|
||||
const {
|
||||
onOpen: onPaymentMethodOpen,
|
||||
onClose: onPaymentMethodClose,
|
||||
isOpen: isPaymentMethodOpen,
|
||||
} = useDisclosure();
|
||||
const [isPaymentMethodOpen, setIsPaymentMethodOpen] = useState(false);
|
||||
|
||||
const handlePaymentAdded = () => {
|
||||
setPaymentMethodSaving(true);
|
||||
onPaymentMethodClose();
|
||||
setIsPaymentMethodOpen(false);
|
||||
};
|
||||
|
||||
const isBilling = ctaHref === "/dashboard/settings/billing";
|
||||
@@ -353,7 +348,7 @@ const AddPaymentNotification: React.FC<AddPaymentNotificationProps> = ({
|
||||
<OnboardingModal isOpen={isPaymentMethodOpen}>
|
||||
<LazyOnboardingBilling
|
||||
onSave={handlePaymentAdded}
|
||||
onCancel={onPaymentMethodClose}
|
||||
onCancel={() => setIsPaymentMethodOpen(false)}
|
||||
/>
|
||||
</OnboardingModal>
|
||||
|
||||
@@ -361,13 +356,13 @@ const AddPaymentNotification: React.FC<AddPaymentNotificationProps> = ({
|
||||
<AlertDescription>{description}</AlertDescription>
|
||||
|
||||
{showCTAs && (
|
||||
<div className="mt-4 flex gap-2">
|
||||
<div className="mt-4 flex flex-col gap-3 md:flex-row">
|
||||
{isBilling ? (
|
||||
<ManageBillingButton
|
||||
account={dashboardAccount}
|
||||
loading={paymentMethodSaving}
|
||||
loadingText="Verifying payment method"
|
||||
onClick={onPaymentMethodOpen}
|
||||
onClick={() => setIsPaymentMethodOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
<Button variant="outline" asChild>
|
||||
@@ -412,3 +407,17 @@ const AddPaymentNotification: React.FC<AddPaymentNotificationProps> = ({
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
function BillingAlertsErrorBoundary(errorProps: FallbackProps) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTag("component-crashed", "true");
|
||||
scope.setTag("component-crashed-boundary", "BillingAlertsErrorBoundary");
|
||||
scope.setLevel("fatal");
|
||||
Sentry.captureException(errorProps.error);
|
||||
});
|
||||
}, [errorProps.error]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+81
-86
@@ -1,12 +1,11 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { accountStatus } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { AccountStatus } from "../../../../../@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
createBillableServiceUsageDataStub,
|
||||
createDashboardAccountStub,
|
||||
} from "../../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../../stories/utils";
|
||||
import { BillingAlertsUI } from "./Alert";
|
||||
|
||||
const meta = {
|
||||
@@ -43,92 +42,88 @@ function tomorrow(d: Date) {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="py-4">
|
||||
<ChakraProviderSetup>
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="#usage">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo")}
|
||||
usageData={createBillableServiceUsageDataStub()}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="#usage">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo")}
|
||||
usageData={createBillableServiceUsageDataStub()}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="#paymentVerification">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
status: AccountStatus.PaymentVerification,
|
||||
stripePaymentActionUrl: "https://example.com",
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub({
|
||||
limits: {
|
||||
embeddedWallets: 100,
|
||||
storage: 100,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<BadgeContainer label="#paymentVerification">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
status: accountStatus.paymentVerification,
|
||||
stripePaymentActionUrl: "https://example.com",
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub({
|
||||
limits: {
|
||||
embeddedWallets: 100,
|
||||
storage: 100,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="#serviceCutoff">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
recurringPaymentFailures: [
|
||||
{
|
||||
subscriptionId: "s1",
|
||||
subscriptionDescription: "s1 desc",
|
||||
paymentFailureCode: "400",
|
||||
serviceCutoffDate: yesterday(new Date()).toISOString(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub({
|
||||
limits: {
|
||||
embeddedWallets: 100,
|
||||
storage: 100,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="#serviceCutoff">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
recurringPaymentFailures: [
|
||||
{
|
||||
subscriptionId: "s1",
|
||||
subscriptionDescription: "s1 desc",
|
||||
paymentFailureCode: "400",
|
||||
serviceCutoffDate: yesterday(new Date()).toISOString(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub({
|
||||
limits: {
|
||||
embeddedWallets: 100,
|
||||
storage: 100,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="#recurringPayment">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
recurringPaymentFailures: [
|
||||
{
|
||||
subscriptionId: "s1",
|
||||
subscriptionDescription: "s1 desc",
|
||||
paymentFailureCode: "400",
|
||||
serviceCutoffDate: tomorrow(new Date()).toISOString(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub({
|
||||
limits: {
|
||||
embeddedWallets: 100,
|
||||
storage: 100,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="#recurringPayment">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
recurringPaymentFailures: [
|
||||
{
|
||||
subscriptionId: "s1",
|
||||
subscriptionDescription: "s1 desc",
|
||||
paymentFailureCode: "400",
|
||||
serviceCutoffDate: tomorrow(new Date()).toISOString(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub({
|
||||
limits: {
|
||||
embeddedWallets: 100,
|
||||
storage: 100,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="usage + serviceCutoff">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
recurringPaymentFailures: [
|
||||
{
|
||||
subscriptionId: "s1",
|
||||
subscriptionDescription: "s1 desc",
|
||||
paymentFailureCode: "400",
|
||||
serviceCutoffDate: yesterday(new Date()).toISOString(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub()}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
</ChakraProviderSetup>
|
||||
</div>
|
||||
<BadgeContainer label="usage + serviceCutoff">
|
||||
<BillingAlertsUI
|
||||
dashboardAccount={createDashboardAccountStub("foo", {
|
||||
recurringPaymentFailures: [
|
||||
{
|
||||
subscriptionId: "s1",
|
||||
subscriptionDescription: "s1 desc",
|
||||
paymentFailureCode: "400",
|
||||
serviceCutoffDate: yesterday(new Date()).toISOString(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
usageData={createBillableServiceUsageDataStub()}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-8
@@ -1,12 +1,11 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { type Account, AccountStatus } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { type Account, accountStatus } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { OnboardingModal } from "components/onboarding/Modal";
|
||||
import { getRecurringPaymentFailureResponse } from "lib/billing";
|
||||
import { ExternalLinkIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Text } from "tw-components";
|
||||
import { LazyOnboardingBilling } from "../../../../onboarding/LazyOnboardingBilling";
|
||||
import { ManageBillingButton } from "../ManageButton";
|
||||
|
||||
@@ -70,23 +69,21 @@ export const RecurringPaymentFailureAlert: React.FC<
|
||||
<ul className="list-disc pl-3.5">
|
||||
{affectedServices.map((service) => (
|
||||
<li key={service}>
|
||||
<Text>{service}</Text>
|
||||
<span>{service}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<ManageBillingButton
|
||||
account={dashboardAccount}
|
||||
loading={paymentMethodSaving}
|
||||
loadingText="Verifying payment method"
|
||||
onClick={
|
||||
[
|
||||
AccountStatus.ValidPayment,
|
||||
AccountStatus.InvalidPayment,
|
||||
].includes(dashboardAccount.status)
|
||||
dashboardAccount.status === accountStatus.validPayment ||
|
||||
dashboardAccount.status === accountStatus.invalidPayment
|
||||
? undefined
|
||||
: () => setIsPaymentMethodOpen(true)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
import {
|
||||
type Account,
|
||||
AccountPlan,
|
||||
AccountStatus,
|
||||
type AccountPlan,
|
||||
accountPlan,
|
||||
accountStatus,
|
||||
useUpdateAccountPlan,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
@@ -30,7 +31,7 @@ interface BillingProps {
|
||||
|
||||
export const Billing: React.FC<BillingProps> = ({ account, teamId }) => {
|
||||
const updatePlanMutation = useUpdateAccountPlan(
|
||||
account?.plan === AccountPlan.Free,
|
||||
account?.plan === accountPlan.free,
|
||||
);
|
||||
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
|
||||
const [paymentMethodSaving, setPaymentMethodSaving] = useState(false);
|
||||
@@ -52,10 +53,10 @@ export const Billing: React.FC<BillingProps> = ({ account, teamId }) => {
|
||||
"Failed to change your billing plan.",
|
||||
);
|
||||
|
||||
const validPayment = account.status === AccountStatus.ValidPayment;
|
||||
const validPayment = account.status === accountStatus.validPayment;
|
||||
const paymentVerification =
|
||||
account.status === AccountStatus.PaymentVerification;
|
||||
const invalidPayment = account.status === AccountStatus.InvalidPayment;
|
||||
account.status === accountStatus.paymentVerification;
|
||||
const invalidPayment = account.status === accountStatus.invalidPayment;
|
||||
|
||||
const handleUpdatePlan = useCallback(
|
||||
(plan: AccountPlan, feedback?: string) => {
|
||||
@@ -114,7 +115,7 @@ export const Billing: React.FC<BillingProps> = ({ account, teamId }) => {
|
||||
return;
|
||||
}
|
||||
// downgrade from Growth to Free
|
||||
if (plan === AccountPlan.Free || account.plan === AccountPlan.Growth) {
|
||||
if (plan === accountPlan.free || account.plan === accountPlan.growth) {
|
||||
setDowngradePlan(plan);
|
||||
} else {
|
||||
handleUpdatePlan(plan);
|
||||
@@ -211,8 +212,8 @@ export const Billing: React.FC<BillingProps> = ({ account, teamId }) => {
|
||||
// and didn't have it already set, so update it here when payment
|
||||
// method is available.
|
||||
if (
|
||||
account.plan !== AccountPlan.Growth &&
|
||||
selectedPlan === AccountPlan.Growth
|
||||
account.plan !== accountPlan.growth &&
|
||||
selectedPlan === accountPlan.growth
|
||||
) {
|
||||
handleUpdatePlan(selectedPlan);
|
||||
setSelectedPlan(undefined);
|
||||
@@ -232,12 +233,11 @@ export const Billing: React.FC<BillingProps> = ({ account, teamId }) => {
|
||||
handleUpdatePlan,
|
||||
]);
|
||||
|
||||
const showSteps = [
|
||||
AccountStatus.NoCustomer,
|
||||
AccountStatus.NoPayment,
|
||||
AccountStatus.InvalidPayment,
|
||||
AccountStatus.InvalidPaymentMethod,
|
||||
].includes(account.status);
|
||||
const showSteps =
|
||||
account.status === accountStatus.noCustomer ||
|
||||
account.status === accountStatus.noPayment ||
|
||||
account.status === accountStatus.invalidPayment ||
|
||||
account.status === accountStatus.invalidPaymentMethod;
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={8}>
|
||||
|
||||
@@ -149,4 +149,4 @@ export type ApiKeyPayConfigValidationSchema = z.infer<
|
||||
>;
|
||||
|
||||
// FIXME: Remove
|
||||
export const HIDDEN_SERVICES = ["relayer", "chainsaw", "insight"];
|
||||
export const HIDDEN_SERVICES = ["relayer", "chainsaw"];
|
||||
|
||||
+2
-2
@@ -8,12 +8,12 @@ export function AccountAbstractionSummary(props: {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 lg:gap-6">
|
||||
<Stat
|
||||
label="Sponsored Transactions"
|
||||
label="Mainnet Sponsored Transactions"
|
||||
value={props.aggregateUserOpUsageQuery?.successful || 0}
|
||||
icon={ActivityIcon}
|
||||
/>
|
||||
<Stat
|
||||
label="Total Sponsored"
|
||||
label="Mainnet Gas Sponsored"
|
||||
value={props.aggregateUserOpUsageQuery?.sponsoredUsd || 0}
|
||||
formatter={(value: number) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
|
||||
+21
-3
@@ -21,7 +21,8 @@ import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import { formatTickerNumber } from "../../../lib/format-utils";
|
||||
|
||||
type ChartData = {
|
||||
time: string; // human readable date
|
||||
@@ -69,9 +70,12 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-muted/50 p-4 md:p-6">
|
||||
<h3 className="mb-4 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Sponsored Transactions
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
Total number of sponsored transactions.
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-8 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<ExportToCSVButton
|
||||
@@ -153,7 +157,21 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<ChartTooltip cursor={true} content={<ChartTooltipContent />} />
|
||||
<YAxis
|
||||
dataKey={(data) => data.successful + data.failed}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{(["failed", "successful"] as const).map((result) => {
|
||||
return (
|
||||
|
||||
+24
-4
@@ -22,8 +22,9 @@ import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import { useAllChainsData } from "../../../hooks/chains/allChains";
|
||||
import { formatTickerNumber } from "../../../lib/format-utils";
|
||||
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
@@ -51,7 +52,8 @@ export function TotalSponsoredChartCard(props: {
|
||||
if (!chartData) {
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[chainId || "Unknown"]: Math.round(stat.sponsoredUsd * 100) / 100,
|
||||
[chain?.name || chainId || "Unknown"]:
|
||||
Math.round(stat.sponsoredUsd * 100) / 100,
|
||||
} as ChartData);
|
||||
} else {
|
||||
chartData[chain?.name || chainId || "Unknown"] =
|
||||
@@ -120,7 +122,7 @@ export function TotalSponsoredChartCard(props: {
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<ExportToCSVButton
|
||||
className="bg-background"
|
||||
fileName="Connect Wallets"
|
||||
fileName="Gas Sponsored"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
// Shows the number of each type of wallet connected on all dates
|
||||
@@ -200,7 +202,25 @@ export function TotalSponsoredChartCard(props: {
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<ChartTooltip cursor={true} content={<ChartTooltipContent />} />
|
||||
<YAxis
|
||||
dataKey={(data) => {
|
||||
console.log(data);
|
||||
return Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
.reduce((acc, current) => Number(acc) + Number(current), 0);
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `$${formatTickerNumber(value)}`}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent valueFormatter={(value) => `$${value}`} />
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{uniqueChainIds.map((chainId) => {
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,8 @@ export const THIRDWEB_API_HOST = "/api/server-proxy/api";
|
||||
export const THIRDWEB_ANALYTICS_API_HOST = "/api/server-proxy/analytics";
|
||||
|
||||
export const THIRDWEB_EWS_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST || "https://ews.thirdweb.com";
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST ||
|
||||
"https://in-app-wallet.thirdweb.com";
|
||||
|
||||
export const THIRDWEB_PAY_DOMAIN =
|
||||
process.env.NEXT_PUBLIC_PAY_URL || "pay.thirdweb-dev.com";
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import "server-only";
|
||||
|
||||
export async function fetchAnalytics(input: string | URL, init?: RequestInit) {
|
||||
const [pathname, searchParams] = input.toString().split("?");
|
||||
if (!pathname) {
|
||||
throw new Error("Invalid input, no pathname provided");
|
||||
}
|
||||
|
||||
// create a new URL object for the analytics server
|
||||
const API_SERVER_URL = new URL(
|
||||
process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
|
||||
);
|
||||
API_SERVER_URL.pathname = pathname;
|
||||
for (const param of searchParams?.split("&") || []) {
|
||||
const [key, value] = param.split("=");
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
API_SERVER_URL.searchParams.append(
|
||||
decodeURIComponent(key),
|
||||
decodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
|
||||
return await fetch(API_SERVER_URL, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Bearer ${process.env.ANALYTICS_SERVICE_API_KEY}`,
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user