Skip to content

Commit 744b7d3

Browse files
committed
self-serve infra deployment
1 parent 9f88b5c commit 744b7d3

File tree

54 files changed

+1348
-228
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1348
-228
lines changed

apps/dashboard/src/@/actions/billing.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use server";
2+
import "server-only";
23

34
import { getAuthToken } from "@/api/auth-token";
45
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
6+
import type { ChainInfraSKU } from "@/types/billing";
7+
import { getAbsoluteUrl } from "@/utils/vercel";
58

69
export async function reSubscribePlan(options: {
710
teamId: string;
@@ -35,3 +38,77 @@ export async function reSubscribePlan(options: {
3538
status: 200,
3639
};
3740
}
41+
42+
export async function getChainInfraCheckoutURL(options: {
43+
teamSlug: string;
44+
skus: ChainInfraSKU[];
45+
chainId: number;
46+
annual: boolean;
47+
}) {
48+
const token = await getAuthToken();
49+
50+
if (!token) {
51+
return {
52+
error: "You are not logged in",
53+
status: "error",
54+
} as const;
55+
}
56+
57+
const redirectTo = new URL(getAbsoluteUrl());
58+
redirectTo.pathname = `/team/${options.teamSlug}/chain-infra/${options.chainId}`;
59+
60+
const res = await fetch(
61+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamSlug}/checkout/create-link`,
62+
{
63+
body: JSON.stringify({
64+
annual: options.annual,
65+
chainId: options.chainId,
66+
redirectTo: redirectTo.toString(),
67+
skus: options.skus,
68+
}),
69+
headers: {
70+
Authorization: `Bearer ${token}`,
71+
"Content-Type": "application/json",
72+
},
73+
method: "POST",
74+
},
75+
);
76+
if (!res.ok) {
77+
const text = await res.text();
78+
console.error("Failed to create checkout link", text, res.status);
79+
switch (res.status) {
80+
case 402: {
81+
return {
82+
error:
83+
"You have outstanding invoices, please pay these first before re-subscribing.",
84+
status: "error",
85+
} as const;
86+
}
87+
case 429: {
88+
return {
89+
error: "Too many requests, please try again later.",
90+
status: "error",
91+
} as const;
92+
}
93+
default: {
94+
return {
95+
error: "An unknown error occurred, please try again later.",
96+
status: "error",
97+
} as const;
98+
}
99+
}
100+
}
101+
102+
const json = await res.json();
103+
if (!json.result) {
104+
return {
105+
error: "An unknown error occurred, please try again later.",
106+
status: "error",
107+
} as const;
108+
}
109+
110+
return {
111+
data: json.result as string,
112+
status: "success",
113+
} as const;
114+
}
Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
1-
export const authOptions = [
2-
"email",
3-
"phone",
4-
"passkey",
5-
"siwe",
6-
"guest",
7-
"google",
8-
"facebook",
9-
"x",
10-
"discord",
11-
"farcaster",
12-
"telegram",
13-
"github",
14-
"twitch",
15-
"steam",
16-
"apple",
17-
"coinbase",
18-
"line",
19-
] as const;
1+
import "server-only";
2+
3+
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
4+
import { getAuthToken } from "./auth-token";
5+
6+
export type AuthOption =
7+
| "email"
8+
| "phone"
9+
| "passkey"
10+
| "siwe"
11+
| "guest"
12+
| "google"
13+
| "facebook"
14+
| "x"
15+
| "discord"
16+
| "farcaster"
17+
| "telegram"
18+
| "github"
19+
| "twitch"
20+
| "steam"
21+
| "apple"
22+
| "coinbase"
23+
| "line";
2024

2125
export type Ecosystem = {
2226
name: string;
2327
imageUrl?: string;
2428
id: string;
2529
slug: string;
2630
permission: "PARTNER_WHITELIST" | "ANYONE";
27-
authOptions: (typeof authOptions)[number][];
31+
authOptions: AuthOption[];
2832
customAuthOptions?: {
2933
authEndpoint?: {
3034
url: string;
@@ -47,6 +51,54 @@ export type Ecosystem = {
4751
updatedAt: string;
4852
};
4953

54+
export async function fetchEcosystemList(teamIdOrSlug: string) {
55+
const token = await getAuthToken();
56+
57+
if (!token) {
58+
return [];
59+
}
60+
61+
const res = await fetch(
62+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
63+
{
64+
headers: {
65+
Authorization: `Bearer ${token}`,
66+
},
67+
},
68+
);
69+
70+
if (!res.ok) {
71+
return [];
72+
}
73+
74+
return (await res.json()).result as Ecosystem[];
75+
}
76+
77+
export async function fetchEcosystem(slug: string, teamIdOrSlug: string) {
78+
const token = await getAuthToken();
79+
80+
if (!token) {
81+
return null;
82+
}
83+
84+
const res = await fetch(
85+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
86+
{
87+
headers: {
88+
Authorization: `Bearer ${token}`,
89+
},
90+
},
91+
);
92+
if (!res.ok) {
93+
const data = await res.json();
94+
console.error(data);
95+
return null;
96+
}
97+
98+
const data = (await res.json()) as { result: Ecosystem };
99+
return data.result;
100+
}
101+
50102
type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
51103
export type Partner = {
52104
id: string;

apps/dashboard/src/@/api/team-subscription.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getAuthToken } from "@/api/auth-token";
22
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
3-
import type { ProductSKU } from "@/types/billing";
3+
import type { ChainInfraSKU, ProductSKU } from "@/types/billing";
44

55
type InvoiceLine = {
66
// amount for this line item
@@ -22,7 +22,7 @@ type Invoice = {
2222

2323
export type TeamSubscription = {
2424
id: string;
25-
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT";
25+
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT" | "CHAIN";
2626
status:
2727
| "incomplete"
2828
| "incomplete_expired"
@@ -37,6 +37,13 @@ export type TeamSubscription = {
3737
trialStart: string | null;
3838
trialEnd: string | null;
3939
upcomingInvoice: Invoice;
40+
skus: (ProductSKU | ChainInfraSKU)[];
41+
};
42+
43+
type ChainTeamSubscription = Omit<TeamSubscription, "skus"> & {
44+
chainId: string;
45+
skus: ChainInfraSKU[];
46+
isLegacy: boolean;
4047
};
4148

4249
export async function getTeamSubscriptions(slug: string) {
@@ -60,3 +67,60 @@ export async function getTeamSubscriptions(slug: string) {
6067
}
6168
return null;
6269
}
70+
71+
const CHAIN_PLAN_TO_INFRA = {
72+
"chain:plan:gold": ["chain:infra:rpc", "chain:infra:account_abstraction"],
73+
"chain:plan:platinum": [
74+
"chain:infra:rpc",
75+
"chain:infra:insight",
76+
"chain:infra:account_abstraction",
77+
],
78+
"chain:plan:ultimate": [
79+
"chain:infra:rpc",
80+
"chain:infra:insight",
81+
"chain:infra:account_abstraction",
82+
],
83+
};
84+
85+
export async function getChainSubscriptions(slug: string) {
86+
const allSubscriptions = await getTeamSubscriptions(slug);
87+
if (!allSubscriptions) {
88+
return null;
89+
}
90+
91+
// first replace any sku that MIGHT match a chain plan
92+
const updatedSubscriptions = allSubscriptions
93+
.filter((s) => s.type === "CHAIN")
94+
.map((s) => {
95+
const skus = s.skus;
96+
const updatedSkus = skus.flatMap((sku) => {
97+
const plan =
98+
CHAIN_PLAN_TO_INFRA[sku as keyof typeof CHAIN_PLAN_TO_INFRA];
99+
return plan ? plan : sku;
100+
});
101+
return {
102+
...s,
103+
skus: updatedSkus,
104+
};
105+
});
106+
107+
return updatedSubscriptions.filter(
108+
(s): s is ChainTeamSubscription =>
109+
"chainId" in s && typeof s.chainId === "string",
110+
);
111+
}
112+
113+
export async function getChainSubscriptionForChain(
114+
slug: string,
115+
chainId: number,
116+
) {
117+
const chainSubscriptions = await getChainSubscriptions(slug);
118+
119+
if (!chainSubscriptions) {
120+
return null;
121+
}
122+
123+
return (
124+
chainSubscriptions.find((s) => s.chainId === chainId.toString()) ?? null
125+
);
126+
}

apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: {
152152
disableChainId?: boolean;
153153
align?: "center" | "start" | "end";
154154
disableTestnets?: boolean;
155+
disableDeprecated?: boolean;
155156
placeholder?: string;
156157
client: ThirdwebClient;
157158
}) {
@@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: {
169170
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
170171
}
171172

173+
if (props.disableDeprecated) {
174+
chains = chains.filter((chain) => chain.status !== "deprecated");
175+
}
176+
172177
return chains;
173-
}, [allChains, props.chainIds, props.disableTestnets]);
178+
}, [
179+
allChains,
180+
props.chainIds,
181+
props.disableTestnets,
182+
props.disableDeprecated,
183+
]);
174184

175185
const options = useMemo(() => {
176186
return chainsToShow.map((chain) => {

0 commit comments

Comments
 (0)