Skip to content

self-serve infra deployment: part 1 #7456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dashboard/biome.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"extends": "//"
}
12 changes: 11 additions & 1 deletion apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: {
disableChainId?: boolean;
align?: "center" | "start" | "end";
disableTestnets?: boolean;
disableDeprecated?: boolean;
placeholder?: string;
client: ThirdwebClient;
}) {
Expand All @@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: {
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
}

if (props.disableDeprecated) {
chains = chains.filter((chain) => chain.status !== "deprecated");
}

return chains;
}, [allChains, props.chainIds, props.disableTestnets]);
}, [
allChains,
props.chainIds,
props.disableTestnets,
props.disableDeprecated,
]);

const options = useMemo(() => {
return chainsToShow.map((chain) => {
Expand Down
66 changes: 44 additions & 22 deletions apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ const ACCENT = {
type UpsellBannerCardProps = {
title: React.ReactNode;
description: React.ReactNode;
cta: {
text: React.ReactNode;
icon?: React.ReactNode;
target?: "_blank";
link: string;
};
cta?:
| {
text: React.ReactNode;
icon?: React.ReactNode;
target?: "_blank";
link: string;
}
| {
text: React.ReactNode;
icon?: React.ReactNode;
onClick: () => void;
};
accentColor?: keyof typeof ACCENT;
icon?: React.ReactNode;
};
Expand Down Expand Up @@ -93,25 +99,41 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
</div>
</div>

<Button
asChild
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
size="sm"
>
<Link
href={props.cta.link}
rel={
props.cta.target === "_blank" ? "noopener noreferrer" : undefined
}
target={props.cta.target}
{props.cta && "target" in props.cta ? (
<Button
asChild
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
size="sm"
>
<Link
href={props.cta.link}
rel={
props.cta.target === "_blank"
? "noopener noreferrer"
: undefined
}
target={props.cta.target}
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</Link>
</Button>
) : props.cta && "onClick" in props.cta ? (
<Button
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
onClick={props.cta.onClick}
size="sm"
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</Link>
</Button>
</Button>
) : null}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/icons/ChainIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const ChainIconClient = ({
fallback={<img alt="" src={fallbackChainIcon} />}
key={resolvedSrc}
loading={restProps.loading || "lazy"}
skeleton={<div className="animate-pulse rounded-full bg-border" />}
skeleton={<span className="animate-pulse rounded-full bg-border" />}
src={resolvedSrc}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getTeamBySlug, getTeams } from "@/api/team";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
import { AppFooter } from "@/components/footers/app-footer";
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { TabPathLinks } from "@/components/ui/tabs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
Expand Down Expand Up @@ -86,6 +87,14 @@ export default async function TeamLayout(props: {
name: "Ecosystems",
path: `/team/${params.team_slug}/~/ecosystem`,
},
{
name: (
<>
Scale <Badge className="ml-2">New</Badge>
</>
),
path: `/team/${params.team_slug}/~/scale`,
},
{
name: "Usage",
path: `/team/${params.team_slug}/~/usage`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getChain } from "../../../../../../(dashboard)/(chain)/utils";

export default async function InfrastructurePage(props: {
params: Promise<{
team_slug: string;
chain_id: string;
}>;
}) {
const params = await props.params;
const chain = await getChain(params.chain_id);
return <div>Infrastructure for: {chain.name}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"use client";

import { DatabaseIcon, NetworkIcon, ShieldIcon } from "lucide-react";
import { useMemo, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { ClientOnly } from "@/components/blocks/client-only";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { Badge } from "../../../../../../../../../@/components/ui/badge";
import { ChainIconClient } from "../../../../../../../../../@/icons/ChainIcon";
import { OrderSummary } from "./order-summary";
import {
type PaymentFrequency,
PaymentFrequencySelector,
} from "./payment-frequency-selector";
import { type ServiceConfig, ServiceSelector } from "./service-selector";

const services = {
accountAbstraction: {
description: "Smart wallets & gasless transactions",
features: [
"Fully managed Bundler & Paymaster",
"Audited ERC-4337 smart wallets out of the box",
"ERC-7702 support",
"Session key support",
],
icon: ShieldIcon,
id: "account-abstraction" as const,
monthlyPrice: 750,
name: "Account Abstraction",
upsellReason: " ",
},
insight: {
description: "Instant, real-time data APIs, without the hassle",
features: [
"Comprehensive onchain data APIs",
"Webhooks for real-time event streaming",
"Instant wallet, token & NFT balance lookups",
"Fully managed and battle-tested",
],
icon: DatabaseIcon,
id: "insight" as const,
monthlyPrice: 2000,
name: "Insight",
upsellReason: " ",
},
rpc: {
description: "Low-latency edge RPC with no node maintenance",
features: [
"Low-latency edge RPC",
"Auto-scaling & global load balancing",
"Smart caching & automatic failover",
"Fully managed and battle-tested",
],
icon: NetworkIcon,
id: "rpc" as const,
monthlyPrice: 2000,
name: "RPC",
required: true,
upsellReason: " ",
},
} satisfies Record<string, ServiceConfig>;

const serviceConfigs = [
services.rpc,
services.insight,
services.accountAbstraction,
];

export function InfrastructureCheckout(props: { client: ThirdwebClient }) {
const [selectedChain, setSelectedChain] = useState<number>(0);
const { idToChain } = useAllChainsData();
const [selectedServices, setSelectedServices] = useState<ServiceConfig[]>([
services.rpc,
]);

const selectedChainDetails = useMemo(() => {
return idToChain.get(selectedChain);
}, [idToChain, selectedChain]);

const [paymentFrequency, setPaymentFrequency] =
useState<PaymentFrequency>("monthly");

return (
<div className="grid lg:grid-cols-3 gap-8">
{/* Configuration Section */}
<div className="lg:col-span-2 space-y-6">
{/* Chain Selection */}
<Card>
<CardHeader className="space-y-2">
<CardTitle className="flex items-center gap-2">
<Badge className="font-mono" variant="outline">
Step 1
</Badge>
Select Chain
</CardTitle>
<CardDescription>
Choose the chain to deploy infrastructure on.
</CardDescription>
</CardHeader>
<CardContent>
<ClientOnly ssr={false}>
<SingleNetworkSelector
chainId={selectedChain}
className="bg-background"
client={props.client}
disableDeprecated
onChange={setSelectedChain}
placeholder="Select a chain"
/>
</ClientOnly>
</CardContent>
</Card>

{/* Service Selection */}
<Card>
<CardHeader className="space-y-2">
<CardTitle className="flex items-center gap-2">
<Badge className="font-mono" variant="outline">
Step 2
</Badge>
Select Services
</CardTitle>
<CardDescription>
Choose the infrastructure services you need. RPC service is
required for all other services.
</CardDescription>
</CardHeader>
<CardContent>
<ServiceSelector
selectedServices={selectedServices}
services={serviceConfigs}
setSelectedServices={setSelectedServices}
/>
</CardContent>
</Card>

{/* Payment Frequency */}
<Card>
<CardHeader className="space-y-2">
<CardTitle className="flex items-center gap-2">
<Badge className="font-mono" variant="outline">
Step 3
</Badge>
Payment Frequency
</CardTitle>
<CardDescription>
Choose your billing frequency. Save 15% with annual payment.
</CardDescription>
</CardHeader>
<CardContent>
<PaymentFrequencySelector
annualDiscountPercent={15}
paymentFrequency={paymentFrequency}
setPaymentFrequency={setPaymentFrequency}
/>
</CardContent>
</Card>
</div>

{/* Pricing Summary */}
<div className="lg:col-span-1">
<Card className="sticky top-8">
<CardHeader className="space-y-2">
<CardTitle>Order Summary</CardTitle>
<CardDescription>
{selectedChainDetails ? (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIconClient
className="size-5"
client={props.client}
loading="lazy"
src={selectedChainDetails.icon?.url}
/>
{selectedChainDetails.name}
</span>
<Badge className="gap-2 max-sm:hidden" variant="outline">
<span className="text-muted-foreground">Chain ID</span>
{selectedChainDetails.chainId}
</Badge>
</div>
) : (
<span className="text-muted-foreground">Select a chain</span>
)}
</CardDescription>
</CardHeader>
<CardContent>
<OrderSummary
paymentFrequency={paymentFrequency}
selectedChainId={selectedChain}
selectedServices={selectedServices}
/>
</CardContent>
</Card>
</div>
</div>
);
}
Loading
Loading