Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
-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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
<Details summary="What cryptocurrencies does Pay support?">
|
||||
Pay can support any currency on our [supported chains](/connect/pay/supported-chains).
|
||||
</Details>
|
||||
|
||||
### How are cryptocurrency prices set in thirdweb Pay?
|
||||
|
||||
<Details summary="How are cryptocurrency prices set in thirdweb Pay?">
|
||||
Prices are set depending on the liquidity provider used to route your transaction.
|
||||
</Details>
|
||||
|
||||
### Does thirdweb Pay offer fiat payouts?
|
||||
|
||||
<Details summary="Does thirdweb Pay offer fiat payouts?">
|
||||
Fiat payouts are not currently available.
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ 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
|
||||
|
||||
@@ -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 chain. 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 > Pay 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
|
||||
|
||||
|
||||
@@ -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: "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`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ 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,19 @@
|
||||
# thirdweb
|
||||
|
||||
## 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.3",
|
||||
"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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
+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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"`);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,16 +72,12 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
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.from).toBe(TEST_ACCOUNT_A.address);
|
||||
expect(message.to).toBe(erc20Contract.address);
|
||||
expect(message.value).toBe(0n);
|
||||
expect(message.data).toBe(
|
||||
"0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
|
||||
);
|
||||
expect(messageType).toMatchInlineSnapshot(`"forward"`);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -27,7 +27,8 @@ export async function isZkSyncChain(chain: Chain) {
|
||||
chain.id === 2741 ||
|
||||
chain.id === 240 ||
|
||||
chain.id === 555271 ||
|
||||
chain.id === 61166
|
||||
chain.id === 61166 ||
|
||||
chain.id === 555272
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,54 +20,57 @@ import { getContract } from "../../contract/contract.js";
|
||||
import { deployContract } from "../../contract/deployment/deploy-with-abi.js";
|
||||
import { resolveImplementation } from "./resolveImplementation.js";
|
||||
|
||||
describe("Resolve implementation", async () => {
|
||||
it("should extract implementation address for minimal proxy contract", async () => {
|
||||
const resolved = resolveImplementation(NFT_DROP_CONTRACT);
|
||||
expect((await resolved).address).to.equal(NFT_DROP_IMPLEMENTATION);
|
||||
});
|
||||
|
||||
it("should extract implementation address for matic proxy contract", async () => {
|
||||
const resolved = resolveImplementation(POLYGON_USDT_PROXY_CONTRACT);
|
||||
expect((await resolved).address).to.equal(
|
||||
POLYGON_USDT_IMPLEMENTATION.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should extract implementation address for base USDC proxy contract", async () => {
|
||||
const resolved = resolveImplementation(BASE_USDC_PROXY_CONTRACT);
|
||||
expect((await resolved).address).to.equal(
|
||||
BASE_USDC_IMPLEMENTATION.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should extract implementation address for ERC1967 proxy contract", async () => {
|
||||
const implementationAddress = await deployContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account: TEST_ACCOUNT_A,
|
||||
bytecode: DUMMY_BYTECODE,
|
||||
abi: [],
|
||||
describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
"Resolve implementation",
|
||||
async () => {
|
||||
it("should extract implementation address for minimal proxy contract", async () => {
|
||||
const resolved = resolveImplementation(NFT_DROP_CONTRACT);
|
||||
expect((await resolved).address).to.equal(NFT_DROP_IMPLEMENTATION);
|
||||
});
|
||||
|
||||
const proxyAddress = await deployContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account: TEST_ACCOUNT_A,
|
||||
bytecode: ERC1967_PROXY_BYTECODE,
|
||||
abi: ERC1967_PROXY_CONSTRUCTOR_ABI as Abi,
|
||||
constructorParams: {
|
||||
logic: implementationAddress,
|
||||
data: "0x",
|
||||
},
|
||||
it("should extract implementation address for matic proxy contract", async () => {
|
||||
const resolved = resolveImplementation(POLYGON_USDT_PROXY_CONTRACT);
|
||||
expect((await resolved).address).to.equal(
|
||||
POLYGON_USDT_IMPLEMENTATION.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
const proxy = getContract({
|
||||
chain: ANVIL_CHAIN,
|
||||
address: proxyAddress,
|
||||
client: TEST_CLIENT,
|
||||
it("should extract implementation address for base USDC proxy contract", async () => {
|
||||
const resolved = resolveImplementation(BASE_USDC_PROXY_CONTRACT);
|
||||
expect((await resolved).address).to.equal(
|
||||
BASE_USDC_IMPLEMENTATION.toLowerCase(),
|
||||
);
|
||||
});
|
||||
|
||||
const resolved = await resolveImplementation(proxy);
|
||||
expect(resolved.address).to.equal(implementationAddress);
|
||||
});
|
||||
});
|
||||
it("should extract implementation address for ERC1967 proxy contract", async () => {
|
||||
const implementationAddress = await deployContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account: TEST_ACCOUNT_A,
|
||||
bytecode: DUMMY_BYTECODE,
|
||||
abi: [],
|
||||
});
|
||||
|
||||
const proxyAddress = await deployContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account: TEST_ACCOUNT_A,
|
||||
bytecode: ERC1967_PROXY_BYTECODE,
|
||||
abi: ERC1967_PROXY_CONSTRUCTOR_ABI as Abi,
|
||||
constructorParams: {
|
||||
logic: implementationAddress,
|
||||
data: "0x",
|
||||
},
|
||||
});
|
||||
|
||||
const proxy = getContract({
|
||||
chain: ANVIL_CHAIN,
|
||||
address: proxyAddress,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const resolved = await resolveImplementation(proxy);
|
||||
expect(resolved.address).to.equal(implementationAddress);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export async function resolvePromisedValue<V>(
|
||||
value: V,
|
||||
): Promise<V extends () => Promise<infer R> ? R : V> {
|
||||
// @ts-expect-error - this works fine, but TS doesn't like it since 5.8
|
||||
return typeof value === "function" ? await value() : value;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ import type { ClientScopedStorage } from "../authentication/client-scoped-storag
|
||||
|
||||
export async function signMessage({
|
||||
client,
|
||||
payload: { message, isRaw },
|
||||
payload: { message, isRaw, originalMessage, chainId },
|
||||
storage,
|
||||
}: {
|
||||
client: ThirdwebClient;
|
||||
payload: {
|
||||
message: string;
|
||||
isRaw: boolean;
|
||||
originalMessage?: string;
|
||||
chainId?: number;
|
||||
};
|
||||
storage: ClientScopedStorage;
|
||||
}) {
|
||||
@@ -37,6 +39,8 @@ export async function signMessage({
|
||||
messagePayload: {
|
||||
message,
|
||||
isRaw,
|
||||
originalMessage,
|
||||
chainId,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -217,10 +217,10 @@ export class EnclaveWallet implements IWebWallet {
|
||||
|
||||
return { transactionHash };
|
||||
},
|
||||
async signMessage({ message }) {
|
||||
async signMessage({ message, originalMessage, chainId }) {
|
||||
const messagePayload = (() => {
|
||||
if (typeof message === "string") {
|
||||
return { message, isRaw: false };
|
||||
return { message, isRaw: false, originalMessage, chainId };
|
||||
}
|
||||
return {
|
||||
message:
|
||||
@@ -228,6 +228,8 @@ export class EnclaveWallet implements IWebWallet {
|
||||
? message.raw
|
||||
: bytesToHex(message.raw),
|
||||
isRaw: true,
|
||||
originalMessage,
|
||||
chainId,
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -182,7 +182,15 @@ export type Account = {
|
||||
* const signature = await account.signMessage({ message: 'hello!' });
|
||||
* ```
|
||||
*/
|
||||
signMessage: ({ message }: { message: SignableMessage }) => Promise<Hex>;
|
||||
signMessage: ({
|
||||
message,
|
||||
originalMessage,
|
||||
chainId,
|
||||
}: {
|
||||
message: SignableMessage;
|
||||
originalMessage?: string;
|
||||
chainId?: number;
|
||||
}) => Promise<Hex>;
|
||||
/**
|
||||
* Sign the given typed data and return the signature
|
||||
* @example
|
||||
|
||||
@@ -179,6 +179,7 @@ export async function estimateUserOpGasCost(args: {
|
||||
client: args.client,
|
||||
}),
|
||||
),
|
||||
waitForDeployment: false,
|
||||
});
|
||||
|
||||
let gasLimit = 0n;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { maxUint96 } from "ox/Solidity";
|
||||
import { concat, keccak256, toHex } from "viem";
|
||||
import { concat } from "viem";
|
||||
import type { Chain } from "../../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import {
|
||||
@@ -16,9 +16,11 @@ import type { PreparedTransaction } from "../../../transaction/prepare-transacti
|
||||
import type { TransactionReceipt } from "../../../transaction/types.js";
|
||||
import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js";
|
||||
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
|
||||
import type { Hex } from "../../../utils/encoding/hex.js";
|
||||
import { type Hex, toHex } from "../../../utils/encoding/hex.js";
|
||||
import { hexToBytes } from "../../../utils/encoding/to-bytes.js";
|
||||
import { isThirdwebUrl } from "../../../utils/fetch.js";
|
||||
import { keccak256 } from "../../../utils/hashing/keccak256.js";
|
||||
import { stringify } from "../../../utils/json.js";
|
||||
import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js";
|
||||
import type { Account } from "../../interfaces/wallet.js";
|
||||
import type {
|
||||
@@ -321,7 +323,9 @@ async function populateUserOp_v0_7(args: {
|
||||
createAccountOverride: overrides?.createAccount,
|
||||
}),
|
||||
);
|
||||
markAccountDeploying(accountContract);
|
||||
if (waitForDeployment) {
|
||||
markAccountDeploying(accountContract);
|
||||
}
|
||||
}
|
||||
|
||||
const partialOp: UserOperationV07 = {
|
||||
@@ -481,7 +485,9 @@ async function populateUserOp_v0_6(args: {
|
||||
accountSalt: overrides?.accountSalt,
|
||||
createAccountOverride: overrides?.createAccount,
|
||||
});
|
||||
markAccountDeploying(accountContract);
|
||||
if (waitForDeployment) {
|
||||
markAccountDeploying(accountContract);
|
||||
}
|
||||
}
|
||||
|
||||
const partialOp: UserOperationV06 = {
|
||||
@@ -593,6 +599,54 @@ export async function signUserOp(args: {
|
||||
}): Promise<UserOperationV06 | UserOperationV07> {
|
||||
const { userOp, chain, entrypointAddress, adminAccount } = args;
|
||||
|
||||
const userOpHash = await getUserOpHash({
|
||||
client: args.client,
|
||||
userOp,
|
||||
chain,
|
||||
entrypointAddress,
|
||||
});
|
||||
|
||||
if (adminAccount.signMessage) {
|
||||
const signature = await adminAccount.signMessage({
|
||||
message: {
|
||||
raw: hexToBytes(userOpHash),
|
||||
},
|
||||
originalMessage: stringify(userOp),
|
||||
chainId: chain.id,
|
||||
});
|
||||
return {
|
||||
...userOp,
|
||||
signature,
|
||||
};
|
||||
}
|
||||
throw new Error("signMessage not implemented in signingAccount");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of a user operation.
|
||||
* @param args - The options for getting the user operation hash
|
||||
* @returns - The user operation hash
|
||||
* @example
|
||||
* ```ts
|
||||
* import { getUserOpHash } from "thirdweb/wallets/smart";
|
||||
*
|
||||
* const userOp = await createUnsignedUserOp(...);
|
||||
* const userOpHash = await getUserOpHash({
|
||||
* client,
|
||||
* userOp,
|
||||
* chain,
|
||||
* });
|
||||
* ```
|
||||
* @walletUtils
|
||||
*/
|
||||
export async function getUserOpHash(args: {
|
||||
client: ThirdwebClient;
|
||||
userOp: UserOperationV06 | UserOperationV07;
|
||||
chain: Chain;
|
||||
entrypointAddress?: string;
|
||||
}): Promise<Hex> {
|
||||
const { userOp, chain, entrypointAddress } = args;
|
||||
|
||||
const entrypointVersion = getEntryPointVersion(
|
||||
entrypointAddress || ENTRYPOINT_ADDRESS_v0_6,
|
||||
);
|
||||
@@ -619,19 +673,7 @@ export async function signUserOp(args: {
|
||||
userOp: userOp as UserOperationV06,
|
||||
});
|
||||
}
|
||||
|
||||
if (adminAccount.signMessage) {
|
||||
const signature = await adminAccount.signMessage({
|
||||
message: {
|
||||
raw: hexToBytes(userOpHash),
|
||||
},
|
||||
});
|
||||
return {
|
||||
...userOp,
|
||||
signature,
|
||||
};
|
||||
}
|
||||
throw new Error("signMessage not implemented in signingAccount");
|
||||
return userOpHash;
|
||||
}
|
||||
|
||||
async function getAccountInitCode(options: {
|
||||
|
||||
@@ -49,9 +49,10 @@ const contract = getContract({
|
||||
address: "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8",
|
||||
});
|
||||
|
||||
describe.sequential(
|
||||
describe.runIf(process.env.TW_SECRET_KEY).sequential(
|
||||
"SmartWallet 0.7 core tests",
|
||||
{
|
||||
retry: 0,
|
||||
timeout: 240_000,
|
||||
},
|
||||
() => {
|
||||
@@ -200,7 +201,7 @@ describe.sequential(
|
||||
});
|
||||
|
||||
await expect(tx).rejects.toMatchInlineSnapshot(`
|
||||
[TransactionError: Error - Not authorized
|
||||
[TransactionError: Execution Reverted: {"code":3,"message":"execution reverted: Not authorized"}
|
||||
|
||||
contract: ${contract.address}
|
||||
chainId: 11155111]
|
||||
|
||||
@@ -52,6 +52,7 @@ const factoryAddress = "0x564cf6453a1b0FF8DB603E92EA4BbD410dea45F3"; // pre 712
|
||||
describe.runIf(process.env.TW_SECRET_KEY).sequential(
|
||||
"SmartWallet core tests",
|
||||
{
|
||||
retry: 0,
|
||||
timeout: 240_000,
|
||||
},
|
||||
() => {
|
||||
|
||||
@@ -28,7 +28,7 @@ const client = TEST_CLIENT;
|
||||
const DEFAULT_FACTORY_ADDRESS = "0xB1846E893CA01c5Dcdaa40371C1e13f2e0Df5717";
|
||||
const DEFAULT_VALIDATOR_ADDRESS = "0x7D3631d823e0De311DC86f580946EeF2eEC81fba";
|
||||
|
||||
describe.sequential(
|
||||
describe.runIf(process.env.TW_SECRET_KEY).sequential(
|
||||
"SmartWallet modular tests",
|
||||
{
|
||||
retry: 0,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.27
|
||||
|
||||
## 0.2.26
|
||||
|
||||
## 0.2.25
|
||||
|
||||
## 0.2.24
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/wagmi-adapter",
|
||||
"version": "0.2.25",
|
||||
"version": "0.2.27",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
Generated
+2132
-2059
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user