Skip to content

Commit ae2e3de

Browse files
committed
self-serve infra deployment
1 parent 45bb790 commit ae2e3de

File tree

26 files changed

+755
-38
lines changed

26 files changed

+755
-38
lines changed

apps/dashboard/biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
33
"extends": "//"
44
}

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) => {

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

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@ const ACCENT = {
3838
type UpsellBannerCardProps = {
3939
title: React.ReactNode;
4040
description: React.ReactNode;
41-
cta: {
42-
text: React.ReactNode;
43-
icon?: React.ReactNode;
44-
target?: "_blank";
45-
link: string;
46-
};
41+
cta?:
42+
| {
43+
text: React.ReactNode;
44+
icon?: React.ReactNode;
45+
target?: "_blank";
46+
link: string;
47+
}
48+
| {
49+
text: React.ReactNode;
50+
icon?: React.ReactNode;
51+
onClick: () => void;
52+
};
4753
accentColor?: keyof typeof ACCENT;
4854
icon?: React.ReactNode;
4955
};
@@ -93,25 +99,41 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
9399
</div>
94100
</div>
95101

96-
<Button
97-
asChild
98-
className={cn(
99-
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
100-
color.btn,
101-
)}
102-
size="sm"
103-
>
104-
<Link
105-
href={props.cta.link}
106-
rel={
107-
props.cta.target === "_blank" ? "noopener noreferrer" : undefined
108-
}
109-
target={props.cta.target}
102+
{props.cta && "target" in props.cta ? (
103+
<Button
104+
asChild
105+
className={cn(
106+
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
107+
color.btn,
108+
)}
109+
size="sm"
110+
>
111+
<Link
112+
href={props.cta.link}
113+
rel={
114+
props.cta.target === "_blank"
115+
? "noopener noreferrer"
116+
: undefined
117+
}
118+
target={props.cta.target}
119+
>
120+
{props.cta.text}
121+
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
122+
</Link>
123+
</Button>
124+
) : props.cta && "onClick" in props.cta ? (
125+
<Button
126+
className={cn(
127+
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
128+
color.btn,
129+
)}
130+
onClick={props.cta.onClick}
131+
size="sm"
110132
>
111133
{props.cta.text}
112134
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
113-
</Link>
114-
</Button>
135+
</Button>
136+
) : null}
115137
</div>
116138
</div>
117139
);

apps/dashboard/src/@/icons/ChainIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const ChainIconClient = ({
3030
fallback={<img alt="" src={fallbackChainIcon} />}
3131
key={resolvedSrc}
3232
loading={restProps.loading || "lazy"}
33-
skeleton={<div className="animate-pulse rounded-full bg-border" />}
33+
skeleton={<span className="animate-pulse rounded-full bg-border" />}
3434
src={resolvedSrc}
3535
/>
3636
);

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getTeamBySlug, getTeams } from "@/api/team";
66
import { CustomChatButton } from "@/components/chat/CustomChatButton";
77
import { AppFooter } from "@/components/footers/app-footer";
88
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
9+
import { Badge } from "@/components/ui/badge";
910
import { Button } from "@/components/ui/button";
1011
import { TabPathLinks } from "@/components/ui/tabs";
1112
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
@@ -86,6 +87,14 @@ export default async function TeamLayout(props: {
8687
name: "Ecosystems",
8788
path: `/team/${params.team_slug}/~/ecosystem`,
8889
},
90+
{
91+
name: (
92+
<>
93+
Scale <Badge className="ml-2">New</Badge>
94+
</>
95+
),
96+
path: `/team/${params.team_slug}/~/scale`,
97+
},
8998
{
9099
name: "Usage",
91100
path: `/team/${params.team_slug}/~/usage`,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getChain } from "../../../../../../(dashboard)/(chain)/utils";
2+
3+
export default async function InfrastructurePage(props: {
4+
params: Promise<{
5+
team_slug: string;
6+
chain_id: string;
7+
}>;
8+
}) {
9+
const params = await props.params;
10+
const chain = await getChain(params.chain_id);
11+
return <div>Infrastructure for: {chain.name}</div>;
12+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"use client";
2+
3+
import { DatabaseIcon, NetworkIcon, ShieldIcon } from "lucide-react";
4+
import { useMemo, useState } from "react";
5+
import type { ThirdwebClient } from "thirdweb";
6+
import { ClientOnly } from "@/components/blocks/client-only";
7+
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
8+
import {
9+
Card,
10+
CardContent,
11+
CardDescription,
12+
CardHeader,
13+
CardTitle,
14+
} from "@/components/ui/card";
15+
import { useAllChainsData } from "@/hooks/chains/allChains";
16+
import { Badge } from "../../../../../../../../../@/components/ui/badge";
17+
import { ChainIconClient } from "../../../../../../../../../@/icons/ChainIcon";
18+
import { OrderSummary } from "./order-summary";
19+
import {
20+
type PaymentFrequency,
21+
PaymentFrequencySelector,
22+
} from "./payment-frequency-selector";
23+
import { type ServiceConfig, ServiceSelector } from "./service-selector";
24+
25+
const services = {
26+
accountAbstraction: {
27+
description: "Smart wallets & gasless transactions",
28+
features: [
29+
"Fully managed Bundler & Paymaster",
30+
"Audited ERC-4337 smart wallets out of the box",
31+
"ERC-7702 support",
32+
"Session key support",
33+
],
34+
icon: ShieldIcon,
35+
id: "account-abstraction" as const,
36+
monthlyPrice: 750,
37+
name: "Account Abstraction",
38+
upsellReason: " ",
39+
},
40+
insight: {
41+
description: "Instant, real-time data APIs, without the hassle",
42+
features: [
43+
"Comprehensive onchain data APIs",
44+
"Webhooks for real-time event streaming",
45+
"Instant wallet, token & NFT balance lookups",
46+
"Fully managed and battle-tested",
47+
],
48+
icon: DatabaseIcon,
49+
id: "insight" as const,
50+
monthlyPrice: 2000,
51+
name: "Insight",
52+
upsellReason: " ",
53+
},
54+
rpc: {
55+
description: "Low-latency edge RPC with no node maintenance",
56+
features: [
57+
"Low-latency edge RPC",
58+
"Auto-scaling & global load balancing",
59+
"Smart caching & automatic failover",
60+
"Fully managed and battle-tested",
61+
],
62+
icon: NetworkIcon,
63+
id: "rpc" as const,
64+
monthlyPrice: 2000,
65+
name: "RPC",
66+
required: true,
67+
upsellReason: " ",
68+
},
69+
} satisfies Record<string, ServiceConfig>;
70+
71+
const serviceConfigs = [
72+
services.rpc,
73+
services.insight,
74+
services.accountAbstraction,
75+
];
76+
77+
export function InfrastructureCheckout(props: { client: ThirdwebClient }) {
78+
const [selectedChain, setSelectedChain] = useState<number>(0);
79+
const { idToChain } = useAllChainsData();
80+
const [selectedServices, setSelectedServices] = useState<ServiceConfig[]>([
81+
services.rpc,
82+
]);
83+
84+
const selectedChainDetails = useMemo(() => {
85+
return idToChain.get(selectedChain);
86+
}, [idToChain, selectedChain]);
87+
88+
const [paymentFrequency, setPaymentFrequency] =
89+
useState<PaymentFrequency>("monthly");
90+
91+
return (
92+
<div className="grid lg:grid-cols-3 gap-8">
93+
{/* Configuration Section */}
94+
<div className="lg:col-span-2 space-y-6">
95+
{/* Chain Selection */}
96+
<Card>
97+
<CardHeader className="space-y-2">
98+
<CardTitle className="flex items-center gap-2">
99+
<Badge className="font-mono" variant="outline">
100+
Step 1
101+
</Badge>
102+
Select Chain
103+
</CardTitle>
104+
<CardDescription>
105+
Choose the chain to deploy infrastructure on.
106+
</CardDescription>
107+
</CardHeader>
108+
<CardContent>
109+
<ClientOnly ssr={false}>
110+
<SingleNetworkSelector
111+
chainId={selectedChain}
112+
className="bg-background"
113+
client={props.client}
114+
disableDeprecated
115+
onChange={setSelectedChain}
116+
placeholder="Select a chain"
117+
/>
118+
</ClientOnly>
119+
</CardContent>
120+
</Card>
121+
122+
{/* Service Selection */}
123+
<Card>
124+
<CardHeader className="space-y-2">
125+
<CardTitle className="flex items-center gap-2">
126+
<Badge className="font-mono" variant="outline">
127+
Step 2
128+
</Badge>
129+
Select Services
130+
</CardTitle>
131+
<CardDescription>
132+
Choose the infrastructure services you need. RPC service is
133+
required for all other services.
134+
</CardDescription>
135+
</CardHeader>
136+
<CardContent>
137+
<ServiceSelector
138+
selectedServices={selectedServices}
139+
services={serviceConfigs}
140+
setSelectedServices={setSelectedServices}
141+
/>
142+
</CardContent>
143+
</Card>
144+
145+
{/* Payment Frequency */}
146+
<Card>
147+
<CardHeader className="space-y-2">
148+
<CardTitle className="flex items-center gap-2">
149+
<Badge className="font-mono" variant="outline">
150+
Step 3
151+
</Badge>
152+
Payment Frequency
153+
</CardTitle>
154+
<CardDescription>
155+
Choose your billing frequency. Save 15% with annual payment.
156+
</CardDescription>
157+
</CardHeader>
158+
<CardContent>
159+
<PaymentFrequencySelector
160+
annualDiscountPercent={15}
161+
paymentFrequency={paymentFrequency}
162+
setPaymentFrequency={setPaymentFrequency}
163+
/>
164+
</CardContent>
165+
</Card>
166+
</div>
167+
168+
{/* Pricing Summary */}
169+
<div className="lg:col-span-1">
170+
<Card className="sticky top-8">
171+
<CardHeader className="space-y-2">
172+
<CardTitle>Order Summary</CardTitle>
173+
<CardDescription>
174+
{selectedChainDetails ? (
175+
<div className="flex justify-between gap-4">
176+
<span className="flex grow gap-2 truncate text-left">
177+
<ChainIconClient
178+
className="size-5"
179+
client={props.client}
180+
loading="lazy"
181+
src={selectedChainDetails.icon?.url}
182+
/>
183+
{selectedChainDetails.name}
184+
</span>
185+
<Badge className="gap-2 max-sm:hidden" variant="outline">
186+
<span className="text-muted-foreground">Chain ID</span>
187+
{selectedChainDetails.chainId}
188+
</Badge>
189+
</div>
190+
) : (
191+
<span className="text-muted-foreground">Select a chain</span>
192+
)}
193+
</CardDescription>
194+
</CardHeader>
195+
<CardContent>
196+
<OrderSummary
197+
paymentFrequency={paymentFrequency}
198+
selectedChainId={selectedChain}
199+
selectedServices={selectedServices}
200+
/>
201+
</CardContent>
202+
</Card>
203+
</div>
204+
</div>
205+
);
206+
}

0 commit comments

Comments
 (0)