Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0b3b7f8f6 | ||
|
|
617fe68c32 | ||
|
|
01d0666208 | ||
|
|
621357a5c6 | ||
|
|
0527ee26b7 | ||
|
|
834ed8337b | ||
|
|
11bb4f5fee | ||
|
|
c547164320 | ||
|
|
38ae2d3c6b | ||
|
|
d75320191d | ||
|
|
6046d4510d | ||
|
|
f1d32261f0 | ||
|
|
04baa74ad7 | ||
|
|
44bb0034a6 | ||
|
|
cfeba37e40 | ||
|
|
fb446bd099 | ||
|
|
24063af948 | ||
|
|
e2c9d4de34 | ||
|
|
0f6aa6b85d | ||
|
|
9f1a77f4c4 | ||
|
|
158a08aafc | ||
|
|
67354dc848 | ||
|
|
575b7b3a58 | ||
|
|
35ccf8f2e8 | ||
|
|
b5e02a4f10 | ||
|
|
cb71dd597e | ||
|
|
83ec6957f9 | ||
|
|
c0bbc21a84 | ||
|
|
9fcf1b80de | ||
|
|
33ddf56a0a | ||
|
|
f9fa5df6b1 | ||
|
|
0d82fc0dd1 | ||
|
|
ef6029b939 | ||
|
|
004601812a |
@@ -55,4 +55,6 @@ module.exports = [
|
||||
"/learn/guides/:guide_slug",
|
||||
"/learn/courses",
|
||||
"/learn/glossary",
|
||||
// -- faucets --
|
||||
"/faucets",
|
||||
];
|
||||
|
||||
@@ -153,7 +153,7 @@ const baseNextConfig: NextConfig = {
|
||||
// re-write /home to / (this is so that logged in users will be able to go to /home and NOT be redirected to the logged in app)
|
||||
{
|
||||
source: "/home",
|
||||
destination: "/",
|
||||
destination: "https://landing.thirdweb.com",
|
||||
},
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
source: path,
|
||||
|
||||
+21
-21
@@ -45,9 +45,9 @@
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "1.1.8",
|
||||
"@sentry/nextjs": "9.2.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.66.9",
|
||||
"@sentry/nextjs": "9.3.0",
|
||||
"@shazow/whatsabi": "0.20.0",
|
||||
"@tanstack/react-query": "5.67.1",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "2.0.0",
|
||||
@@ -61,13 +61,13 @@
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "12.4.7",
|
||||
"framer-motion": "12.4.10",
|
||||
"fuse.js": "7.1.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.5.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.476.0",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "15.2.0",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.4",
|
||||
@@ -94,7 +94,7 @@
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "2.0.1",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.19.0",
|
||||
"swagger-ui-react": "^5.20.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
@@ -104,21 +104,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@chromatic-com/storybook": "3.2.5",
|
||||
"@next/bundle-analyzer": "15.2.0",
|
||||
"@next/eslint-plugin-next": "15.2.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-essentials": "8.6.0",
|
||||
"@storybook/addon-interactions": "8.6.0",
|
||||
"@storybook/addon-links": "8.6.0",
|
||||
"@storybook/addon-onboarding": "8.6.0",
|
||||
"@storybook/addon-viewport": "8.6.0",
|
||||
"@storybook/blocks": "8.6.0",
|
||||
"@storybook/nextjs": "8.6.0",
|
||||
"@storybook/react": "8.6.0",
|
||||
"@storybook/test": "8.6.0",
|
||||
"@storybook/addon-essentials": "8.6.3",
|
||||
"@storybook/addon-interactions": "8.6.3",
|
||||
"@storybook/addon-links": "8.6.3",
|
||||
"@storybook/addon-onboarding": "8.6.3",
|
||||
"@storybook/addon-viewport": "8.6.3",
|
||||
"@storybook/blocks": "8.6.3",
|
||||
"@storybook/nextjs": "8.6.3",
|
||||
"@storybook/react": "8.6.3",
|
||||
"@storybook/test": "8.6.3",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -130,16 +130,16 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.19.1",
|
||||
"checkly": "5.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"eslint-plugin-storybook": "0.11.3",
|
||||
"knip": "5.45.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.3",
|
||||
"storybook": "8.6.0",
|
||||
"storybook": "8.6.3",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,3 +172,10 @@ input:-webkit-autofill:active {
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
/* Remove this when chakra is removed */
|
||||
/* Make chakra modals have same z-index as shadcn dialog */
|
||||
body {
|
||||
--chakra-zIndices-modal: 50;
|
||||
--chakra-zIndices-overlay: 50;
|
||||
}
|
||||
|
||||
@@ -371,7 +371,6 @@ export type RotateSecretKeyAPIReturnType = {
|
||||
data: {
|
||||
secret: string;
|
||||
secretMasked: string;
|
||||
secretHash: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -232,13 +232,15 @@ export function useEngineUpdateDeployment() {
|
||||
|
||||
export type RemoveEngineFromDashboardIParams = {
|
||||
instanceId: string;
|
||||
teamIdOrSlug: string;
|
||||
};
|
||||
|
||||
export async function removeEngineFromDashboard({
|
||||
instanceId,
|
||||
teamIdOrSlug,
|
||||
}: RemoveEngineFromDashboardIParams) {
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/engine/${instanceId}`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${instanceId}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
@@ -273,18 +275,20 @@ export async function deleteCloudHostedEngine({
|
||||
}
|
||||
|
||||
export type EditEngineInstanceParams = {
|
||||
teamIdOrSlug: string;
|
||||
instanceId: string;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export async function editEngineInstance({
|
||||
teamIdOrSlug,
|
||||
instanceId,
|
||||
name,
|
||||
url,
|
||||
}: EditEngineInstanceParams) {
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/engine/${instanceId}`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${instanceId}`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -1628,7 +1632,7 @@ export function useEngineSubscriptionsLastBlock(params: {
|
||||
|
||||
interface EngineResourceMetrics {
|
||||
error: string;
|
||||
data: {
|
||||
result: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
errorRate: ResultItem[];
|
||||
@@ -1637,7 +1641,7 @@ interface EngineResourceMetrics {
|
||||
};
|
||||
}
|
||||
|
||||
export function useEngineSystemMetrics(engineId: string) {
|
||||
export function useEngineSystemMetrics(engineId: string, teamIdOrSlug: string) {
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
|
||||
return useQuery({
|
||||
@@ -1645,7 +1649,7 @@ export function useEngineSystemMetrics(engineId: string) {
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "GET",
|
||||
pathname: `/v1/engine/${engineId}/metrics`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/metrics`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -1674,14 +1678,14 @@ export interface EngineAlertRule {
|
||||
pausedAt: Date | null;
|
||||
}
|
||||
|
||||
export function useEngineAlertRules(engineId: string) {
|
||||
export function useEngineAlertRules(engineId: string, teamIdOrSlug: string) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.alertRules(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineAlertRule[];
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/alert-rules`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/alert-rules`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -1703,14 +1707,19 @@ export interface EngineAlert {
|
||||
endsAt: Date | null;
|
||||
}
|
||||
|
||||
export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
|
||||
export function useEngineAlerts(
|
||||
engineId: string,
|
||||
teamIdOrSlug: string,
|
||||
limit: number,
|
||||
offset = 0,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.alerts(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineAlert[];
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/alerts`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/alerts`,
|
||||
searchParams: {
|
||||
limit: `${limit}`,
|
||||
offset: `${offset}`,
|
||||
@@ -1754,14 +1763,17 @@ export type EngineNotificationChannel = {
|
||||
subscriptionRoutes: string[];
|
||||
};
|
||||
|
||||
export function useEngineNotificationChannels(engineId: string) {
|
||||
export function useEngineNotificationChannels(
|
||||
engineId: string,
|
||||
teamIdOrSlug: string,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineNotificationChannel[];
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/notification-channels`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -1781,7 +1793,10 @@ export interface CreateNotificationChannelInput {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
export function useEngineCreateNotificationChannel(
|
||||
engineId: string,
|
||||
teamIdOrSlug: string,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
@@ -1789,7 +1804,7 @@ export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineNotificationChannel;
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/notification-channels`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -1812,13 +1827,16 @@ export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
export function useEngineDeleteNotificationChannel(
|
||||
engineId: string,
|
||||
teamIdOrSlug: string,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (notificationChannelId: string) => {
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels/${notificationChannelId}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,16 @@ export function AccountSettingsPage(props: {
|
||||
client={props.client}
|
||||
defaultTeamSlug={props.defaultTeamSlug}
|
||||
defaultTeamName={props.defaultTeamName}
|
||||
cancelSubscriptions={async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "DELETE",
|
||||
pathname: `/v1/teams/${props.defaultTeamSlug}/subscriptions`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
}}
|
||||
onAccountDeleted={async () => {
|
||||
await doLogout();
|
||||
if (activeWallet) {
|
||||
|
||||
@@ -143,6 +143,9 @@ function Variants() {
|
||||
onAccountDeleted={() => {
|
||||
console.log("Account deleted");
|
||||
}}
|
||||
cancelSubscriptions={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
import { EllipsisIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { CircleXIcon, EllipsisIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -64,6 +64,7 @@ export function AccountSettingsPageUI(props: {
|
||||
defaultTeamSlug: string;
|
||||
defaultTeamName: string;
|
||||
redirectToBillingPortal: BillingBillingPortalAction;
|
||||
cancelSubscriptions: () => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -89,6 +90,7 @@ export function AccountSettingsPageUI(props: {
|
||||
defaultTeamSlug={props.defaultTeamSlug}
|
||||
defaultTeamName={props.defaultTeamName}
|
||||
redirectToBillingPortal={props.redirectToBillingPortal}
|
||||
cancelSubscriptions={props.cancelSubscriptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -207,6 +209,7 @@ function DeleteAccountCard(props: {
|
||||
defaultTeamSlug: string;
|
||||
defaultTeamName: string;
|
||||
redirectToBillingPortal: BillingBillingPortalAction;
|
||||
cancelSubscriptions: () => Promise<void>;
|
||||
}) {
|
||||
const title = "Delete Account";
|
||||
const description = (
|
||||
@@ -230,6 +233,10 @@ function DeleteAccountCard(props: {
|
||||
},
|
||||
});
|
||||
|
||||
const cancelSubscriptions = useMutation({
|
||||
mutationFn: props.cancelSubscriptions,
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
deleteAccount.mutate();
|
||||
}
|
||||
@@ -264,25 +271,47 @@ function DeleteAccountCard(props: {
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Failed to delete account</AlertTitle>
|
||||
<AlertDescription>
|
||||
<span className="mb-1 block">
|
||||
Your default team "{props.defaultTeamName}" has active
|
||||
subscriptions. These subscriptions have to be cancelled
|
||||
first before deleting account
|
||||
</span>
|
||||
<span className="mb-4 block">
|
||||
Reach out to support to help you cancel them
|
||||
</span>
|
||||
<Button
|
||||
variant="default"
|
||||
asChild
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Link href="/support/create-ticket">
|
||||
Contact Support
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="mb-4">
|
||||
<span className="block">
|
||||
Your default team "{props.defaultTeamName}" has active
|
||||
subscriptions. These subscriptions have to be cancelled
|
||||
before deleting account
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 lg:flex-row">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => {
|
||||
const promise = cancelSubscriptions.mutateAsync();
|
||||
toast.promise(promise, {
|
||||
success: "Subscriptions cancelled successfully",
|
||||
error: "Failed to cancel subscriptions",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{cancelSubscriptions.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<CircleXIcon className="size-4" />
|
||||
)}
|
||||
Cancel subscriptions
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Link href="/support/create-ticket" target="_blank">
|
||||
Contact Support
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@ export function MobileBurgerMenuButton(
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
href="/home"
|
||||
className="text-base text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Home Page
|
||||
|
||||
@@ -24,7 +24,7 @@ const LazyOnboardingUI = lazy(
|
||||
() => import("./onboarding/on-boarding-ui.client"),
|
||||
);
|
||||
|
||||
const wallets = [
|
||||
const loginOptions = [
|
||||
inAppWallet({
|
||||
auth: {
|
||||
options: [
|
||||
@@ -45,9 +45,27 @@ const wallets = [
|
||||
createWallet("io.zerion.wallet"),
|
||||
];
|
||||
|
||||
const inAppWalletLoginOptions = [
|
||||
inAppWallet({
|
||||
auth: {
|
||||
options: [
|
||||
"google",
|
||||
"apple",
|
||||
"facebook",
|
||||
"github",
|
||||
"email",
|
||||
"phone",
|
||||
"passkey",
|
||||
"wallet",
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export function LoginAndOnboardingPage(props: {
|
||||
account: Account | undefined;
|
||||
redirectPath: string;
|
||||
loginWithInAppWallet: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex min-h-dvh flex-col overflow-hidden bg-background">
|
||||
@@ -91,6 +109,7 @@ export function LoginAndOnboardingPage(props: {
|
||||
<LoginAndOnboardingPageContent
|
||||
account={props.account}
|
||||
redirectPath={props.redirectPath}
|
||||
loginWithInAppWallet={props.loginWithInAppWallet}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -99,6 +118,7 @@ export function LoginAndOnboardingPage(props: {
|
||||
export function LoginAndOnboardingPageContent(props: {
|
||||
account: Account | undefined;
|
||||
redirectPath: string;
|
||||
loginWithInAppWallet: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex grow flex-col">
|
||||
@@ -114,6 +134,7 @@ export function LoginAndOnboardingPageContent(props: {
|
||||
<PageContent
|
||||
redirectPath={props.redirectPath}
|
||||
account={props.account}
|
||||
loginWithInAppWallet={props.loginWithInAppWallet}
|
||||
/>
|
||||
</ClientOnly>
|
||||
</main>
|
||||
@@ -139,6 +160,7 @@ function LoadingCard() {
|
||||
function PageContent(props: {
|
||||
redirectPath: string;
|
||||
account: Account | undefined;
|
||||
loginWithInAppWallet: boolean;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<
|
||||
| { id: "login" }
|
||||
@@ -190,7 +212,12 @@ function PageContent(props: {
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
return (
|
||||
<CustomConnectEmbed
|
||||
onLogin={onLogin}
|
||||
loginWithInAppWallet={props.loginWithInAppWallet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
@@ -215,6 +242,7 @@ function PageContent(props: {
|
||||
|
||||
function CustomConnectEmbed(props: {
|
||||
onLogin: () => void;
|
||||
loginWithInAppWallet: boolean;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const client = useThirdwebClient();
|
||||
@@ -222,6 +250,16 @@ function CustomConnectEmbed(props: {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Turnstile
|
||||
options={{
|
||||
// only show if interaction is required
|
||||
appearance: "interaction-only",
|
||||
// match the theme of the rest of the app
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
}}
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
onSuccess={(token) => setTurnstileToken(token)}
|
||||
/>
|
||||
<ConnectEmbed
|
||||
auth={{
|
||||
getLoginPayload,
|
||||
@@ -247,7 +285,9 @@ function CustomConnectEmbed(props: {
|
||||
return isLoggedInResult;
|
||||
},
|
||||
}}
|
||||
wallets={wallets}
|
||||
wallets={
|
||||
props.loginWithInAppWallet ? inAppWalletLoginOptions : loginOptions
|
||||
}
|
||||
client={client}
|
||||
modalSize="wide"
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
@@ -255,16 +295,6 @@ function CustomConnectEmbed(props: {
|
||||
privacyPolicyUrl="/privacy-policy"
|
||||
termsOfServiceUrl="/terms"
|
||||
/>
|
||||
<Turnstile
|
||||
options={{
|
||||
// only show if interaction is required
|
||||
appearance: "interaction-only",
|
||||
// match the theme of the rest of the app
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
}}
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
onSuccess={(token) => setTurnstileToken(token)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import { isValidEncodedRedirectPath } from "./isValidEncodedRedirectPath";
|
||||
|
||||
export default async function Page(props: {
|
||||
searchParams: Promise<{
|
||||
next?: string;
|
||||
next: string | string[] | undefined;
|
||||
"in-app-wallet": string | string[] | undefined;
|
||||
}>;
|
||||
}) {
|
||||
const nextPath = (await props.searchParams).next;
|
||||
const searchParams = await props.searchParams;
|
||||
const nextPath =
|
||||
typeof searchParams.next === "string" ? searchParams.next : undefined;
|
||||
const account = await getRawAccount();
|
||||
|
||||
// don't redirect away from login page if authToken is already present and onboarding is done
|
||||
@@ -20,6 +23,10 @@ export default async function Page(props: {
|
||||
nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/team";
|
||||
|
||||
return (
|
||||
<LoginAndOnboardingPage account={account} redirectPath={redirectPath} />
|
||||
<LoginAndOnboardingPage
|
||||
account={account}
|
||||
redirectPath={redirectPath}
|
||||
loginWithInAppWallet={searchParams["in-app-wallet"] === "true"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export function NebulaLoginPage(props: {
|
||||
|
||||
{showPage === "connect" && (
|
||||
<LoginAndOnboardingPageContent
|
||||
loginWithInAppWallet={false}
|
||||
account={props.account}
|
||||
redirectPath={
|
||||
message ? `/?prompt=${encodeURIComponent(message)}` : "/"
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const [ecosystem, team] = await Promise.all([
|
||||
fetchEcosystem(params.slug, authToken),
|
||||
fetchEcosystem(params.slug, authToken, params.team_slug),
|
||||
getTeamBySlug(params.team_slug),
|
||||
]);
|
||||
|
||||
|
||||
+7
-2
@@ -10,7 +10,7 @@ export async function EcosystemLayoutSlug({
|
||||
ecosystemLayoutPath,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { slug: string };
|
||||
params: { slug: string; team_slug: string };
|
||||
ecosystemLayoutPath: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
@@ -19,7 +19,11 @@ export async function EcosystemLayoutSlug({
|
||||
redirect(ecosystemLayoutPath);
|
||||
}
|
||||
|
||||
const ecosystem = await fetchEcosystem(params.slug, authToken);
|
||||
const ecosystem = await fetchEcosystem(
|
||||
params.slug,
|
||||
authToken,
|
||||
params.team_slug,
|
||||
);
|
||||
|
||||
if (!ecosystem) {
|
||||
redirect(ecosystemLayoutPath);
|
||||
@@ -30,6 +34,7 @@ export async function EcosystemLayoutSlug({
|
||||
<EcosystemHeader
|
||||
ecosystem={ecosystem}
|
||||
ecosystemLayoutPath={ecosystemLayoutPath}
|
||||
teamIdOrSlug={params.team_slug}
|
||||
/>
|
||||
|
||||
<SidebarLayout
|
||||
|
||||
+7
-1
@@ -62,8 +62,11 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
|
||||
function EcosystemSelect(props: {
|
||||
ecosystem: Ecosystem;
|
||||
ecosystemLayoutPath: string;
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
const { data: ecosystems, isPending } = useEcosystemList();
|
||||
const { data: ecosystems, isPending } = useEcosystemList({
|
||||
teamIdOrSlug: props.teamIdOrSlug,
|
||||
});
|
||||
|
||||
return isPending ? (
|
||||
<Skeleton className="h-10 w-full md:w-[160px]" />
|
||||
@@ -109,8 +112,10 @@ function EcosystemSelect(props: {
|
||||
export function EcosystemHeader(props: {
|
||||
ecosystem: Ecosystem;
|
||||
ecosystemLayoutPath: string;
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
const { data: fetchedEcosystem } = useEcosystem({
|
||||
teamIdOrSlug: props.teamIdOrSlug,
|
||||
slug: props.ecosystem.slug,
|
||||
refetchInterval:
|
||||
props.ecosystem.status === "requested"
|
||||
@@ -192,6 +197,7 @@ export function EcosystemHeader(props: {
|
||||
<EcosystemSelect
|
||||
ecosystem={ecosystem}
|
||||
ecosystemLayoutPath={props.ecosystemLayoutPath}
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-2
@@ -7,8 +7,11 @@ import { IntegrationPermissionsSection } from "../server/integration-permissions
|
||||
export function EcosystemPermissionsPage({
|
||||
params,
|
||||
authToken,
|
||||
}: { params: { slug: string }; authToken: string }) {
|
||||
const { data: ecosystem } = useEcosystem({ slug: params.slug });
|
||||
}: { params: { slug: string; team_slug: string }; authToken: string }) {
|
||||
const { data: ecosystem } = useEcosystem({
|
||||
slug: params.slug,
|
||||
teamIdOrSlug: params.team_slug,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
|
||||
+4
-2
@@ -3,21 +3,23 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
|
||||
export function useEcosystem({
|
||||
teamIdOrSlug,
|
||||
slug,
|
||||
refetchInterval,
|
||||
refetchOnWindowFocus,
|
||||
initialData,
|
||||
}: {
|
||||
teamIdOrSlug: string;
|
||||
slug: string;
|
||||
refetchInterval?: number;
|
||||
refetchOnWindowFocus?: boolean;
|
||||
initialData?: Ecosystem;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: ["ecosystems", slug],
|
||||
queryKey: ["ecosystems", teamIdOrSlug, slug],
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/ecosystem-wallet/${slug}`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
+10
-5
@@ -31,9 +31,13 @@ const formSchema = z.object({
|
||||
.refine((name) => /^[a-zA-Z0-9 ]*$/.test(name), {
|
||||
message: "Name can only contain letters, numbers and spaces",
|
||||
}),
|
||||
logo: z.instanceof(File, {
|
||||
message: "Logo is required",
|
||||
}),
|
||||
logo: z
|
||||
.instanceof(File, {
|
||||
message: "Logo is required",
|
||||
})
|
||||
.refine((file) => file.size <= 500 * 1024, {
|
||||
message: "Logo size must be less than 500KB",
|
||||
}),
|
||||
permission: z.union([z.literal("PARTNER_WHITELIST"), z.literal("ANYONE")]),
|
||||
});
|
||||
|
||||
@@ -118,8 +122,9 @@ export function CreateEcosystemForm(props: { teamSlug: string }) {
|
||||
accept="image/png, image/jpeg"
|
||||
onUpload={(files) => {
|
||||
if (files[0]) {
|
||||
form.setValue("logo", files[0]);
|
||||
form.clearErrors("logo");
|
||||
form.setValue("logo", files[0], {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
+7
-3
@@ -3,13 +3,17 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export function useEcosystemList() {
|
||||
export function useEcosystemList({
|
||||
teamIdOrSlug,
|
||||
}: {
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
queryKey: ["ecosystems", address],
|
||||
queryKey: ["ecosystems", teamIdOrSlug, address],
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
pathname: "/v1/ecosystem-wallet/list",
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
@@ -20,10 +20,12 @@ export default async function Page(props: {
|
||||
loginRedirect(ecosystemLayoutPath);
|
||||
}
|
||||
|
||||
const ecosystems = await fetchEcosystemList(authToken).catch((err) => {
|
||||
console.error("failed to fetch ecosystems", err);
|
||||
return [];
|
||||
});
|
||||
const ecosystems = await fetchEcosystemList(authToken, team_slug).catch(
|
||||
(err) => {
|
||||
console.error("failed to fetch ecosystems", err);
|
||||
return [];
|
||||
},
|
||||
);
|
||||
if (ecosystems[0]) {
|
||||
redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export async function fetchEcosystem(slug: string, authToken: string) {
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/${slug}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
export async function fetchEcosystem(
|
||||
slug: string,
|
||||
authToken: string,
|
||||
teamIdOrSlug: string,
|
||||
) {
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
console.error(data);
|
||||
|
||||
+11
-5
@@ -1,12 +1,18 @@
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export async function fetchEcosystemList(authToken: string) {
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/list`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
export async function fetchEcosystemList(
|
||||
authToken: string,
|
||||
teamIdOrSlug: string,
|
||||
) {
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
|
||||
+6
-3
@@ -28,12 +28,15 @@ const formSchema = z.object({
|
||||
|
||||
type ImportEngineParams = z.infer<typeof formSchema>;
|
||||
|
||||
async function importEngine(data: ImportEngineParams) {
|
||||
async function importEngine({
|
||||
teamIdOrSlug,
|
||||
...data
|
||||
}: ImportEngineParams & { teamIdOrSlug: string }) {
|
||||
// Instance URLs should end with a /.
|
||||
const url = data.url.endsWith("/") ? data.url : `${data.url}/`;
|
||||
|
||||
const res = await apiServerProxy({
|
||||
pathname: "/v1/engine",
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/engine`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -59,7 +62,7 @@ export function EngineImportCard(props: {
|
||||
<EngineImportCardUI
|
||||
prefillImportUrl={props.prefillImportUrl}
|
||||
importEngine={async (params) => {
|
||||
await importEngine(params);
|
||||
await importEngine({ ...params, teamIdOrSlug: props.teamSlug });
|
||||
router.push(`/team/${props.teamSlug}/~/engine`);
|
||||
}}
|
||||
/>
|
||||
|
||||
+15
@@ -74,6 +74,7 @@ type RemovedEngineFromDashboard = (
|
||||
) => Promise<void>;
|
||||
|
||||
export function EngineInstancesTable(props: {
|
||||
teamIdOrSlug: string;
|
||||
instances: EngineInstance[];
|
||||
engineLinkPrefix: string;
|
||||
}) {
|
||||
@@ -81,6 +82,7 @@ export function EngineInstancesTable(props: {
|
||||
|
||||
return (
|
||||
<EngineInstancesTableUI
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
instances={props.instances}
|
||||
engineLinkPrefix={props.engineLinkPrefix}
|
||||
deleteCloudHostedEngine={async (params) => {
|
||||
@@ -100,6 +102,7 @@ export function EngineInstancesTable(props: {
|
||||
}
|
||||
|
||||
export function EngineInstancesTableUI(props: {
|
||||
teamIdOrSlug: string;
|
||||
instances: EngineInstance[];
|
||||
engineLinkPrefix: string;
|
||||
deleteCloudHostedEngine: DeletedCloudHostedEngine;
|
||||
@@ -124,6 +127,7 @@ export function EngineInstancesTableUI(props: {
|
||||
{props.instances.map((instance) => (
|
||||
<EngineInstanceRow
|
||||
key={instance.id}
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
instance={instance}
|
||||
engineLinkPrefix={props.engineLinkPrefix}
|
||||
deleteCloudHostedEngine={props.deleteCloudHostedEngine}
|
||||
@@ -145,6 +149,7 @@ export function EngineInstancesTableUI(props: {
|
||||
}
|
||||
|
||||
function EngineInstanceRow(props: {
|
||||
teamIdOrSlug: string;
|
||||
instance: EngineInstance;
|
||||
engineLinkPrefix: string;
|
||||
deleteCloudHostedEngine: DeletedCloudHostedEngine;
|
||||
@@ -187,6 +192,7 @@ function EngineInstanceRow(props: {
|
||||
</TableRow>
|
||||
|
||||
<EditModal
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
instance={instance}
|
||||
open={isEditModalOpen}
|
||||
onOpenChange={setIsEditModalOpen}
|
||||
@@ -194,6 +200,7 @@ function EngineInstanceRow(props: {
|
||||
/>
|
||||
|
||||
<RemoveModal
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
instance={instance}
|
||||
onOpenChange={setIsRemoveModalOpen}
|
||||
open={isRemoveModalOpen}
|
||||
@@ -349,6 +356,7 @@ function EngineActionsDropdown(props: {
|
||||
|
||||
function EditModal(props: {
|
||||
open: boolean;
|
||||
teamIdOrSlug: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
instance: EngineInstance;
|
||||
editEngineInstance: EditedEngineInstance;
|
||||
@@ -357,6 +365,7 @@ function EditModal(props: {
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<EditModalContent
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
instance={props.instance}
|
||||
editEngineInstance={props.editEngineInstance}
|
||||
closeModal={() => props.onOpenChange(false)}
|
||||
@@ -372,6 +381,7 @@ const editEngineFormSchema = z.object({
|
||||
});
|
||||
|
||||
function EditModalContent(props: {
|
||||
teamIdOrSlug: string;
|
||||
instance: EngineInstance;
|
||||
editEngineInstance: EditedEngineInstance;
|
||||
closeModal: () => void;
|
||||
@@ -396,6 +406,7 @@ function EditModalContent(props: {
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
editInstance.mutate(
|
||||
{
|
||||
teamIdOrSlug: props.teamIdOrSlug,
|
||||
instanceId: props.instance.id,
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
@@ -475,6 +486,7 @@ function EditModalContent(props: {
|
||||
}
|
||||
|
||||
function RemoveModal(props: {
|
||||
teamIdOrSlug: string;
|
||||
instance: EngineInstance;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -491,6 +503,7 @@ function RemoveModal(props: {
|
||||
(instance.status === "active" && !instance.deploymentId) ? (
|
||||
<RemoveEngineFromDashboardModalContent
|
||||
instance={instance}
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
close={() => onOpenChange(false)}
|
||||
removeEngineFromDashboard={props.removeEngineFromDashboard}
|
||||
/>
|
||||
@@ -507,6 +520,7 @@ function RemoveModal(props: {
|
||||
}
|
||||
|
||||
function RemoveEngineFromDashboardModalContent(props: {
|
||||
teamIdOrSlug: string;
|
||||
instance: EngineInstance;
|
||||
close: () => void;
|
||||
removeEngineFromDashboard: RemovedEngineFromDashboard;
|
||||
@@ -552,6 +566,7 @@ function RemoveEngineFromDashboardModalContent(props: {
|
||||
removeFromDashboard.mutate(
|
||||
{
|
||||
instanceId: instance.id,
|
||||
teamIdOrSlug: props.teamIdOrSlug,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ export const EngineInstancesList = (props: {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<EngineInstancesTable
|
||||
teamIdOrSlug={props.team_slug}
|
||||
instances={props.instances}
|
||||
engineLinkPrefix={engineLinkPrefix}
|
||||
/>
|
||||
|
||||
@@ -29,7 +29,10 @@ export default async function Page(props: {
|
||||
loginRedirect(`/team/${params.team_slug}/~/engine`);
|
||||
}
|
||||
|
||||
const res = await getEngineInstances({ authToken });
|
||||
const res = await getEngineInstances({
|
||||
authToken,
|
||||
teamIdOrSlug: params.team_slug,
|
||||
});
|
||||
|
||||
return (
|
||||
<EngineInstancesList
|
||||
|
||||
+7
-1
@@ -9,8 +9,12 @@ import { RecentEngineAlertsSection } from "./RecentEngineAlerts";
|
||||
|
||||
export function EngineAlertsPage(props: {
|
||||
instance: EngineInstance;
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
const alertRulesQuery = useEngineAlertRules(props.instance.id);
|
||||
const alertRulesQuery = useEngineAlertRules(
|
||||
props.instance.id,
|
||||
props.teamIdOrSlug,
|
||||
);
|
||||
const alertRules = alertRulesQuery.data ?? [];
|
||||
|
||||
return (
|
||||
@@ -19,12 +23,14 @@ export function EngineAlertsPage(props: {
|
||||
alertRules={alertRules}
|
||||
engineId={props.instance.id}
|
||||
alertRulesIsLoading={alertRulesQuery.isLoading}
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
/>
|
||||
<div className="h-8" />
|
||||
<RecentEngineAlertsSection
|
||||
alertRules={alertRules}
|
||||
engineId={props.instance.id}
|
||||
alertRulesIsLoading={alertRulesQuery.isLoading}
|
||||
teamIdOrSlug={props.teamIdOrSlug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+4
@@ -43,19 +43,23 @@ type CreateAlertMutation = UseMutationResult<
|
||||
>;
|
||||
|
||||
export function ManageEngineAlertsSection(props: {
|
||||
teamIdOrSlug: string;
|
||||
alertRules: EngineAlertRule[];
|
||||
alertRulesIsLoading: boolean;
|
||||
engineId: string;
|
||||
}) {
|
||||
const notificationChannelsQuery = useEngineNotificationChannels(
|
||||
props.engineId,
|
||||
props.teamIdOrSlug,
|
||||
);
|
||||
const deleteAlertMutation = useEngineDeleteNotificationChannel(
|
||||
props.engineId,
|
||||
props.teamIdOrSlug,
|
||||
);
|
||||
|
||||
const createAlertMutation = useEngineCreateNotificationChannel(
|
||||
props.engineId,
|
||||
props.teamIdOrSlug,
|
||||
);
|
||||
|
||||
// not passing the mutation to avoid multiple rows sharing the same mutation state, we create the new mutation for each row instead in each component instead
|
||||
|
||||
+7
-1
@@ -23,10 +23,16 @@ export function RecentEngineAlertsSection(props: {
|
||||
alertRules: EngineAlertRule[];
|
||||
alertRulesIsLoading: boolean;
|
||||
engineId: string;
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
// TODO - pagination
|
||||
// required : return the total number of alerts in response from API
|
||||
const alertsQuery = useEngineAlerts(props.engineId, 100, 0);
|
||||
const alertsQuery = useEngineAlerts(
|
||||
props.engineId,
|
||||
props.teamIdOrSlug,
|
||||
100,
|
||||
0,
|
||||
);
|
||||
const alerts = alertsQuery.data ?? [];
|
||||
|
||||
return (
|
||||
|
||||
+3
-1
@@ -9,5 +9,7 @@ export default async function Page(props: EngineInstancePageProps) {
|
||||
teamSlug: params.team_slug,
|
||||
});
|
||||
|
||||
return <EngineAlertsPage instance={instance} />;
|
||||
return (
|
||||
<EngineAlertsPage instance={instance} teamIdOrSlug={params.team_slug} />
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export const EngineConfiguration: React.FC<EngineConfigurationProps> = ({
|
||||
instanceUrl={instance.url}
|
||||
authToken={authToken}
|
||||
/>
|
||||
<EngineSystem instance={instance} />
|
||||
<EngineSystem instance={instance} teamIdOrSlug={teamSlug} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-4
@@ -6,11 +6,15 @@ import {
|
||||
|
||||
interface EngineSystemProps {
|
||||
instance: EngineInstance;
|
||||
teamIdOrSlug: string;
|
||||
}
|
||||
|
||||
export const EngineSystem: React.FC<EngineSystemProps> = ({ instance }) => {
|
||||
export const EngineSystem: React.FC<EngineSystemProps> = ({
|
||||
instance,
|
||||
teamIdOrSlug,
|
||||
}) => {
|
||||
const healthQuery = useEngineSystemHealth(instance.url);
|
||||
const metricsQuery = useEngineSystemMetrics(instance.id);
|
||||
const metricsQuery = useEngineSystemMetrics(instance.id, teamIdOrSlug);
|
||||
if (!healthQuery.data) {
|
||||
return null;
|
||||
}
|
||||
@@ -21,9 +25,9 @@ export const EngineSystem: React.FC<EngineSystemProps> = ({ instance }) => {
|
||||
<br />
|
||||
Enabled: {healthQuery.data.features?.join(", ")}
|
||||
<br />
|
||||
CPU: {metricsQuery.data?.data?.cpu?.toFixed(2) ?? "..."}%
|
||||
CPU: {metricsQuery.data?.result?.cpu?.toFixed(2) ?? "..."}%
|
||||
<br />
|
||||
Memory: {metricsQuery.data?.data?.memory?.toFixed(0) ?? "..."}MB
|
||||
Memory: {metricsQuery.data?.result?.memory?.toFixed(0) ?? "..."}MB
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export default async function Layout(props: {
|
||||
}
|
||||
|
||||
const instance = await getEngineInstance({
|
||||
teamIdOrSlug: params.team_slug,
|
||||
authToken,
|
||||
engineId: params.engineId,
|
||||
accountId: account.id,
|
||||
|
||||
+5
-3
@@ -23,7 +23,7 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
|
||||
teamSlug,
|
||||
authToken,
|
||||
}) => {
|
||||
const systemMetricsQuery = useEngineSystemMetrics(instance.id);
|
||||
const systemMetricsQuery = useEngineSystemMetrics(instance.id, teamSlug);
|
||||
const queueMetricsQuery = useEngineQueueMetrics({
|
||||
authToken,
|
||||
instanceUrl: instance.url,
|
||||
@@ -69,8 +69,10 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
|
||||
<div className="mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Healthcheck instance={instance} />
|
||||
</div>
|
||||
<StatusCodes datapoints={systemMetricsQuery.data.data.statusCodes} />
|
||||
<ErrorRate datapoints={systemMetricsQuery.data.data.errorRate} />
|
||||
<StatusCodes
|
||||
datapoints={systemMetricsQuery.data.result.statusCodes}
|
||||
/>
|
||||
<ErrorRate datapoints={systemMetricsQuery.data.result.errorRate} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
+10
-6
@@ -2,6 +2,7 @@ import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
|
||||
export async function getEngineInstance(params: {
|
||||
teamIdOrSlug: string;
|
||||
authToken: string;
|
||||
engineId: string;
|
||||
accountId: string;
|
||||
@@ -19,11 +20,14 @@ export async function getEngineInstance(params: {
|
||||
return sandboxEngine;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/engine/${params.engineId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${params.teamIdOrSlug}/engine/${params.engineId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorMessage = await res.text();
|
||||
@@ -33,8 +37,8 @@ export async function getEngineInstance(params: {
|
||||
}
|
||||
|
||||
const json = (await res.json()) as {
|
||||
data: EngineInstance;
|
||||
result: EngineInstance;
|
||||
};
|
||||
|
||||
return json.data;
|
||||
return json.result;
|
||||
}
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ export async function engineInstancePageHandler(params: {
|
||||
}
|
||||
|
||||
const instance = await getEngineInstance({
|
||||
teamIdOrSlug: params.teamSlug,
|
||||
authToken,
|
||||
engineId: params.engineId,
|
||||
accountId: account.id,
|
||||
|
||||
+10
-8
@@ -3,12 +3,16 @@ import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
|
||||
export async function getEngineInstances(params: {
|
||||
authToken: string;
|
||||
teamIdOrSlug: string;
|
||||
}) {
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/engine`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${params.teamIdOrSlug}/engine`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorMessage = await res.text();
|
||||
@@ -20,12 +24,10 @@ export async function getEngineInstances(params: {
|
||||
}
|
||||
|
||||
const json = (await res.json()) as {
|
||||
data: {
|
||||
instances: EngineInstance[];
|
||||
};
|
||||
result: EngineInstance[];
|
||||
};
|
||||
|
||||
return {
|
||||
data: json.data.instances,
|
||||
data: json.result,
|
||||
};
|
||||
}
|
||||
|
||||
+28
-17
@@ -66,7 +66,7 @@ async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
|
||||
async function ContractDistributionCard({
|
||||
data,
|
||||
}: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
const _reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
@@ -83,25 +83,36 @@ async function ContractDistributionCard({
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
const [chainId, contractAddress] = key.split(":");
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(chainId));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
const contractData = await fetchDashboardContractMetadata(
|
||||
getContract({
|
||||
chain,
|
||||
address: contractAddress as string, // we filter above
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
).catch(() => undefined);
|
||||
return {
|
||||
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
|
||||
link: `/${chainId}/${contractAddress}`,
|
||||
value,
|
||||
};
|
||||
try {
|
||||
const [chainId, contractAddress] = key.split(":");
|
||||
if (Number(chainId) === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(chainId));
|
||||
const chainMeta = await getChainMetadata(chain).catch(
|
||||
() => undefined,
|
||||
);
|
||||
const contractData = await fetchDashboardContractMetadata(
|
||||
getContract({
|
||||
chain,
|
||||
address: contractAddress as string, // we filter above
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
).catch(() => undefined);
|
||||
return {
|
||||
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
|
||||
link: `/${chainId}/${contractAddress}`,
|
||||
value,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reducedData = _reducedData.filter((d) => !!d);
|
||||
|
||||
const aggregateFn = () =>
|
||||
new Set(
|
||||
data
|
||||
|
||||
+13
-9
@@ -115,19 +115,23 @@ async function AsyncYourFactories(props: {
|
||||
authToken: props.authToken,
|
||||
});
|
||||
|
||||
const client = getThirdwebClient();
|
||||
const client = getThirdwebClient(props.authToken);
|
||||
|
||||
const factories = (
|
||||
await Promise.all(
|
||||
deployedContracts.map(async (c) => {
|
||||
const contract = getContract({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(c.chainId)),
|
||||
address: c.contractAddress,
|
||||
client,
|
||||
});
|
||||
const m = await getCompilerMetadata(contract);
|
||||
return m.name.indexOf("AccountFactory") > -1 ? c : null;
|
||||
try {
|
||||
const contract = getContract({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(c.chainId)),
|
||||
address: c.contractAddress,
|
||||
client,
|
||||
});
|
||||
const m = await getCompilerMetadata(contract);
|
||||
return m.name.indexOf("AccountFactory") > -1 ? c : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((f) => f !== null);
|
||||
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 { differenceInDays } from "date-fns";
|
||||
import { ArrowRightIcon, CableIcon, WalletCardsIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import type {
|
||||
InAppWalletStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
} from "types/analytics";
|
||||
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";
|
||||
|
||||
export function ConnectAnalyticsDashboardUI(props: {
|
||||
walletUsage: WalletStats[];
|
||||
aggregateWalletUsage: WalletStats[];
|
||||
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(
|
||||
(acc, curr) => {
|
||||
acc.totalWallets += curr.totalConnections;
|
||||
acc.uniqueWallets += curr.uniqueWalletsConnected;
|
||||
return acc;
|
||||
},
|
||||
{ uniqueWallets: 0, totalWallets: 0 },
|
||||
);
|
||||
}, [props.aggregateWalletUsage]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 lg:gap-6">
|
||||
<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-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">
|
||||
In-App Wallets
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
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`}>
|
||||
Account Abstraction Metrics
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AccountAbstractionSummary
|
||||
aggregateUserOpUsageQuery={props.aggregateUserOpUsageQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4">
|
||||
<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
|
||||
walletStats={props.walletUsage}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={props.walletUsage}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
|
||||
<WalletDistributionChartCard
|
||||
walletStats={props.walletUsage}
|
||||
isPending={props.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
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";
|
||||
|
||||
const meta = {
|
||||
title: "Charts/Connect/Analytics Dashboard",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
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]}
|
||||
connectLayoutSlug="connectLayoutSlug"
|
||||
isPending={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
} from "../../../../../../../stories/utils";
|
||||
import { DailyConnectionsChartCard } from "./DailyConnectionsChartCard";
|
||||
import { createWalletStatsStub } from "./storyUtils";
|
||||
|
||||
const meta = {
|
||||
title: "Charts/Connect/Daily Connections",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[1150px] flex-col gap-6 py-8">
|
||||
<BadgeContainer label="30 days">
|
||||
<DailyConnectionsChartCard
|
||||
walletStats={createWalletStatsStub(30)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="60 days">
|
||||
<DailyConnectionsChartCard
|
||||
walletStats={createWalletStatsStub(60)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="10 days">
|
||||
<DailyConnectionsChartCard
|
||||
walletStats={createWalletStatsStub(10)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="0 days">
|
||||
<DailyConnectionsChartCard
|
||||
walletStats={createWalletStatsStub(0)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading">
|
||||
<DailyConnectionsChartCard
|
||||
walletStats={createWalletStatsStub(0)}
|
||||
isPending={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-243
@@ -1,243 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon";
|
||||
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, useState } from "react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
LabelList,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import type { WalletStats } from "types/analytics";
|
||||
import { formatTickerNumber } from "../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartToShow = "uniqueWallets" | "totalWallets";
|
||||
|
||||
type ChartData = {
|
||||
time: string; // human readable date
|
||||
totalWallets: number;
|
||||
uniqueWallets: number;
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
uniqueWallets: {
|
||||
label: "Unique Wallets",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
totalWallets: {
|
||||
label: "Total Wallets",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const chartLabelToShow: Record<ChartToShow, string> = {
|
||||
uniqueWallets: "Unique Wallets",
|
||||
totalWallets: "Total Wallets",
|
||||
};
|
||||
|
||||
export function DailyConnectionsChartCard(props: {
|
||||
walletStats: WalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
const { walletStats } = props;
|
||||
|
||||
const [chartToShow, setChartToShow] = useState<ChartToShow>("uniqueWallets");
|
||||
const chartToShowOptions: ChartToShow[] = ["uniqueWallets", "totalWallets"];
|
||||
|
||||
const barChartData: ChartData[] = useMemo(() => {
|
||||
const chartDataMap: Map<string, ChartData> = new Map();
|
||||
|
||||
for (const data of walletStats) {
|
||||
const chartData = chartDataMap.get(data.date);
|
||||
if (!chartData) {
|
||||
chartDataMap.set(data.date, {
|
||||
time: format(new Date(data.date), "MMM dd"),
|
||||
totalWallets: data.totalConnections,
|
||||
uniqueWallets: data.uniqueWalletsConnected,
|
||||
});
|
||||
} else {
|
||||
chartData.totalWallets += data.totalConnections;
|
||||
chartData.uniqueWallets += data.uniqueWalletsConnected;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(chartDataMap.values());
|
||||
}, [walletStats]);
|
||||
|
||||
const disableActions = props.isPending || barChartData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Daily Connections
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
Total and unique wallets addresses that connected to your app each day.
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
{/* Selector */}
|
||||
<Select
|
||||
onValueChange={(v) => {
|
||||
setChartToShow(v as ChartToShow);
|
||||
}}
|
||||
value={chartToShow}
|
||||
disabled={disableActions}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{chartToShowOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{chartLabelToShow[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<ExportToCSVButton
|
||||
disabled={disableActions}
|
||||
className="bg-background"
|
||||
getData={async () => {
|
||||
const header = ["Date", "Total Wallets", "Unique Wallets"];
|
||||
const rows = barChartData.map((row) => [
|
||||
row.time,
|
||||
row.totalWallets.toString(),
|
||||
row.uniqueWallets.toString(),
|
||||
]);
|
||||
return { header, rows };
|
||||
}}
|
||||
fileName="DailyConnections"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[250px] w-full md:h-[350px]"
|
||||
>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : barChartData.length === 0 ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="mb-6 text-lg">
|
||||
Send your first connect event
|
||||
</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/getting-started"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react-native/v5"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/dotnet/getting-started"
|
||||
label="Unity"
|
||||
icon={UnityIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/unreal-engine/getting-started"
|
||||
label="Unreal Engine"
|
||||
icon={UnrealIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/dotnet/getting-started"
|
||||
label=".NET"
|
||||
icon={DotNetIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EmptyChartState>
|
||||
) : (
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={barChartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<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}
|
||||
fill={`var(--color-${chartToShow})`}
|
||||
radius={8}
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
>
|
||||
{barChartData.length < 50 && (
|
||||
<LabelList
|
||||
position="top"
|
||||
offset={12}
|
||||
className="invisible fill-foreground sm:visible"
|
||||
fontSize={12}
|
||||
formatter={(value: number) => formatTickerNumber(value)}
|
||||
/>
|
||||
)}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
} from "../../../../../../../stories/utils";
|
||||
import { WalletConnectorsChartCard } from "./WalletConnectorsChartCard";
|
||||
import { createWalletStatsStub } from "./storyUtils";
|
||||
|
||||
const meta = {
|
||||
title: "Charts/Connect/Wallet Connectors",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="30 days">
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={createWalletStatsStub(30)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="60 days">
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={createWalletStatsStub(60)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="10 days">
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={createWalletStatsStub(10)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="0 days">
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={createWalletStatsStub(0)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading">
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={createWalletStatsStub(0)}
|
||||
isPending={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-260
@@ -1,260 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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 { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import type { WalletStats } from "types/analytics";
|
||||
import {
|
||||
formatTickerNumber,
|
||||
formatWalletType,
|
||||
} from "../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartToShow = "uniqueWalletsConnected" | "totalConnections";
|
||||
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
};
|
||||
|
||||
const chartLabelToShow: Record<ChartToShow, string> = {
|
||||
uniqueWalletsConnected: "Unique Wallets",
|
||||
totalConnections: "Total Wallets",
|
||||
};
|
||||
export function WalletConnectorsChartCard(props: {
|
||||
walletStats: WalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
// show top 10 wallets as distinct, and combine the rest as "Others"
|
||||
const topWalletsToShow = 10;
|
||||
const { walletStats } = props;
|
||||
const [chartToShow, setChartToShow] = useState<ChartToShow>(
|
||||
"uniqueWalletsConnected",
|
||||
);
|
||||
const chartToShowOptions: ChartToShow[] = [
|
||||
"uniqueWalletsConnected",
|
||||
"totalConnections",
|
||||
];
|
||||
|
||||
const { chartConfig, chartData } = useMemo(() => {
|
||||
const _chartConfig: ChartConfig = {};
|
||||
const _chartDataMap: Map<string, ChartData> = new Map();
|
||||
const walletTypeToValueMap: Map<string, number> = new Map();
|
||||
// for each stat, add it in _chartDataMap
|
||||
for (const stat of walletStats) {
|
||||
const chartData = _chartDataMap.get(stat.date);
|
||||
const { walletType: rawWalletType } = stat;
|
||||
const walletType = formatWalletType(rawWalletType);
|
||||
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[walletType]: stat[chartToShow],
|
||||
} as ChartData);
|
||||
} else {
|
||||
chartData[walletType] =
|
||||
(chartData[walletType] || 0) + stat[chartToShow];
|
||||
}
|
||||
|
||||
walletTypeToValueMap.set(
|
||||
walletType,
|
||||
stat[chartToShow] + (walletTypeToValueMap.get(walletType) || 0),
|
||||
);
|
||||
}
|
||||
|
||||
const walletTypesSorted = Array.from(walletTypeToValueMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const walletTypesToShow = walletTypesSorted.slice(0, topWalletsToShow);
|
||||
const walletTypesToTagAsOthers = walletTypesSorted.slice(topWalletsToShow);
|
||||
|
||||
// replace walletTypesToTagAsOthers walletType with "other"
|
||||
for (const data of _chartDataMap.values()) {
|
||||
for (const walletType in data) {
|
||||
if (walletTypesToTagAsOthers.includes(walletType)) {
|
||||
data.others = (data.others || 0) + (data[walletType] || 0);
|
||||
delete data[walletType];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walletTypesToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: walletTypesToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
});
|
||||
|
||||
// Add Other
|
||||
walletTypesToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
|
||||
return {
|
||||
chartData: Array.from(_chartDataMap.values()),
|
||||
chartConfig: _chartConfig,
|
||||
};
|
||||
}, [walletStats, chartToShow]);
|
||||
|
||||
const uniqueWalletTypes = Object.keys(chartConfig);
|
||||
const disableActions = props.isPending || chartData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Connected Wallets
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
The different types of wallets used to connect to your app each day.
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<Select
|
||||
onValueChange={(v) => {
|
||||
setChartToShow(v as ChartToShow);
|
||||
}}
|
||||
value={chartToShow}
|
||||
disabled={disableActions}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{chartToShowOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{chartLabelToShow[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<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", ...uniqueWalletTypes];
|
||||
const rows = chartData.map((data) => {
|
||||
const { time, ...rest } = data;
|
||||
return [
|
||||
time,
|
||||
...uniqueWalletTypes.map((w) => (rest[w] || 0).toString()),
|
||||
];
|
||||
});
|
||||
return { header, rows };
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ChartContainer config={chartConfig} className="h-[400px] w-full">
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : chartData.length === 0 ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="mb-6 text-lg">
|
||||
Connect any wallet to your app
|
||||
</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/supported-wallets"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
</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 />} />
|
||||
{uniqueWalletTypes.map((walletType) => {
|
||||
return (
|
||||
<Bar
|
||||
key={walletType}
|
||||
dataKey={walletType}
|
||||
fill={chartConfig[walletType]?.color}
|
||||
radius={4}
|
||||
stackId="a"
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
} from "../../../../../../../stories/utils";
|
||||
import { WalletDistributionChartCard } from "./WalletDistributionChartCard";
|
||||
import { createWalletStatsStub } from "./storyUtils";
|
||||
|
||||
const meta = {
|
||||
title: "Charts/Connect/Wallet Distribution",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="30 days">
|
||||
<WalletDistributionChartCard
|
||||
walletStats={createWalletStatsStub(30)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="60 days">
|
||||
<WalletDistributionChartCard
|
||||
walletStats={createWalletStatsStub(60)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="10 days">
|
||||
<WalletDistributionChartCard
|
||||
walletStats={createWalletStatsStub(10)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="0 days">
|
||||
<WalletDistributionChartCard
|
||||
walletStats={createWalletStatsStub(0)}
|
||||
isPending={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading">
|
||||
<WalletDistributionChartCard
|
||||
walletStats={createWalletStatsStub(0)}
|
||||
isPending={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-263
@@ -1,263 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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 { DocLink } from "components/shared/DocLink";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Pie, PieChart } from "recharts";
|
||||
import type { WalletStats } from "types/analytics";
|
||||
import {
|
||||
formatTickerNumber,
|
||||
formatWalletType,
|
||||
} from "../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartToShow = "totalConnections" | "uniqueWalletsConnected";
|
||||
|
||||
type ChartData = {
|
||||
walletType: string;
|
||||
value: number;
|
||||
fill: string;
|
||||
};
|
||||
|
||||
const chartLabelToShow: Record<ChartToShow, string> = {
|
||||
uniqueWalletsConnected: "Unique Wallets",
|
||||
totalConnections: "Total Wallets",
|
||||
};
|
||||
|
||||
export function WalletDistributionChartCard(props: {
|
||||
walletStats: WalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
// show these many top wallets as distinct, and combine the rest as "Others"
|
||||
const topWalletsToShow = 10;
|
||||
const { walletStats } = props;
|
||||
const [chartToShow, setChartToShow] =
|
||||
useState<ChartToShow>("totalConnections");
|
||||
const chartToShowOptions: ChartToShow[] = [
|
||||
"totalConnections",
|
||||
"uniqueWalletsConnected",
|
||||
];
|
||||
|
||||
const { chartConfig, chartData, totalConnections, uniqueConnections } =
|
||||
useMemo(() => {
|
||||
const _chartDataMap: Map<string, number> = new Map();
|
||||
const walletTypeToValueMap: Map<string, number> = new Map();
|
||||
|
||||
let _totalConnections = 0;
|
||||
let _uniqueConnections = 0;
|
||||
|
||||
for (const stat of walletStats) {
|
||||
const { walletType: rawWalletType } = stat;
|
||||
const walletType = formatWalletType(rawWalletType);
|
||||
const chartData = _chartDataMap.get(walletType);
|
||||
|
||||
_totalConnections += stat.totalConnections;
|
||||
_uniqueConnections += stat.uniqueWalletsConnected;
|
||||
_chartDataMap.set(walletType, (chartData || 0) + stat[chartToShow]);
|
||||
walletTypeToValueMap.set(
|
||||
walletType,
|
||||
(walletTypeToValueMap.get(walletType) || 0) + stat[chartToShow],
|
||||
);
|
||||
}
|
||||
|
||||
const walletTypesSortedByValue = Array.from(
|
||||
walletTypeToValueMap.entries(),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const walletTypesToShow = walletTypesSortedByValue.slice(
|
||||
0,
|
||||
topWalletsToShow,
|
||||
);
|
||||
|
||||
const walletTypesToTagAsOthers =
|
||||
walletTypesSortedByValue.slice(topWalletsToShow);
|
||||
|
||||
for (const walletType of walletTypesToTagAsOthers) {
|
||||
const val = _chartDataMap.get(walletType);
|
||||
if (val) {
|
||||
const othersVal = _chartDataMap.get("others");
|
||||
_chartDataMap.set("others", othersVal ? othersVal + val : val);
|
||||
}
|
||||
|
||||
_chartDataMap.delete(walletType);
|
||||
}
|
||||
|
||||
const _chartConfig: ChartConfig = {};
|
||||
walletTypesToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: walletTypesToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
});
|
||||
|
||||
// Add Others
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
|
||||
const _chartData: ChartData[] = Array.from(_chartDataMap).map(
|
||||
([walletType, data]) => {
|
||||
return {
|
||||
walletType,
|
||||
value: data,
|
||||
fill: _chartConfig[walletType]?.color || "transparent",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// sort the data
|
||||
_chartData.sort((a, b) => b.value - a.value);
|
||||
|
||||
return {
|
||||
chartData: _chartData,
|
||||
chartConfig: _chartConfig,
|
||||
totalConnections: _totalConnections,
|
||||
uniqueConnections: _uniqueConnections,
|
||||
};
|
||||
}, [walletStats, chartToShow]);
|
||||
|
||||
const disableActions = props.isPending || chartData.length === 0;
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Wallet Distribution
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
Distribution of wallet types used to connect to your app.
|
||||
</p>
|
||||
|
||||
{/* Selector */}
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<Select
|
||||
onValueChange={(v) => {
|
||||
setChartToShow(v as ChartToShow);
|
||||
}}
|
||||
value={chartToShow}
|
||||
disabled={props.isPending || chartData.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{chartToShowOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{chartLabelToShow[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<ExportToCSVButton
|
||||
fileName="Wallet Distribution"
|
||||
className="bg-background"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
const header = [
|
||||
"Wallet",
|
||||
`${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
|
||||
`Percentage of ${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
|
||||
];
|
||||
const rows = chartData.map((d) => {
|
||||
return [
|
||||
d.walletType,
|
||||
d.value.toString(),
|
||||
`${((d.value / (chartToShow === "totalConnections" ? totalConnections : uniqueConnections)) * 100).toFixed(2)}%`,
|
||||
];
|
||||
});
|
||||
return {
|
||||
header,
|
||||
rows,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mt-6 h-[500px] w-full [&_.recharts-pie-label-text]:fill-foreground"
|
||||
>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : chartData.length === 0 ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="mb-6 text-lg">
|
||||
Connect any wallet to your app
|
||||
</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/supported-wallets"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EmptyChartState>
|
||||
) : (
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(v) => {
|
||||
if (typeof v === "number") {
|
||||
const sumValue =
|
||||
chartToShow === "uniqueWalletsConnected"
|
||||
? uniqueConnections
|
||||
: totalConnections;
|
||||
const percentageValue = ((v / sumValue) * 100).toFixed(2);
|
||||
return `${percentageValue}% - ${formatTickerNumber(v)}`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} className="" />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="value"
|
||||
nameKey="walletType"
|
||||
innerRadius={60}
|
||||
strokeWidth={2}
|
||||
stroke="hsl(var(--muted))"
|
||||
/>
|
||||
</PieChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import type { WalletId } from "thirdweb/wallets";
|
||||
import type { WalletStats } from "types/analytics";
|
||||
|
||||
const walletsToPickFrom: WalletId[] = [
|
||||
"io.metamask",
|
||||
"com.binance",
|
||||
"com.coinbase.wallet",
|
||||
"io.zerion.wallet",
|
||||
"me.rainbow",
|
||||
"com.trustwallet.app",
|
||||
"com.zengo",
|
||||
"xyz.argent",
|
||||
"com.exodus",
|
||||
"app.phantom",
|
||||
"com.okex.wallet",
|
||||
"io.clingon",
|
||||
"com.broearn",
|
||||
"com.coinomi",
|
||||
"com.ripio",
|
||||
"com.sabay.wallet",
|
||||
"io.tokoin",
|
||||
"world.fncy",
|
||||
"io.copiosa",
|
||||
];
|
||||
|
||||
const pickRandomWallet = () => {
|
||||
return (
|
||||
walletsToPickFrom[Math.floor(Math.random() * walletsToPickFrom.length)] ||
|
||||
"io.metamask"
|
||||
);
|
||||
};
|
||||
|
||||
export function createWalletStatsStub(days: number): WalletStats[] {
|
||||
const stubbedData: WalletStats[] = [];
|
||||
|
||||
let d = days;
|
||||
while (d !== 0) {
|
||||
const uniqueWallets = Math.floor(Math.random() * 100);
|
||||
stubbedData.push({
|
||||
date: new Date(2024, 1, d).toLocaleString(),
|
||||
totalConnections: uniqueWallets + Math.floor(Math.random() * 100),
|
||||
uniqueWalletsConnected: uniqueWallets,
|
||||
walletType: pickRandomWallet(),
|
||||
});
|
||||
|
||||
if (Math.random() > 0.7) {
|
||||
d--;
|
||||
}
|
||||
}
|
||||
|
||||
return stubbedData;
|
||||
}
|
||||
+2
-3
@@ -76,9 +76,8 @@ function Story(props: {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
data: {
|
||||
secret: new Array(86).fill("x").join(""),
|
||||
secretHash: new Array(64).fill("x").join(""),
|
||||
secretMasked: "123...4567",
|
||||
secret: `sk_${new Array(86).fill("x").join("")}`,
|
||||
secretMasked: "sk_123...4567",
|
||||
},
|
||||
};
|
||||
}}
|
||||
|
||||
+20
-4
@@ -14,7 +14,12 @@ import { Flex, FormControl } from "@chakra-ui/react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage";
|
||||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
|
||||
import { DEFAULT_FEE_BPS, DEFAULT_FEE_RECIPIENT } from "constants/addresses";
|
||||
import {
|
||||
DEFAULT_FEE_BPS,
|
||||
DEFAULT_FEE_BPS_NEW,
|
||||
DEFAULT_FEE_RECIPIENT,
|
||||
THIRDWEB_PUBLISHER_ADDRESS,
|
||||
} from "constants/addresses";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
@@ -161,6 +166,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
const constructorParams =
|
||||
metadata.abi.find((a) => a.type === "constructor")?.inputs || [];
|
||||
|
||||
const defaultFeeRecipientFunction = metadata.abi.find(
|
||||
(a) => a.type === "function" && a.name === "DEFAULT_FEE_RECIPIENT",
|
||||
);
|
||||
const hasInbuiltDefaultFeeConfig =
|
||||
defaultFeeRecipientFunction &&
|
||||
metadata.publisher === THIRDWEB_PUBLISHER_ADDRESS;
|
||||
|
||||
const [customFactoryNetwork, customFactoryAddress] = Object.entries(
|
||||
metadata?.factoryDeploymentData?.customFactoryInput
|
||||
?.customFactoryAddresses || {},
|
||||
@@ -457,7 +469,9 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
name: params.contractMetadata?.name || "",
|
||||
contractURI: _contractURI,
|
||||
defaultAdmin: params.deployParams._defaultAdmin as string,
|
||||
platformFeeBps: DEFAULT_FEE_BPS,
|
||||
platformFeeBps: hasInbuiltDefaultFeeConfig
|
||||
? DEFAULT_FEE_BPS_NEW
|
||||
: DEFAULT_FEE_BPS,
|
||||
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
|
||||
trustedForwarders: params.deployParams._trustedForwarders
|
||||
? JSON.parse(params.deployParams._trustedForwarders as string)
|
||||
@@ -473,8 +487,10 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
payees,
|
||||
shares,
|
||||
_contractURI,
|
||||
_platformFeeBps: DEFAULT_FEE_BPS,
|
||||
_platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
|
||||
platformFeeBps: hasInbuiltDefaultFeeConfig
|
||||
? DEFAULT_FEE_BPS_NEW
|
||||
: DEFAULT_FEE_BPS,
|
||||
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
|
||||
};
|
||||
|
||||
const salt = params.deployDeterministic
|
||||
|
||||
+4
-4
@@ -5,13 +5,13 @@ interface PlatformFeeFieldsetProps {
|
||||
isMarketplace: boolean;
|
||||
}
|
||||
|
||||
export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = (
|
||||
props,
|
||||
) => {
|
||||
export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = ({
|
||||
isMarketplace,
|
||||
}) => {
|
||||
return (
|
||||
<Fieldset legend="Platform fees">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
{props.isMarketplace ? (
|
||||
{isMarketplace ? (
|
||||
<p className="mb-3 pt-4 text-muted-foreground text-sm italic">
|
||||
A 2.5% platform fee is deducted from each sale to support ongoing
|
||||
platform operations and improvements.{" "}
|
||||
|
||||
@@ -285,6 +285,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
twAccount,
|
||||
}) => {
|
||||
// TODO: clean this up
|
||||
|
||||
const functionsWithExtension = useMemo(() => {
|
||||
const allFunctions = fnsOrEvents.filter((f) => f.type === "function");
|
||||
const results: ExtensionFunctions[] = [];
|
||||
|
||||
@@ -16,7 +16,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import { TagIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -24,17 +23,6 @@ import { toast } from "sonner";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
|
||||
const LazyShareFreeWalletsModal = dynamic(
|
||||
() =>
|
||||
import("./share-free-wallets-modal.client").then((mod) => ({
|
||||
default: mod.ShareFreeWalletsModal,
|
||||
})),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export type ActiveCouponResponse = {
|
||||
id: string;
|
||||
start: number;
|
||||
@@ -49,10 +37,7 @@ export type ActiveCouponResponse = {
|
||||
|
||||
function ApplyCouponCard(props: {
|
||||
teamId: string | undefined;
|
||||
onCouponApplied: (
|
||||
data: ActiveCouponResponse,
|
||||
isFreeWalletsCoupon: boolean,
|
||||
) => void;
|
||||
onCouponApplied: (data: ActiveCouponResponse) => void;
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -102,9 +87,7 @@ export function ApplyCouponCardUI(props: {
|
||||
status: number;
|
||||
data: null | ActiveCouponResponse;
|
||||
}>;
|
||||
onCouponApplied:
|
||||
| ((data: ActiveCouponResponse, isFreeWalletsCoupon: boolean) => void)
|
||||
| undefined;
|
||||
onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined;
|
||||
prefillPromoCode?: string;
|
||||
scrollIntoView?: boolean;
|
||||
isPaymentSetup: boolean;
|
||||
@@ -145,14 +128,7 @@ export function ApplyCouponCardUI(props: {
|
||||
case 200: {
|
||||
toast.success("Coupon applied successfully");
|
||||
if (res.data) {
|
||||
props.onCouponApplied?.(
|
||||
res.data,
|
||||
// prod & dev
|
||||
values.promoCode === "FREEWALLETS" ||
|
||||
// new prod code
|
||||
values.promoCode === "FREEWALLETS24" ||
|
||||
values.promoCode === "TESTFREEWALLETS",
|
||||
);
|
||||
props.onCouponApplied?.(res.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -284,7 +260,6 @@ export function CouponSection(props: {
|
||||
isPaymentSetup: boolean;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const [optimisticCouponData, setOptimisticCouponData] = useState<
|
||||
| {
|
||||
type: "added";
|
||||
@@ -358,15 +333,12 @@ export function CouponSection(props: {
|
||||
<Suspense fallback={<LoadingCouponSection />}>
|
||||
<ApplyCouponCard
|
||||
teamId={props.teamId}
|
||||
onCouponApplied={(coupon, isFreeWalletsCoupon) => {
|
||||
onCouponApplied={(coupon) => {
|
||||
setOptimisticCouponData({
|
||||
type: "added",
|
||||
data: coupon,
|
||||
});
|
||||
|
||||
if (isFreeWalletsCoupon) {
|
||||
setShowShareModal(true);
|
||||
}
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
@@ -375,13 +347,6 @@ export function CouponSection(props: {
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{showShareModal && (
|
||||
<LazyShareFreeWalletsModal
|
||||
isOpen={showShareModal}
|
||||
onOpenChange={setShowShareModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
-401
@@ -1,401 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ChevronLeftIcon, MailIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { DynamicHeight } from "../../../../@/components/ui/DynamicHeight";
|
||||
import { XIcon } from "../../../icons/brand-icons/XIcon";
|
||||
|
||||
export function ShareFreeWalletsModal(props: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<"base" | "email">("base");
|
||||
|
||||
return (
|
||||
<Dialog open={props.isOpen} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className="p-0 md:max-w-[380px]">
|
||||
<DynamicHeight>
|
||||
{screen === "base" && (
|
||||
<div className="fade-in-0 animate-in duration-300">
|
||||
<DialogHeader className="p-6 text-center">
|
||||
<div className="mb-4 flex justify-center gap-2">
|
||||
<VibrantSmileIcon className="size-16" />
|
||||
</div>
|
||||
<DialogTitle className="mb-1 text-lg">
|
||||
Congratulations!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-balance text-base text-muted-foreground">
|
||||
You are getting free wallets for the next 12 months
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 border-border border-t p-6">
|
||||
<Button variant="outline" className="gap-2" asChild>
|
||||
<TrackedLinkTW
|
||||
href={`https://x.com/intent/post?text=${encodeURIComponent(twitterPostBody)}`}
|
||||
target="_blank"
|
||||
category="share-free-wallets"
|
||||
label="twitter-share"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
Share on X
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="gap-2"
|
||||
onClick={() => setScreen("email")}
|
||||
>
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
Share via Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{screen === "email" && (
|
||||
<SendEmailScreen goBack={() => setScreen("base")} />
|
||||
)}
|
||||
</DynamicHeight>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const emailFormSchema = z.object({
|
||||
email: z.string().email(),
|
||||
app: z.string(),
|
||||
});
|
||||
|
||||
function SendEmailScreen(props: {
|
||||
goBack: () => void;
|
||||
}) {
|
||||
const form = useForm<z.infer<typeof emailFormSchema>>({
|
||||
resolver: zodResolver(emailFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
app: "default",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fade-in-0 animate-in duration-300">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(() => {})}>
|
||||
<div className="p-6">
|
||||
<DialogHeader className="text-center">
|
||||
<div className="mb-4 flex justify-center gap-2">
|
||||
<VibrantEmailIcon className="size-16" />
|
||||
</div>
|
||||
<DialogTitle className="mb-1 text-lg">
|
||||
Share via Email
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-balance text-base text-muted-foreground">
|
||||
Give the gift of unlimited wallets to another dev
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="[email protected]"
|
||||
{...field}
|
||||
type="email"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="app"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Share with</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => form.setValue("app", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Email App" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="default">
|
||||
Default Email App
|
||||
</SelectItem>
|
||||
<SelectItem value="gmail">Gmail</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3 border-border border-t p-6">
|
||||
<Button variant="outline" className="gap-2" onClick={props.goBack}>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
{!form.formState.isValid ? (
|
||||
<Button variant="primary" className="gap-2" type="submit">
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
Share via Email
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" className="gap-2" asChild>
|
||||
<TrackedLinkTW
|
||||
href={
|
||||
form.watch("app") === "gmail"
|
||||
? openGmailEmailApp(form.watch("email"))
|
||||
: openDefaultEmailApp(form.watch("email"))
|
||||
}
|
||||
target="_blank"
|
||||
category="share-free-wallets"
|
||||
label="email-share"
|
||||
>
|
||||
<MailIcon className="size-4 shrink-0" />
|
||||
Share via Email
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function openDefaultEmailApp(email: string) {
|
||||
return `mailto:${email}?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
|
||||
}
|
||||
|
||||
function openGmailEmailApp(email: string) {
|
||||
return `https://mail.google.com/mail/?view=cm&fs=1&to=${email}&su=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
|
||||
}
|
||||
|
||||
const twitterPostBody = `\
|
||||
Just claimed the free, unlimited wallets offer from @thirdweb! 🚀
|
||||
|
||||
No more per-wallet pricing, full support for web, mobile, and console.
|
||||
|
||||
Here's to building without constraints ✨
|
||||
|
||||
👉 https://thirdweb.com/unlimited-wallets 👈`;
|
||||
|
||||
const emailSubject = "You've unlocked Unlimited Wallets!";
|
||||
|
||||
const emailPostBody = `\
|
||||
Hey there,
|
||||
|
||||
I just came across something that might interest you. thirdweb is offering free, unlimited wallets for developers.
|
||||
I've claimed it and thought you might want to take a look too.
|
||||
|
||||
Here's the lowdown:
|
||||
* It's completely free
|
||||
* No more per-wallet pricing constraints
|
||||
* Full support for web, mobile, and console development
|
||||
|
||||
I'm pretty excited about the possibilities this opens up for building without limitations. You can check it out here: https://thirdweb.com/unlimited-wallets
|
||||
|
||||
Let me know what you think if you decide to give it a try!`;
|
||||
|
||||
function VibrantSmileIcon(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
fill="url(#paint0_linear_754_5630)"
|
||||
fillOpacity="0.08"
|
||||
/>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
stroke="url(#paint1_linear_754_5630)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M26.2857 34.8567C26.2857 34.8567 28.4286 37.7139 32 37.7139C35.5714 37.7139 37.7143 34.8567 37.7143 34.8567M36.2857 27.7139H36.3M27.7143 27.7139H27.7286M46.2857 31.9996C46.2857 39.8894 39.8898 46.2853 32 46.2853C24.1102 46.2853 17.7143 39.8894 17.7143 31.9996C17.7143 24.1098 24.1102 17.7139 32 17.7139C39.8898 17.7139 46.2857 24.1098 46.2857 31.9996ZM37 27.7139C37 28.1084 36.6802 28.4282 36.2857 28.4282C35.8912 28.4282 35.5714 28.1084 35.5714 27.7139C35.5714 27.3194 35.8912 26.9996 36.2857 26.9996C36.6802 26.9996 37 27.3194 37 27.7139ZM28.4286 27.7139C28.4286 28.1084 28.1088 28.4282 27.7143 28.4282C27.3198 28.4282 27 28.1084 27 27.7139C27 27.3194 27.3198 26.9996 27.7143 26.9996C28.1088 26.9996 28.4286 27.3194 28.4286 27.7139Z"
|
||||
stroke="url(#paint2_linear_754_5630)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_754_5630"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_754_5630"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_754_5630"
|
||||
x1="15.7735"
|
||||
y1="26.8818"
|
||||
x2="44.9647"
|
||||
y2="47.5678"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function VibrantEmailIcon(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
fill="url(#paint0_linear_754_9605)"
|
||||
fillOpacity="0.08"
|
||||
/>
|
||||
<path
|
||||
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
|
||||
stroke="url(#paint1_linear_754_9605)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M17.7144 24.857L29.3785 33.0219C30.3231 33.6831 30.7953 34.0137 31.309 34.1417C31.7628 34.2548 32.2374 34.2548 32.6911 34.1417C33.2048 34.0137 33.6771 33.6831 34.6216 33.0219L46.2858 24.857M24.5715 43.4284H39.4286C41.8289 43.4284 43.029 43.4284 43.9457 42.9613C44.7522 42.5504 45.4078 41.8948 45.8187 41.0884C46.2858 40.1716 46.2858 38.9715 46.2858 36.5713V27.4284C46.2858 25.0282 46.2858 23.8281 45.8187 22.9113C45.4078 22.1049 44.7522 21.4493 43.9457 21.0384C43.029 20.5713 41.8289 20.5713 39.4286 20.5713H24.5715C22.1713 20.5713 20.9712 20.5713 20.0544 21.0384C19.248 21.4493 18.5924 22.1049 18.1815 22.9113C17.7144 23.8281 17.7144 25.0282 17.7144 27.4284V36.5713C17.7144 38.9715 17.7144 40.1716 18.1815 41.0884C18.5924 41.8948 19.248 42.5504 20.0544 42.9613C20.9712 43.4284 22.1713 43.4284 24.5715 43.4284Z"
|
||||
stroke="url(#paint2_linear_754_9605)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_754_9605"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_754_9605"
|
||||
x1="-4.34726"
|
||||
y1="20.5361"
|
||||
x2="61.0409"
|
||||
y2="66.8729"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_754_9605"
|
||||
x1="15.7736"
|
||||
y1="27.9056"
|
||||
x2="40.3444"
|
||||
y2="49.6705"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { mobileViewport } from "../../../../stories/utils";
|
||||
import { ShareFreeWalletsModal } from "./share-free-wallets-modal.client";
|
||||
|
||||
const meta = {
|
||||
title: "Billing/Coupons/ShareFreeWalletsModal",
|
||||
component: Variants,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Variants>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Variants() {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
return (
|
||||
<div className="flex grow items-center justify-center p-6">
|
||||
<Button onClick={() => setIsOpen(true)}>Open Again</Button>
|
||||
<ShareFreeWalletsModal isOpen={isOpen} onOpenChange={setIsOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,11 +44,10 @@ export const SmartWalletsBillingAlert = ({
|
||||
<AlertTitle>Account Abstraction on Mainnet</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Text as="span" pr={1}>
|
||||
You've enabled Account Abstraction for one of your API keys
|
||||
and haven't added a payment method.
|
||||
You've enabled Account Abstraction for one of your API keys.
|
||||
<br />
|
||||
To use them on Mainnet,
|
||||
</Text>
|
||||
To enable AA on mainnet chains,
|
||||
</Text>{" "}
|
||||
<TrackedLink
|
||||
href="/team/~/~/settings/billing"
|
||||
category="api_keys"
|
||||
@@ -57,7 +56,7 @@ export const SmartWalletsBillingAlert = ({
|
||||
color="blue.500"
|
||||
>
|
||||
<Text as="span" color="blue.500">
|
||||
add a payment method.
|
||||
subscribe to a billing plan.
|
||||
</Text>
|
||||
</TrackedLink>
|
||||
</AlertDescription>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export const THIRDWEB_DEPLOYER_ADDRESS =
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
export const THIRDWEB_PUBLISHER_ADDRESS =
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
export const DEFAULT_FEE_RECIPIENT =
|
||||
"0x1af20c6b23373350ad464700b5965ce4b0d2ad94";
|
||||
|
||||
export const DEFAULT_FEE_BPS = 250;
|
||||
export const DEFAULT_FEE_BPS_NEW = 150;
|
||||
|
||||
@@ -5,14 +5,3 @@ const compactNumberFormatter = new Intl.NumberFormat("en-US", {
|
||||
export const formatTickerNumber = (value: number) => {
|
||||
return compactNumberFormatter.format(value);
|
||||
};
|
||||
|
||||
export const formatWalletType = (walletType: string) => {
|
||||
return walletType.toLowerCase().includes("inapp") ||
|
||||
walletType.toLowerCase().includes("embedded")
|
||||
? "in-app"
|
||||
: walletType.toLowerCase().includes("metamask")
|
||||
? "metamask"
|
||||
: walletType.toLowerCase().includes("coinbase")
|
||||
? "coinbase"
|
||||
: walletType.toLowerCase();
|
||||
};
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"@radix-ui/react-tooltip": "1.1.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jose": "6.0.7",
|
||||
"lucide-react": "0.476.0",
|
||||
"jose": "6.0.8",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "15.2.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "19.0.0",
|
||||
@@ -37,13 +37,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.0",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"eslint": "^9",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-next": "15.2.0",
|
||||
"postcss": "8.5.3",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "1.1.8",
|
||||
"@tanstack/react-query": "5.66.9",
|
||||
"@tanstack/react-query": "5.67.1",
|
||||
"@thirdweb-dev/engine": "^0.0.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jose": "6.0.7",
|
||||
"lucide-react": "0.476.0",
|
||||
"jose": "6.0.8",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "15.2.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
@@ -55,15 +55,15 @@
|
||||
"zod": "3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.13.5",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "15.2.0",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"postcss": "8.5.3",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ function NoFundsPopup() {
|
||||
|
||||
<CodeExample
|
||||
preview={<PayTransactionButtonPreview />}
|
||||
code={`import { trasnfer } from "thirdweb/extensions/erc1155";
|
||||
import { PayEmbed, useActiveAccount } from "thirdweb/react";
|
||||
code={`import { transfer } from "thirdweb/extensions/erc1155";
|
||||
import { TransactionButton, useActiveAccount } from "thirdweb/react";
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
+1
-7
@@ -157,13 +157,7 @@ export function BlueprintPlaygroundUI(props: {
|
||||
const defaultValues = useMemo(() => {
|
||||
const values: Record<string, string | number> = {};
|
||||
for (const param of parameters) {
|
||||
if (param.name === "chain") {
|
||||
values.chain = "1";
|
||||
} else if (
|
||||
param.schema &&
|
||||
"type" in param.schema &&
|
||||
param.schema.default
|
||||
) {
|
||||
if (param.schema && "type" in param.schema && param.schema.default) {
|
||||
values[param.name] = param.schema.default;
|
||||
} else {
|
||||
values[param.name] = "";
|
||||
|
||||
@@ -3,24 +3,21 @@ import type { ConnectButtonProps } from "thirdweb/react";
|
||||
import { ecosystemWallet } from "thirdweb/wallets";
|
||||
import { StyledConnectEmbed } from "../styled-connect-embed";
|
||||
|
||||
const getEcosystem = () => {
|
||||
const getEcosystemWallet = () => {
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_IN_APP_WALLET_URL?.endsWith(".thirdweb-dev.com")
|
||||
) {
|
||||
// dev ecosystem
|
||||
return "ecosystem.catlovers";
|
||||
return ecosystemWallet("ecosystem.catlovers");
|
||||
}
|
||||
// prod ecosystem
|
||||
return "ecosystem.thirdweb-engs";
|
||||
return ecosystemWallet("ecosystem.thirdweb-engs", {
|
||||
partnerId: "50fd8850-3c6c-48dd-969c-622f88b52d95",
|
||||
});
|
||||
};
|
||||
|
||||
export function EcosystemConnectEmbed(
|
||||
props?: Omit<ConnectButtonProps, "client" | "theme">,
|
||||
) {
|
||||
return (
|
||||
<StyledConnectEmbed
|
||||
{...props}
|
||||
wallets={[ecosystemWallet(getEcosystem())]}
|
||||
/>
|
||||
);
|
||||
return <StyledConnectEmbed {...props} wallets={[getEcosystemWallet()]} />;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function PayTransactionPreview() {
|
||||
const { theme } = useTheme();
|
||||
const { data: nft } = useReadContract(getNFT, {
|
||||
contract: nftContract,
|
||||
tokenId: 1n,
|
||||
tokenId: 2n,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -51,7 +51,7 @@ export function PayTransactionPreview() {
|
||||
transaction: claimTo({
|
||||
contract: nftContract,
|
||||
quantity: 1n,
|
||||
tokenId: 1n,
|
||||
tokenId: 2n,
|
||||
to: account?.address || "",
|
||||
}),
|
||||
metadata: nft?.metadata,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@tanstack/react-query": "5.66.9",
|
||||
"@tanstack/react-query": "5.67.1",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -35,7 +35,7 @@
|
||||
"flexsearch": "^0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"lucide-react": "0.476.0",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "15.2.0",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
@@ -59,10 +59,10 @@
|
||||
"@types/flexsearch": "^0.7.6",
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-html-parser": "^2.0.6",
|
||||
"@types/react-html-parser": "^2.0.7",
|
||||
"@types/tryghost__content-api": "^1.3.16",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
@@ -77,6 +77,6 @@
|
||||
"postcss": "8.5.3",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,42 @@ With a thirdweb API key, you get access to bundler and paymaster infrastructure
|
||||
|
||||
To support a chain not listed, [contact us](https://thirdweb.com/contact-us).
|
||||
|
||||
## Using Paymaster & Bundler URL
|
||||
|
||||
To use the Paymaster & Bundler URL for thirdweb, you can follow the guide below:
|
||||
|
||||
### URL Pattern
|
||||
|
||||
The URL pattern is predictable for all chains and is used for both paymaster and bundler operations:
|
||||
|
||||
```
|
||||
https://<chain_id>.bundler.thirdweb.com/v2/<thirdweb-client-id>
|
||||
```
|
||||
|
||||
Replace `<chain_id>` with the appropriate chain ID and `<thirdweb-client-id>` with your thirdweb client ID.
|
||||
|
||||
### Client ID in Header
|
||||
|
||||
You can also pass the client ID via the header:
|
||||
|
||||
```
|
||||
x-client-id: <your_thirdweb_client_id>
|
||||
```
|
||||
|
||||
### Frontend Calls
|
||||
|
||||
For frontend calls, the calling domain needs to be allowlisted. This can be done from the thirdweb dashboard in the API key settings.
|
||||
|
||||
### Backend Usage
|
||||
|
||||
For backend usage, the secret key is required as a header, and the client ID can be omitted:
|
||||
|
||||
```
|
||||
x-secret-key: <your_thirdweb_secret_key>
|
||||
```
|
||||
|
||||
Make sure to replace the placeholders with your actual values. This guide will help you set up and use Paymaster & Bundler URLs effectively with thirdweb.
|
||||
|
||||
## Using a Custom Bundler
|
||||
|
||||
Smart accounts are free to use with your own account abstraction infrastructure. To use your own paymaster & bundler,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createMetadata, DocImage } from "@doc";
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Customize ConnectButton",
|
||||
title: "thirdweb Universal Bridge - Customize ConnectButton",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Customize ConnectButton - thirdweb",
|
||||
title: "thirdweb Universal Bridge - Customize ConnectButton - thirdweb",
|
||||
description:
|
||||
"thirdweb Pay - Customizing ConnectButton onramps and crypto purchase flows",
|
||||
"thirdweb Universal Bridge - Customizing ConnectButton onramps and crypto purchase flows",
|
||||
});
|
||||
|
||||
# Customize ConnectButton
|
||||
|
||||
Learn how to customize Pay within the `ConnectButton` interface. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/ConnectButtonProps).
|
||||
Learn how to customize Universal Bridge within the `ConnectButton` interface. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/ConnectButtonProps).
|
||||
|
||||
---
|
||||
|
||||
@@ -39,7 +39,7 @@ You can enable users to select your token on a given chain by passing an array o
|
||||
|
||||
## Prefill Destination Token
|
||||
|
||||
In the case you want your users to purchase your token by default in your application, you can choose to pre-fill the Pay purchase flow with `prefillBuy` .
|
||||
In the case you want your users to purchase your token by default in your application, you can choose to pre-fill the Universal Bridge purchase flow with `prefillBuy` .
|
||||
|
||||
For example, if you wanted users to only purchase Ethereum on Base network, you could do as follows:
|
||||
|
||||
@@ -137,7 +137,7 @@ In some cases, you may only want to show users fiat or crypto payment options fo
|
||||
|
||||
## Theme
|
||||
|
||||
You can set the theme for the Pay component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
|
||||
You can set the theme for the Universal Bridge component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
|
||||
|
||||
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createMetadata, DocImage } from "@doc";
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Customize PayEmbed",
|
||||
title: "thirdweb Universal Bridge - Customize PayEmbed",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Customize PayEmbed - thirdweb",
|
||||
title: "thirdweb Universal Bridge - Customize PayEmbed - thirdweb",
|
||||
description:
|
||||
"thirdweb Pay - Customizing embeddable onramps and crypto purchase flows",
|
||||
"thirdweb Universal Bridge - Customizing embeddable onramps and crypto purchase flows",
|
||||
});
|
||||
|
||||
# PayEmbed Customization
|
||||
@@ -129,7 +129,7 @@ In some cases, you may only want to show users fiat or crypto payment options fo
|
||||
|
||||
## Theme
|
||||
|
||||
You can set the theme for the Pay component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
|
||||
You can set the theme for the component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
|
||||
|
||||
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { createMetadata, DocImage } from "@doc";
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Customize sendTransaction",
|
||||
title: "thirdweb Universal Bridge - Customize sendTransaction",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Customize sendTransaction - thirdweb",
|
||||
title: "thirdweb Universal Bridge - Customize sendTransaction - thirdweb",
|
||||
description:
|
||||
"thirdweb Pay - Customizing point of sale onramps and crypto purchase flows",
|
||||
"thirdweb Universal Bridge- Customizing point of sale onramps and crypto purchase flows",
|
||||
});
|
||||
|
||||
# useSendTransaction Customization
|
||||
|
||||
Learn how to customize the Pay interface when executing a transaction with `useSendTransaction`. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/SendTransactionConfig).
|
||||
Learn how to customize the Universal Bridge interface when executing a transaction with `useSendTransaction`. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/SendTransactionConfig).
|
||||
|
||||
---
|
||||
|
||||
@@ -123,7 +123,7 @@ const { mutate: sendTransaction } = useSendTransaction({
|
||||
|
||||
## Theme
|
||||
|
||||
You can set the theme for the Pay component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
|
||||
You can set the theme for the component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
|
||||
|
||||
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
|
||||
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
import { createMetadata } from "@doc";
|
||||
import { createMetadata, Details } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - FAQs",
|
||||
title: "thirdweb Universal Bridge - FAQs",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - FAQs - thirdweb",
|
||||
description: "thirdweb Pay - Frequently Asked Questions",
|
||||
title: "thirdweb Universal Bridge - FAQs - thirdweb",
|
||||
description: "thirdweb Universal Bridge - Frequently Asked Questions",
|
||||
});
|
||||
|
||||
# FAQs
|
||||
|
||||
### What are the fees for Buy With Fiat?
|
||||
|
||||
<Details summary="What are the fees for Buy With Fiat?">
|
||||
thirdweb takes a 1% fee on all Buy With Fiat transactions. Additional processing fees will depend on the selected onramp vendor.
|
||||
</Details>
|
||||
|
||||
### What are the fees for Buy With Crypto?
|
||||
|
||||
<Details summary="What are the fees for Buy With Crypto?">
|
||||
thirdweb takes a 1% fee on all Buy With Crypto transactions.
|
||||
</Details>
|
||||
|
||||
### How does fee sharing work for Buy With Crypto?
|
||||
|
||||
<Details summary="How does fee sharing work for Buy With Crypto?">
|
||||
thirdweb shares 70% of all fees collected through Buy With Crypto with our developers. Fee sharing is not currently available for Buy With Fiat transactions.
|
||||
</Details>
|
||||
|
||||
### What does the term “network fees” refer to?
|
||||
|
||||
<Details summary="What does the term “network fees” refer to?">
|
||||
Network fees, also referred to as miner fees, are paid to the miner for processing crypto transactions and securing the respective network. These fees do not go to thirdweb.
|
||||
</Details>
|
||||
|
||||
### What are your transaction limits for Buy With Fiat?
|
||||
|
||||
<Details summary="What are your transaction limits for Buy With Fiat?">
|
||||
We currently require a $1 minimum purchase for both stablecoin and non-stablecoin purchases through Buy With Fiat.
|
||||
|
||||
Our transaction maximum starts from $1500 per week for new users and can increase over time to $10,000 per week.
|
||||
</Details>
|
||||
|
||||
### What countries does Buy With Fiat support?
|
||||
|
||||
<Details summary="What countries does Buy With Fiat support?">
|
||||
**Buy With Fiat** is available 130+ countries. The following countries are _UNSUPPORTED_:
|
||||
|
||||
- Afghanistan
|
||||
@@ -60,9 +59,9 @@ Our transaction maximum starts from $1500 per week for new users and can increas
|
||||
- Yemen
|
||||
|
||||
All United States are supported, excluding Hawaii.
|
||||
</Details>
|
||||
|
||||
### Does thirdweb offer direct onramping to tokens through Buy With Fiat?
|
||||
|
||||
<Details summary="Does thirdweb offer direct onramping to tokens through Buy With Fiat?">
|
||||
We offer direct onramping **internationally** to the following tokens:
|
||||
|
||||
- Mainnet (Ethereum)
|
||||
@@ -80,19 +79,22 @@ We offer direct onramping to the following tokens in the **US only**:
|
||||
All other tokens will require an additional Buy With Crypto step.
|
||||
|
||||
\* Not available in New York or the EU.
|
||||
</Details>
|
||||
|
||||
### How does KYC work with Buy With Fiat
|
||||
|
||||
<Details summary="How does KYC work with Buy With Fiat">
|
||||
Users will be required to complete KYC verification for their first purchase using 'Buy with Fiat.' After that, no further KYC will be needed for future purchases on the same platform.
|
||||
</Details>
|
||||
|
||||
### What cryptocurrencies does Pay support?
|
||||
|
||||
Pay can support any currency on our [supported chains](/connect/pay/supported-chains).
|
||||
|
||||
### How are cryptocurrency prices set in thirdweb Pay?
|
||||
<Details summary="What cryptocurrencies does Universal Bridge support?">
|
||||
Universal Bridge can support any currency on our [supported chains](/connect/pay/supported-chains).
|
||||
</Details>
|
||||
|
||||
<Details summary="How are cryptocurrency prices set in thirdweb Universal Bridge?">
|
||||
Prices are set depending on the liquidity provider used to route your transaction.
|
||||
</Details>
|
||||
|
||||
### Does thirdweb Pay offer fiat payouts?
|
||||
|
||||
<Details summary="Does thirdweb Universal Bridge offer fiat payouts?">
|
||||
Fiat payouts are not currently available.
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import paySettingsImage from "../assets/pay-settings.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Fee Sharing",
|
||||
title: "thirdweb Universal Bridge - Fee Sharing",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Fee Sharing | thirdweb",
|
||||
description: "How to participate in fee sharing with thirdweb Pay.",
|
||||
title: "thirdweb Universal Bridge - Fee Sharing | thirdweb",
|
||||
description: "How to participate in fee sharing with thirdweb Universal Bridge",
|
||||
});
|
||||
|
||||
# Fee Sharing
|
||||
@@ -18,6 +18,6 @@ For example, if a user purchases $100 worth of Polygon through your application,
|
||||
|
||||
Plase note that fee sharing only applies to fees collected through swaps and bridges. Fiat purchases do not currently qualify for fee sharing.
|
||||
|
||||
To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Pay in [thirdweb dashboard](https://www.thirdweb.com/team).
|
||||
To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Universal Bridge in [thirdweb dashboard](https://www.thirdweb.com/team).
|
||||
|
||||
<DocImage src={paySettingsImage} />
|
||||
|
||||
@@ -10,19 +10,19 @@ import GetStartedSend from "../assets/get-started-send.png";
|
||||
import GetStartedEmbed from "../assets/get-started-embed.png";
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Get Started",
|
||||
title: "thirdweb Universal Bridge - Get Started",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Buy With Fiat - thirdweb",
|
||||
title: "Universal Bridge Implementation Guide — thirdweb docs",
|
||||
description:
|
||||
"Get Started with Pay - Onchain Purchases Directly From Credit or Debit Card",
|
||||
"Learn how to implement all-in-one web3 payment solution, Universal bridge: the technical step-by-step process",
|
||||
});
|
||||
|
||||
# Get Started
|
||||
|
||||
Learn how to add onramps and crypto purchases to your application. To see which integration is right for you, refer to our [integrations overview](/connect/pay/overview#integration-options).
|
||||
|
||||
The following guide uses our React SDK. You can also learn how to integrate Pay with our [Unity SDK](https://portal.thirdweb.com/unity/pay/getbuywithcryptoquote).
|
||||
The following guide uses our React SDK. You can also learn how to integrate Universal Bridge with our [Unity SDK](https://portal.thirdweb.com/unity/pay/getbuywithcryptoquote).
|
||||
|
||||
---
|
||||
|
||||
@@ -66,7 +66,7 @@ Our `ConnectButton` has a number of [customization options](/connect/pay/customi
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Embed Pay
|
||||
## Option 2: Embed
|
||||
|
||||
The `PayEmbed` allows users to onramp and purchase crypto directly from your application interface.
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
The embedded Pay component will be displayed like so in your application interface:
|
||||
The embedded component will be displayed like so in your application interface:
|
||||
|
||||
<DocImage src={GetStartedEmbed} />
|
||||
|
||||
@@ -96,7 +96,7 @@ Our `PayEmbed` has a number of [customization options](/connect/pay/customizatio
|
||||
|
||||
## Option 3: Send a Transaction with Pay
|
||||
|
||||
Pay is enabled by default for any contract call sent with `sendTransaction`. It will automatically be invoked when a user does not have enough funds to complete the transaction.
|
||||
Universal Bridge is enabled by default for any contract call sent with `sendTransaction`. It will automatically be invoked when a user does not have enough funds to complete the transaction.
|
||||
|
||||
<Callout variant="info">
|
||||
ℹ️ `sendTransaction` is available in [Connect SDK v5](/typescript/v5) or
|
||||
@@ -123,10 +123,10 @@ const onClick = () => {
|
||||
};
|
||||
```
|
||||
|
||||
When a user clicks this button, Pay will perform gas estimation to determine if the user has a sufficient balance. If their balance is sufficient, the transaction will execute normally. If their balance is not sufficient, the following modal will pop up asking them to either onramp funds or convert crypto to the required amount:
|
||||
When a user clicks this button, Universal Bridge will perform gas estimation to determine if the user has a sufficient balance. If their balance is sufficient, the transaction will execute normally. If their balance is not sufficient, the following modal will pop up asking them to either onramp funds or convert crypto to the required amount:
|
||||
|
||||
<DocImage src={GetStartedSend} />
|
||||
|
||||
Once a user onramps or converts their funds to the required token, the user will be prompted once more to confirm the transaction. The transaction will then execute as expected.
|
||||
|
||||
For deeper customization of the Pay transaction modal, you can refer to our [sendTransaction customization guide](/connect/pay/customization/send-transaction).
|
||||
For deeper customization of the Universal Bridge transaction modal, you can refer to our [sendTransaction customization guide](/connect/pay/customization/send-transaction).
|
||||
|
||||
@@ -3,17 +3,17 @@ import DirectPaymentsFlow from "../../assets/direct-payments-flow.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Accept Direct Payments",
|
||||
title: "thirdweb Universal Bridge - Accept Direct Payments",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Accept Direct Payments | thirdweb",
|
||||
title: "thirdweb Universal Bridge - Accept Direct Payments | thirdweb",
|
||||
description:
|
||||
"Combine Pay and Engine to create a smooth point of sale experience on any chain",
|
||||
"Combine Universal Bridge and Engine to create a smooth point of sale experience on any chain",
|
||||
});
|
||||
|
||||
# Accept Direct Payments with Pay & Engine
|
||||
# Accept Direct Payments with Universal Bridge & Engine
|
||||
|
||||
Learn how to accept fiat and crypto from your customers with Pay and trigger Engine actions, like minting NFTs or transferring tokens to user wallets.
|
||||
Learn how to accept fiat and crypto from your customers with Universal Bridge and trigger Engine actions, like minting NFTs or transferring tokens to user wallets.
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -26,11 +26,11 @@ Before jumping in, it's important to understand the full system, from initial pu
|
||||
|
||||
<DocImage src={DirectPaymentsFlow} />
|
||||
|
||||
1. **User Initiates Purchase:** User sends specified funds (ETH, MATIC, USD, etc.) to your wallet via thirdweb Pay.
|
||||
1. **User Initiates Purchase:** User sends specified funds (ETH, MATIC, USD, etc.) to your wallet via thirdweb Universal Bridge.
|
||||
- Your wallet can be any business wallet you choose or your Engine backend wallet.
|
||||
- Use the `purchaseData` field to pass any item data needed in the webhook.
|
||||
E.g., `{ nftId: 1 }`.
|
||||
2. **Pay Processes Purchase:** thirdweb Pay processes the transaction and sends a [Purchase Complete webhook](/connect/pay/webhooks#purchase-complete) to your dapp’s server when the transaction is complete.
|
||||
2. **Universal Bridge Processes Purchase:** thirdweb Universal Bridge processes the transaction and sends a [Purchase Complete webhook](/connect/pay/webhooks#purchase-complete) to your dapp’s server when the transaction is complete.
|
||||
3. **Your Server Calls Engine:** Your server processes the webhook and sends a contract call to the thirdweb Engine backend wallet to perform the item purchase.
|
||||
4. **Engine Executes the Transaction:** Engine backend wallet executes the transaction needed to deliver the token(s) to the user’s address.
|
||||
5. **(Optional) User Is Notified :** Your server confirms the completed Engine transaction and notifies the user.
|
||||
@@ -52,11 +52,11 @@ Now, let's get started.
|
||||
|
||||
4. Make a plan to monitor and keep the float wallet topped up to avoid any issues with the service.
|
||||
</Step>
|
||||
<Step title="Integrate thirdweb Pay (Client)">
|
||||
1. [Integrate thirdweb Pay](https://portal.thirdweb.com/connect/pay/build-a-custom-experience) on the client side to enable fiat and crypto payments.
|
||||
<Step title="Integrate thirdweb Universal Bridge (Client)">
|
||||
1. [Integrate thirdweb Universal Bridge](https://portal.thirdweb.com/connect/pay/build-a-custom-experience) on the client side to enable fiat and crypto payments.
|
||||
|
||||
|
||||
2. Specify the required parameters for your Pay transaction.
|
||||
2. Specify the required parameters for your Universal Bridge transaction.
|
||||
|
||||
```ts
|
||||
const quote = await getBuyWithFiatQuote({
|
||||
@@ -84,9 +84,9 @@ Now, let's get started.
|
||||
|purchaseData|`Object`|Any details you need to pass in order to track your transaction. If this is a secondary sale, you may want to include details like `{ nftId: 1, orderId: 1 }` |
|
||||
|
||||
</Step>
|
||||
<Step title="Integrate thirdweb Pay (Server)">
|
||||
1. Log in & create a “Purchase Complete” webhook in your Team > Connect > Pay > Webhooks page.
|
||||
2. The [Pay webhook POST response](/connect/pay/webhooks#purchase-complete) will include the BuyWithCryptoStatus or BuyWithFiatStatus in the body.
|
||||
<Step title="Integrate thirdweb Universal Bridge (Server)">
|
||||
1. Log in & create a “Purchase Complete” webhook in your Team > Connect > Universal Bridge > Webhooks page.
|
||||
2. The [Universal Bridge webhook POST response](/connect/pay/webhooks#purchase-complete) will include the BuyWithCryptoStatus or BuyWithFiatStatus in the body.
|
||||
* This will also include the purchaseData you previously passed (metadata identifying the customer information, etc.).
|
||||
* A request will be sent on every status change.
|
||||
3. Listen for [completed purchase statuses](https://portal.thirdweb.com/connect/pay/build-a-custom-experience#Poll%20for%20Transaction%20Status). Note that if this is a two-step BuyWithFiat transaction (onramp, then swap), you should also listen for `CRYPTO_SWAP_COMPLETED`
|
||||
|
||||
@@ -10,12 +10,12 @@ import OnrampStepOne from "../../assets/avax-to-usd.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Custom Experience",
|
||||
title: "thirdweb Universal Bridge - Custom Experience",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Buy With Fiat - Custom Experience | thirdweb",
|
||||
title: "thirdweb Universal Bridge - Buy With Fiat - Custom Experience | thirdweb",
|
||||
description:
|
||||
"Learn how to build a custom onramp experience with thirdweb Pay.",
|
||||
"Learn how to build a custom onramp experience with thirdweb Universal Bridge.",
|
||||
});
|
||||
|
||||
# Build a Custom Fiat Experience
|
||||
|
||||
@@ -2,17 +2,17 @@ import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Onramp Providers",
|
||||
title: "thirdweb Universal Bridge - Onramp Providers",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Onramp Providers | thirdweb",
|
||||
title: "thirdweb Universal Bridge - Onramp Providers | thirdweb",
|
||||
description:
|
||||
"Integrate onramp providers for any onchain transaction and set preferred providers",
|
||||
});
|
||||
|
||||
# Onramp Providers
|
||||
|
||||
thirdweb Pay integrates with Stripe, Kado and Transak to power our onramp. By default, we choose a recommended provider based on the location of the user, KYC status, and currency.
|
||||
thirdweb Universal Bridge integrates with Stripe, Kado and Transak to power our onramp. By default, we choose a recommended provider based on the location of the user, KYC status, and currency.
|
||||
|
||||
| Provider | Supported Countries |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard } from "@doc";
|
||||
import PayOverviewImage from "../assets/pay-overview.png";
|
||||
import SupportedChains from "../../../_images/supported-chains.png";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay",
|
||||
title: "thirdweb Universal Bridge",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - thirdweb",
|
||||
title: "thirdweb Universal Bridge Docs: Web3 Payments, On-ramping, bridging & swapping",
|
||||
description:
|
||||
"thirdweb Pay - Fast, flexible credit card and crypto point of sale",
|
||||
"Learn everything about thirdweb’s web3 payments solution, Universal Bridge. Technical docs on onramping, bridging + swapping.",
|
||||
});
|
||||
|
||||
# Overview
|
||||
|
||||
Pay allows your users to purchase cryptocurrencies and execute transactions with their credit card or debit card, or with any token via cross-chain routing.
|
||||
Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencies and execute transactions with any fiat options or tokens via cross-chain routing.
|
||||
|
||||
<DocImage src={PayOverviewImage} />
|
||||
|
||||
@@ -67,14 +68,30 @@ Pay allows your users to purchase cryptocurrencies and execute transactions with
|
||||
|
||||
</div>
|
||||
|
||||
### Use Cases
|
||||
## Supported Chains
|
||||
|
||||
- **Onramp funds directly into any transaction,** perfect for any of the following use cases:
|
||||
- **Games** selling in-game assets onchain.
|
||||
- **DEXes** looking to help users deposit funds.
|
||||
- **Staking apps** onboarding new users to their pools.
|
||||
- **Social apps** looking to add tipping or other paid functionality to their application.
|
||||
- **Non-profits** accepting donations in crypto.
|
||||
Universal Bridge is supported on select EVM compatible chains. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist?service=pay).
|
||||
|
||||
<DocImage src={SupportedChains} />
|
||||
|
||||
## Fee Sharing
|
||||
|
||||
Thirdweb collects a 1% fee per end-user transaction and shares 70% of that fee with you. For example, on a $100 Polygon purchase, a $1.00 fee is charged—of which $0.30 is retained by thirdweb and $0.70 is remitted to you.
|
||||
|
||||
Note: Fee sharing applies only to fees from swaps and bridges; fiat purchases are excluded.
|
||||
|
||||
To enable fee sharing, simply set a recipient address in your dashboard under Team > Project > Connect > Universal Bridge in the thirdweb dashboard.
|
||||
|
||||
## Onramp providers
|
||||
|
||||
| Provider | Supported Countries |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| Transak | https://transak.notion.site/On-Ramp-Payment-Methods-Fees-Other-Details-b0761634feed4b338a69f4f186d906a5 |
|
||||
| Kado | https://www.kado.money/supported-countries |
|
||||
| Stripe | https://docs.stripe.com/crypto/onramp |
|
||||
|
||||
Specify a preferred onramp provider by setting the preferredProvider parameter in ConnectButton, PayEmbed, or Headless Integration. For details, refer to
|
||||
[customization](../pay/customization/connectbutton#preferredProvider) or the [API Reference for preferredProvider](/references/typescript/v5/GetBuyWithFiatQuoteParams).
|
||||
|
||||
## Integration Options
|
||||
|
||||
@@ -83,4 +100,4 @@ Pay allows your users to purchase cryptocurrencies and execute transactions with
|
||||
| [ConnectButton](/connect/pay/get-started#option-1-connectbutton) | Developers who want out-of-the-box support complete with wallet and social login. |
|
||||
| [PayEmbed](/connect/pay/get-started#option-2-embed-pay) | Developers who want onramp and crypto purchase experiences directly in their application. |
|
||||
| [Transaction Flow (sendTransaction)](/connect/pay/get-started#option-3-send-a-transaction-with-pay) | Developers who want users to onramp or purchase crypto directly into a transaction. Great for minting and NFT purchase flows. |
|
||||
| [Custom Pay Experience](/connect/pay/build-a-custom-experience) | Developers who want full customization over every step of the onramp process, from displaying quotes to polling for transaction statuses. |
|
||||
| [Custom Universal Bridge Experience](/connect/pay/build-a-custom-experience) | Developers who want full customization over every step of the onramp process, from displaying quotes to polling for transaction statuses. |
|
||||
|
||||
@@ -2,15 +2,15 @@ import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "thirdweb Pay - Supported Chains",
|
||||
title: "thirdweb Universal Bridge - Supported Chains",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Supported Chains | thirdweb",
|
||||
description: "List of chains supported by Pay.",
|
||||
title: "thirdweb Universal Bridge - Supported Chains | thirdweb",
|
||||
description: "List of chains supported by Universal Bridge.",
|
||||
});
|
||||
|
||||
# Supported Chains
|
||||
|
||||
[View all supported chains with Pay](https://thirdweb.com/chainlist?service=pay).
|
||||
[View all supported chains with Universal Bridge](https://thirdweb.com/chainlist?service=pay).
|
||||
|
||||
If you’d like to suggest a chain for us to support, [contact us](https://thirdweb.com/contact-us).
|
||||
|
||||
@@ -6,8 +6,8 @@ export const metadata = createMetadata({
|
||||
title: "thirdweb Pay - Test Mode",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
title: "thirdweb Pay - Test Mode - thirdweb",
|
||||
description: "thirdweb Pay - Test Mode",
|
||||
title: "Enable Test Mode for Universal bridge — thirdweb docs",
|
||||
description: "Learn how to enable test mode for web3 payments with the thirdweb Universal bridge feature in our technical docs",
|
||||
});
|
||||
|
||||
# Enable Test Mode
|
||||
|
||||
@@ -2,9 +2,9 @@ import { createMetadata, DocImage } from "@doc";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Webhooks | thirdweb Pay",
|
||||
title: "Universal Bridge Webhooks Implementation Guide — thirdweb Docs",
|
||||
description:
|
||||
"Configure Pay webhooks to get notified at any point during a transaction's lifecycle.",
|
||||
"Learn how to set up webhooks for web3 payments with thirdweb’s Universal bridge feature: the technical docs",
|
||||
});
|
||||
|
||||
# Webhooks
|
||||
|
||||
@@ -270,99 +270,78 @@ export const sidebar: SideBar = {
|
||||
// Blockchain API
|
||||
// TODO Overview page?
|
||||
{
|
||||
name: "Onchain Interactions",
|
||||
name: "Universal Bridge",
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
// Pay
|
||||
{
|
||||
name: "Bridge & Swap",
|
||||
name: "Overview",
|
||||
href: `${paySlug}/overview`,
|
||||
},
|
||||
{
|
||||
name: "Get Started",
|
||||
href: `${paySlug}/get-started`,
|
||||
expanded: true,
|
||||
links: [
|
||||
{
|
||||
name: "Overview",
|
||||
href: `${paySlug}/overview`,
|
||||
name: "ConnectButton",
|
||||
href: `${paySlug}/get-started#option-1-connectbutton`,
|
||||
},
|
||||
{
|
||||
name: "Get Started",
|
||||
href: `${paySlug}/get-started`,
|
||||
expanded: true,
|
||||
links: [
|
||||
{
|
||||
name: "ConnectButton",
|
||||
href: `${paySlug}/get-started#option-1-connectbutton`,
|
||||
},
|
||||
{
|
||||
name: "Embed Pay",
|
||||
href: `${paySlug}/get-started#option-2-embed-pay`,
|
||||
},
|
||||
{
|
||||
name: "Send a Transaction",
|
||||
href: `${paySlug}/get-started#option-3-send-a-transaction-with-pay`,
|
||||
},
|
||||
],
|
||||
name: "Embed Pay",
|
||||
href: `${paySlug}/get-started#option-2-embed-pay`,
|
||||
},
|
||||
{
|
||||
name: "Supported Chains",
|
||||
href: `${paySlug}/supported-chains`,
|
||||
},
|
||||
{
|
||||
name: "Onramp Providers",
|
||||
href: `${paySlug}/onramp-providers`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Fee Sharing",
|
||||
href: `${paySlug}/fee-sharing`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Webhooks",
|
||||
href: `${paySlug}/webhooks`,
|
||||
},
|
||||
{
|
||||
name: "Testing Pay",
|
||||
href: `${paySlug}/testing-pay`,
|
||||
},
|
||||
{
|
||||
name: "Guides",
|
||||
isCollapsible: true,
|
||||
|
||||
links: [
|
||||
{
|
||||
name: "Accept Direct Payments",
|
||||
href: `${paySlug}/guides/accept-direct-payments`,
|
||||
},
|
||||
{
|
||||
name: "Build a Custom Experience",
|
||||
href: `${paySlug}/guides/build-a-custom-experience`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Customization",
|
||||
isCollapsible: true,
|
||||
|
||||
links: [
|
||||
{
|
||||
name: "ConnectButton",
|
||||
href: `${paySlug}/customization/connectbutton`,
|
||||
},
|
||||
{
|
||||
name: "PayEmbed",
|
||||
href: `${paySlug}/customization/payembed`,
|
||||
},
|
||||
{
|
||||
name: "useSendTransaction",
|
||||
href: `${paySlug}/customization/send-transaction`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "FAQs",
|
||||
href: `${paySlug}/faqs`,
|
||||
name: "Send a Transaction",
|
||||
href: `${paySlug}/get-started#option-3-send-a-transaction-with-pay`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Tutorials",
|
||||
isCollapsible: true,
|
||||
|
||||
links: [
|
||||
{
|
||||
name: "Accept Direct Payments",
|
||||
href: `${paySlug}/guides/accept-direct-payments`,
|
||||
},
|
||||
{
|
||||
name: "Build a Custom Experience",
|
||||
href: `${paySlug}/guides/build-a-custom-experience`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Customization",
|
||||
isCollapsible: true,
|
||||
|
||||
links: [
|
||||
{
|
||||
name: "ConnectButton",
|
||||
href: `${paySlug}/customization/connectbutton`,
|
||||
},
|
||||
{
|
||||
name: "PayEmbed",
|
||||
href: `${paySlug}/customization/payembed`,
|
||||
},
|
||||
{
|
||||
name: "useSendTransaction",
|
||||
href: `${paySlug}/customization/send-transaction`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
href: `${paySlug}/webhooks`,
|
||||
},
|
||||
{
|
||||
name: "Developer Mode",
|
||||
href: `${paySlug}/testing-pay`,
|
||||
},
|
||||
{
|
||||
name: "FAQs",
|
||||
href: `${paySlug}/faqs`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ separator: true },
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tanstack/react-query": "5.66.9",
|
||||
"@tanstack/react-query": "5.67.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"lucide-react": "0.476.0",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "15.2.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "19.0.0",
|
||||
@@ -35,18 +35,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "15.2.0",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"knip": "5.45.0",
|
||||
"postcss": "8.5.3",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -43,9 +43,9 @@
|
||||
"@changesets/changelog-github": "0.5.1",
|
||||
"@changesets/cli": "2.28.1",
|
||||
"@manypkg/get-packages": "2.2.2",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/node": "22.13.9",
|
||||
"turbo": "2.4.4",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @thirdweb-dev/service-utils
|
||||
|
||||
## 0.8.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6416](https://github.com/thirdweb-dev/js/pull/6416) [`44bb003`](https://github.com/thirdweb-dev/js/commit/44bb0034a69334f05cfa14132d56e354536e77ff) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] Add "pay" as a usage source
|
||||
|
||||
## 0.8.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6409](https://github.com/thirdweb-dev/js/pull/6409) [`67354dc`](https://github.com/thirdweb-dev/js/commit/67354dc848bfb00d2296d5e52990321e9d28abb0) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] remove unneeded cloudflare worker types
|
||||
|
||||
## 0.8.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/service-utils",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.13",
|
||||
"type": "module",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
@@ -51,8 +51,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20250224.0",
|
||||
"@types/node": "22.13.5",
|
||||
"typescript": "5.7.3",
|
||||
"@types/node": "22.13.9",
|
||||
"typescript": "5.8.2",
|
||||
"vitest": "3.0.7"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type {
|
||||
ExecutionContext,
|
||||
KVNamespace,
|
||||
Request,
|
||||
Response,
|
||||
} from "@cloudflare/workers-types";
|
||||
import type { Request } from "@cloudflare/workers-types";
|
||||
import type { CoreServiceConfig, TeamAndProjectResponse } from "../core/api.js";
|
||||
import { authorize } from "../core/authorize/index.js";
|
||||
import type {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Headers, type Request, fetch } from "@cloudflare/workers-types";
|
||||
import type { Request } from "@cloudflare/workers-types";
|
||||
import type { CoreAuthInput } from "../core/types.js";
|
||||
import type {
|
||||
ClientUsageV2Event,
|
||||
@@ -34,7 +34,7 @@ export async function sendUsageV2Events<T extends UsageV2Options>(
|
||||
// Forward headers from the origin request.
|
||||
// Determine endpoint and auth header based on provided credentials.
|
||||
let url: string;
|
||||
const headers = new Headers(authInput.req.headers);
|
||||
const headers = new Headers(Object.fromEntries(authInput.req.headers));
|
||||
headers.set("Content-Type", "application/json");
|
||||
if (serviceKey) {
|
||||
// If a service key is provided, call the non-public usage endpoint.
|
||||
|
||||
@@ -7,6 +7,7 @@ export const USAGE_V2_SOURCES = [
|
||||
"sdk",
|
||||
"storage",
|
||||
"wallet",
|
||||
"pay",
|
||||
] as const;
|
||||
export type UsageV2Source = (typeof USAGE_V2_SOURCES)[number];
|
||||
export function getTopicName(source: UsageV2Source) {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.90.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6432](https://github.com/thirdweb-dev/js/pull/6432) [`617fe68`](https://github.com/thirdweb-dev/js/commit/617fe68c32888462d09ff335cd7fb2d671e667bf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow overriding storage for inApp and ecosystem wallets
|
||||
|
||||
## 5.90.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6424](https://github.com/thirdweb-dev/js/pull/6424) [`38ae2d3`](https://github.com/thirdweb-dev/js/commit/38ae2d3c6b7d5052974e8a61333997699230ad45) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose getUserOpHash utility function
|
||||
|
||||
## 5.90.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6415](https://github.com/thirdweb-dev/js/pull/6415) [`fb446bd`](https://github.com/thirdweb-dev/js/commit/fb446bd0993a0f431fbc67e6142ec20866c4bc65) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - emit onPurchaseSuccess for wallet to wallet transfer flow before execution
|
||||
|
||||
- [#6412](https://github.com/thirdweb-dev/js/pull/6412) [`9f1a77f`](https://github.com/thirdweb-dev/js/commit/9f1a77f4c49350018dad0037c4ab675818057264) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow resending OTP on failures
|
||||
|
||||
## 5.90.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.90.1",
|
||||
"version": "5.90.4",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
@@ -218,7 +218,7 @@
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-tooltip": "1.1.8",
|
||||
"@tanstack/react-query": "5.66.9",
|
||||
"@tanstack/react-query": "5.67.1",
|
||||
"@walletconnect/ethereum-provider": "2.17.5",
|
||||
"@walletconnect/sign-client": "2.17.5",
|
||||
"abitype": "1.0.8",
|
||||
@@ -334,19 +334,19 @@
|
||||
"@aws-sdk/client-lambda": "3.592.0",
|
||||
"@aws-sdk/credential-providers": "3.592.0",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@chromatic-com/storybook": "3.2.5",
|
||||
"@codspeed/vitest-plugin": "4.0.0",
|
||||
"@coinbase/wallet-mobile-sdk": "1.1.2",
|
||||
"@mobile-wallet-protocol/client": "0.1.2",
|
||||
"@react-native-async-storage/async-storage": "2.1.1",
|
||||
"@size-limit/preset-big-lib": "11.2.0",
|
||||
"@storybook/addon-essentials": "8.6.0",
|
||||
"@storybook/addon-interactions": "8.6.0",
|
||||
"@storybook/addon-links": "8.6.0",
|
||||
"@storybook/addon-onboarding": "8.6.0",
|
||||
"@storybook/react": "8.6.0",
|
||||
"@storybook/react-vite": "8.6.0",
|
||||
"@storybook/test": "8.6.0",
|
||||
"@storybook/addon-essentials": "8.6.3",
|
||||
"@storybook/addon-interactions": "8.6.3",
|
||||
"@storybook/addon-links": "8.6.3",
|
||||
"@storybook/addon-onboarding": "8.6.3",
|
||||
"@storybook/react": "8.6.3",
|
||||
"@storybook/react-vite": "8.6.3",
|
||||
"@storybook/test": "8.6.3",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -375,10 +375,10 @@
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"size-limit": "11.2.0",
|
||||
"storybook": "8.6.0",
|
||||
"storybook": "8.6.3",
|
||||
"typedoc": "0.27.9",
|
||||
"typedoc-better-json": "0.9.4",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "5.8.2",
|
||||
"vite": "6.2.0",
|
||||
"vitest": "3.0.7"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
|
||||
MarketplaceV3: "0x8b0DBCf5b7D01eBB0F24525CE8AB72F16CE4F8C8",
|
||||
},
|
||||
[324]: {
|
||||
MarketplaceV3: "0xBc02441a36Bb4029Cd191b20243c2e41B862F118",
|
||||
MarketplaceV3: "0x88e7F2bEf0a23073b7Cc2c79D307Dbd9138Fa79c",
|
||||
},
|
||||
[11124]: {
|
||||
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
|
||||
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
|
||||
MarketplaceV3: "0x9EB0830B0b10010F2a53383517A7D0B75531Bb1b",
|
||||
},
|
||||
[2741]: {
|
||||
MarketplaceV3: "0x4027561E163a420c4e5Db46E07EBd581CAa8Bb62",
|
||||
MarketplaceV3: "0x4b14569c7B79DBe686Ac3Ba5996131E7EDaB7a93",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@ export {
|
||||
resolveArweaveScheme,
|
||||
type ResolveArweaveSchemeOptions,
|
||||
} from "../utils/arweave.js";
|
||||
export type { AsyncStorage } from "../utils/storage/AsyncStorage.js";
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
createUnsignedUserOp,
|
||||
signUserOp,
|
||||
createAndSignUserOp,
|
||||
getUserOpHash,
|
||||
} from "../../wallets/smart/lib/userop.js";
|
||||
|
||||
export {
|
||||
|
||||
@@ -17,10 +17,8 @@ import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value
|
||||
import { toEther } from "../../utils/units.js";
|
||||
import { generateMerkleTreeInfoERC1155 } from "../airdrop/write/merkleInfoERC1155.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
|
||||
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
|
||||
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
|
||||
import { totalSupply } from "./__generated__/IERC1155/read/totalSupply.js";
|
||||
import { nextTokenIdToMint } from "./__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js";
|
||||
import { canClaim } from "./drops/read/canClaim.js";
|
||||
import { getActiveClaimCondition } from "./drops/read/getActiveClaimCondition.js";
|
||||
@@ -472,63 +470,6 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
expect(isGetNFTsSupported(selectors)).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* This is to document the behavior where one can claim without paying if the claiming address
|
||||
* is the same as the PrimaryRecipientAddress, because of this Solidity code:
|
||||
* ```solidity
|
||||
* // CurrencyTransferLib.sol
|
||||
* function safeTransferERC20(address _currency, address _from, address _to, uint256 _amount) internal {
|
||||
* if (_from == _to) {
|
||||
* return;
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
it("address that is the same with PrimaryFeeRecipient can claim without paying ERC20", async () => {
|
||||
const tokenAddress = await deployERC20Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account: TEST_ACCOUNT_C,
|
||||
type: "TokenERC20",
|
||||
params: {
|
||||
name: "token20",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
});
|
||||
const tokenId = 5n;
|
||||
const setClaimTx = setClaimConditions({
|
||||
contract,
|
||||
tokenId,
|
||||
phases: [
|
||||
{
|
||||
maxClaimableSupply: 100n,
|
||||
maxClaimablePerWallet: 100n,
|
||||
currencyAddress: tokenAddress,
|
||||
price: 1000,
|
||||
startTime: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
transaction: setClaimTx,
|
||||
account: TEST_ACCOUNT_C,
|
||||
});
|
||||
|
||||
const transaction = claimTo({
|
||||
contract,
|
||||
tokenId,
|
||||
quantity: 50n,
|
||||
to: TEST_ACCOUNT_C.address,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account: TEST_ACCOUNT_C,
|
||||
});
|
||||
const supplyCount = await totalSupply({ contract, id: tokenId });
|
||||
expect(supplyCount).toBe(50n);
|
||||
});
|
||||
|
||||
it("getActiveClaimCondition should work", async () => {
|
||||
// Create a public allowlist claim phase
|
||||
const snapshot = [
|
||||
|
||||
@@ -300,7 +300,6 @@ export function ConnectButton(props: ConnectButtonProps) {
|
||||
|
||||
usePreloadWalletProviders({
|
||||
wallets,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
// Add props.chain and props.chains to defined chains store
|
||||
|
||||
@@ -215,7 +215,6 @@ export function ConnectEmbed(props: ConnectEmbedProps) {
|
||||
|
||||
usePreloadWalletProviders({
|
||||
wallets,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
const modalSize = useMemo(() => {
|
||||
|
||||
+116
-4
@@ -6,12 +6,18 @@ import type { ThirdwebClient } from "../../../../../../../client/client.js";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
|
||||
import { getContract } from "../../../../../../../contract/contract.js";
|
||||
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
|
||||
import {
|
||||
type GetCurrencyMetadataResult,
|
||||
getCurrencyMetadata,
|
||||
} from "../../../../../../../extensions/erc20/read/getCurrencyMetadata.js";
|
||||
import { approve } from "../../../../../../../extensions/erc20/write/approve.js";
|
||||
import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js";
|
||||
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
|
||||
import { getBuyWithCryptoTransfer } from "../../../../../../../pay/buyWithCrypto/getTransfer.js";
|
||||
import { sendAndConfirmTransaction } from "../../../../../../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js";
|
||||
import { prepareTransaction } from "../../../../../../../transaction/prepare-transaction.js";
|
||||
import type { TransactionReceipt } from "../../../../../../../transaction/types.js";
|
||||
import type { Address } from "../../../../../../../utils/address.js";
|
||||
import { toWei } from "../../../../../../../utils/units.js";
|
||||
import { iconSize } from "../../../../../../core/design-system/index.js";
|
||||
@@ -42,6 +48,7 @@ type TransferConfirmationScreenProps = {
|
||||
tokenAmount: string;
|
||||
transactionMode?: boolean;
|
||||
payOptions?: PayUIOptions;
|
||||
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
|
||||
};
|
||||
|
||||
export function TransferConfirmationScreen(
|
||||
@@ -202,10 +209,33 @@ export function TransferConfirmationScreen(
|
||||
to: receiverAddress,
|
||||
amount: tokenAmount,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction,
|
||||
});
|
||||
const [txResult, tokenMetadata] = await Promise.all([
|
||||
sendAndConfirmTransaction({
|
||||
account: props.payer.account,
|
||||
transaction,
|
||||
}),
|
||||
getCurrencyMetadata({
|
||||
contract: getContract({
|
||||
address: isNativeToken(token)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: token.address,
|
||||
chain: chain,
|
||||
client: client,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
// its the last step before the transaction, so propagate onPurchaseSuccess here
|
||||
props.onSuccess?.(
|
||||
transferBuyWithCryptoQuote({
|
||||
token,
|
||||
chain,
|
||||
tokenMetadata,
|
||||
tokenAmount,
|
||||
fromAddress: payer.account.address,
|
||||
toAddress: receiverAddress,
|
||||
transaction: txResult,
|
||||
}),
|
||||
);
|
||||
// switch to execute step
|
||||
setStep("execute");
|
||||
setStatus({ id: "idle" });
|
||||
@@ -299,3 +329,85 @@ export function TransferConfirmationScreen(
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function transferBuyWithCryptoQuote(args: {
|
||||
token: ERC20OrNativeToken;
|
||||
chain: Chain;
|
||||
tokenMetadata: GetCurrencyMetadataResult;
|
||||
tokenAmount: string;
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
transaction: TransactionReceipt;
|
||||
}): BuyWithCryptoStatus {
|
||||
const {
|
||||
token,
|
||||
chain,
|
||||
tokenMetadata,
|
||||
tokenAmount,
|
||||
fromAddress,
|
||||
toAddress,
|
||||
transaction,
|
||||
} = args;
|
||||
return {
|
||||
status: "COMPLETED",
|
||||
subStatus: "SUCCESS",
|
||||
swapType: "TRANSFER",
|
||||
quote: {
|
||||
createdAt: new Date().toISOString(),
|
||||
fromToken: {
|
||||
chainId: chain.id,
|
||||
tokenAddress: isNativeToken(token)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: token.address,
|
||||
decimals: tokenMetadata.decimals,
|
||||
symbol: tokenMetadata.symbol,
|
||||
name: tokenMetadata.name,
|
||||
priceUSDCents: 0,
|
||||
},
|
||||
toToken: {
|
||||
chainId: chain.id,
|
||||
tokenAddress: isNativeToken(token)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: token.address,
|
||||
decimals: tokenMetadata.decimals,
|
||||
symbol: tokenMetadata.symbol,
|
||||
name: tokenMetadata.name,
|
||||
priceUSDCents: 0,
|
||||
},
|
||||
fromAmountWei: toWei(tokenAmount).toString(),
|
||||
fromAmount: tokenAmount,
|
||||
toAmountWei: toWei(tokenAmount).toString(),
|
||||
toAmount: tokenAmount,
|
||||
toAmountMin: tokenAmount,
|
||||
toAmountMinWei: toWei(tokenAmount).toString(),
|
||||
estimated: {
|
||||
feesUSDCents: 0,
|
||||
gasCostUSDCents: 0,
|
||||
slippageBPS: 0,
|
||||
toAmountMinUSDCents: 0,
|
||||
toAmountUSDCents: 0,
|
||||
fromAmountUSDCents: 0,
|
||||
durationSeconds: 0,
|
||||
},
|
||||
},
|
||||
fromAddress,
|
||||
toAddress,
|
||||
source: {
|
||||
transactionHash: transaction.transactionHash,
|
||||
amount: tokenAmount,
|
||||
amountWei: toWei(tokenAmount).toString(),
|
||||
amountUSDCents: 0,
|
||||
completedAt: new Date().toISOString(),
|
||||
token: {
|
||||
chainId: chain.id,
|
||||
tokenAddress: isNativeToken(token)
|
||||
? NATIVE_TOKEN_ADDRESS
|
||||
: token.address,
|
||||
decimals: tokenMetadata.decimals,
|
||||
symbol: tokenMetadata.symbol,
|
||||
name: tokenMetadata.name,
|
||||
priceUSDCents: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { COINBASE } from "../../../wallets/constants.js";
|
||||
import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js";
|
||||
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
|
||||
import type { CreateWalletArgs } from "../../../wallets/wallet-types.js";
|
||||
import type { EcosystemWalletId } from "../../../wallets/wallet-types.js";
|
||||
|
||||
export function usePreloadWalletProviders({
|
||||
client,
|
||||
wallets,
|
||||
}: { client: ThirdwebClient; wallets: Wallet[] }) {
|
||||
export function usePreloadWalletProviders({ wallets }: { wallets: Wallet[] }) {
|
||||
useQueries({
|
||||
queries: wallets
|
||||
.filter(
|
||||
@@ -29,49 +24,6 @@ export function usePreloadWalletProviders({
|
||||
// return _something_
|
||||
return true;
|
||||
}
|
||||
case "inApp" === w.id: {
|
||||
const [
|
||||
{ InAppWebConnector },
|
||||
{ getOrCreateInAppWalletConnector },
|
||||
] = await Promise.all([
|
||||
import("../../../wallets/in-app/web/lib/web-connector.js"),
|
||||
import("../../../wallets/in-app/core/wallet/in-app-core.js"),
|
||||
]);
|
||||
await getOrCreateInAppWalletConnector(client, async (client) => {
|
||||
return new InAppWebConnector({
|
||||
client,
|
||||
});
|
||||
});
|
||||
// return _something_
|
||||
return true;
|
||||
}
|
||||
case isEcosystemWallet(w.id): {
|
||||
const [
|
||||
{ InAppWebConnector },
|
||||
{ getOrCreateInAppWalletConnector },
|
||||
] = await Promise.all([
|
||||
import("../../../wallets/in-app/web/lib/web-connector.js"),
|
||||
import("../../../wallets/in-app/core/wallet/in-app-core.js"),
|
||||
]);
|
||||
const ecosystemWallet = w as Wallet<EcosystemWalletId>; // we know this is an ecosystem wallet
|
||||
await getOrCreateInAppWalletConnector(
|
||||
client,
|
||||
async (client) => {
|
||||
return new InAppWebConnector({
|
||||
client,
|
||||
ecosystem: {
|
||||
id: ecosystemWallet.id,
|
||||
partnerId: ecosystemWallet.getConfig()?.partnerId,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
id: ecosystemWallet.id,
|
||||
partnerId: ecosystemWallet.getConfig()?.partnerId,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// potentially add more wallets here
|
||||
default: {
|
||||
return false;
|
||||
|
||||
@@ -276,7 +276,7 @@ export function OTPLoginUI(props: {
|
||||
|
||||
{!isWideModal && <Line />}
|
||||
|
||||
<Container p={isWideModal ? undefined : "lg"}>
|
||||
<Container p={isWideModal ? undefined : "lg"} gap="xs">
|
||||
{accountStatus === "error" && (
|
||||
<Text size="sm" center color="danger">
|
||||
{locale.emailLoginScreen.failedToSendCode}
|
||||
@@ -297,7 +297,7 @@ export function OTPLoginUI(props: {
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{accountStatus === "sent" && (
|
||||
{accountStatus !== "sending" && (
|
||||
<LinkButton onClick={sendEmailOrSms} type="button">
|
||||
{locale.emailLoginScreen.resendCode}
|
||||
</LinkButton>
|
||||
|
||||
@@ -74,16 +74,12 @@ describe.runIf(process.env.TW_SECRET_KEY)("prepareengineTransaction", () => {
|
||||
const { message, messageType, signature } = result;
|
||||
|
||||
expect(isHex(signature)).toBe(true);
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
|
||||
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"gas": 64340n,
|
||||
"nonce": 0n,
|
||||
"to": "${erc20Contract.address}",
|
||||
"value": 0n,
|
||||
}
|
||||
`);
|
||||
expect(message.data).toBe(
|
||||
"0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
|
||||
);
|
||||
expect(message.from).toBe(TEST_ACCOUNT_A.address);
|
||||
expect(message.to).toBe(erc20Contract.address);
|
||||
expect(message.value).toBe(0n);
|
||||
expect(messageType).toMatchInlineSnapshot(`"forward"`);
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user