Skip to content

Commit 6a48fde

Browse files
committed
feat: add basic UI for purchasing additional seats
We also switch growth to 4 seats by default. Note that extra seats can only be purchased as an add on to the team or growth plans. We keep the add purchase button in the team dialog for now, though the UI is subject to change.
1 parent 162a65e commit 6a48fde

2 files changed

Lines changed: 43 additions & 13 deletions

File tree

  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects
  • packages/stack-shared/src

apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -191,16 +191,21 @@ function TeamAddUserDialogContent(props: {
191191

192192
const users = props.team.useUsers();
193193
const admins = props.team.useItem("dashboard_admins");
194+
const products = props.team.useProducts();
195+
const hasPaidPlan = products.some(
196+
p => (p.id === "team" || p.id === "growth") && p.type === "subscription"
197+
);
194198

195199
const [email, setEmail] = useState("");
196200
const [formError, setFormError] = useState<string | null>(null);
197201

202+
const invitationsLoaded = invitations != null;
198203
const activeSeats = users.length + (invitations?.length ?? 0);
199204
const seatLimit = admins.quantity;
200-
const atCapacity = activeSeats >= seatLimit;
205+
const atCapacity = invitationsLoaded && activeSeats >= seatLimit;
201206

202207
const handleInvite = async () => {
203-
if (atCapacity) {
208+
if (!invitationsLoaded || atCapacity) {
204209
return;
205210
}
206211

@@ -221,6 +226,19 @@ function TeamAddUserDialogContent(props: {
221226
}
222227
};
223228

229+
const handleAddSeat = async () => {
230+
try {
231+
const checkoutUrl = await props.team.createCheckoutUrl({
232+
productId: "extra-seats",
233+
returnUrl: window.location.href,
234+
});
235+
window.location.assign(checkoutUrl);
236+
} catch (error) {
237+
const message = error instanceof Error ? error.message : "Unknown error";
238+
toast({ variant: "destructive", title: "Failed to start checkout", description: message });
239+
}
240+
};
241+
224242
const handleUpgrade = async () => {
225243
try {
226244
const checkoutUrl = await props.team.createCheckoutUrl({
@@ -231,21 +249,27 @@ function TeamAddUserDialogContent(props: {
231249
} catch (error) {
232250
const message = error instanceof Error ? error.message : "Unknown error";
233251
toast({ variant: "destructive", title: "Failed to start upgrade", description: message });
234-
};
252+
}
235253
};
236254

237255
return (
238256
<>
239257
<div className="space-y-4 py-2">
240258
<div className="flex items-center justify-between rounded-md border border-border px-3 py-2">
241259
<Typography type="label">Dashboard admin seats</Typography>
242-
<Typography variant="secondary">
243-
{activeSeats}/{seatLimit}
244-
</Typography>
260+
{invitationsLoaded ? (
261+
<Typography variant="secondary">
262+
{activeSeats}/{seatLimit}
263+
</Typography>
264+
) : (
265+
<Skeleton className="h-4 w-12" />
266+
)}
245267
</div>
246268
{atCapacity && (
247269
<Typography variant="secondary" className="text-destructive">
248-
You are at capacity. Upgrade your plan to add more admins.
270+
{hasPaidPlan
271+
? "You are at capacity. Add an extra seat for $29/month."
272+
: "You are at capacity. Upgrade your plan to add more admins."}
249273
</Typography>
250274
)}
251275
<div className="space-y-2">
@@ -259,7 +283,7 @@ function TeamAddUserDialogContent(props: {
259283
}}
260284
placeholder="Email"
261285
type="email"
262-
disabled={atCapacity}
286+
disabled={!invitationsLoaded || atCapacity}
263287
autoFocus
264288
/>
265289
{formError && (
@@ -308,11 +332,17 @@ function TeamAddUserDialogContent(props: {
308332
Close
309333
</Button>
310334
{atCapacity ? (
311-
<Button onClick={handleUpgrade} variant="default">
312-
Upgrade plan
313-
</Button>
335+
hasPaidPlan ? (
336+
<Button onClick={handleAddSeat} variant="default">
337+
Add seat ($29/mo)
338+
</Button>
339+
) : (
340+
<Button onClick={handleUpgrade} variant="default">
341+
Upgrade plan
342+
</Button>
343+
)
314344
) : (
315-
<Button onClick={handleInvite}>
345+
<Button onClick={handleInvite} disabled={!invitationsLoaded}>
316346
Invite
317347
</Button>
318348
)}

packages/stack-shared/src/plans.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const PLAN_LIMITS: {
5454
analyticsEvents: 500_000,
5555
},
5656
growth: {
57-
seats: UNLIMITED,
57+
seats: 4,
5858
authUsers: UNLIMITED,
5959
emailsPerMonth: 25_000,
6060
analyticsTimeoutSeconds: 300,

0 commit comments

Comments
 (0)