diff --git a/examples/demo/src/app/payments-demo/api/config-check/route.ts b/examples/demo/src/app/payments-demo/api/config-check/route.ts index 9ec3b7bd4..6b1ec2cf3 100644 --- a/examples/demo/src/app/payments-demo/api/config-check/route.ts +++ b/examples/demo/src/app/payments-demo/api/config-check/route.ts @@ -1,5 +1,4 @@ import { branchConfigSchema, getConfigOverrideErrors } from "@hexclave/shared/dist/config/schema"; -import { ITEM_IDS, PLAN_LIMITS } from "@hexclave/shared/dist/plans"; import { NextResponse } from "next/server"; import { hexclaveServerApp } from "src/hexclave"; @@ -28,9 +27,9 @@ export async function GET() { expected: { freePrice: "0.00", freeInterval: [1, "month"], - freeEmailsPerMonth: PLAN_LIMITS.free.emailsPerMonth, - emailItemId: ITEM_IDS.emailsPerMonth, - emailsPerMonthRepeat: [1, "month"], + apiCallsItemId: "api_calls", + seatsItemId: "seats", + teamProSeats: 25, }, }); } diff --git a/examples/demo/src/app/payments-demo/api/create-checkout-url/route.ts b/examples/demo/src/app/payments-demo/api/create-checkout-url/route.ts index b73c2d22c..75b1a6514 100644 --- a/examples/demo/src/app/payments-demo/api/create-checkout-url/route.ts +++ b/examples/demo/src/app/payments-demo/api/create-checkout-url/route.ts @@ -5,7 +5,7 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -function readBody(value: unknown): { teamId: string, productId: "team" | "growth", returnUrl?: string } { +function readBody(value: unknown): { teamId: string, productId: "team_pro" | "extra_seats", returnUrl?: string } { if (!isRecord(value)) { throw new Error("Request body must be an object."); } @@ -14,8 +14,8 @@ function readBody(value: unknown): { teamId: string, productId: "team" | "growth if (typeof teamId !== "string" || teamId === "") { throw new Error("teamId is required."); } - if (productId !== "team" && productId !== "growth") { - throw new Error("productId must be team or growth."); + if (productId !== "team_pro" && productId !== "extra_seats") { + throw new Error("productId must be team_pro or extra_seats."); } if (returnUrl !== undefined && typeof returnUrl !== "string") { throw new Error("returnUrl must be a string."); diff --git a/examples/demo/src/app/payments-demo/page.tsx b/examples/demo/src/app/payments-demo/page.tsx index cbe75be5a..3528ac193 100644 --- a/examples/demo/src/app/payments-demo/page.tsx +++ b/examples/demo/src/app/payments-demo/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useStackApp, useUser } from "@hexclave/next"; -import { ITEM_IDS, PLAN_LIMITS, resolvePlanId } from "@hexclave/shared/dist/plans"; import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises"; import { Button, Card, CardContent, CardFooter, CardHeader, Input, Typography } from "@hexclave/ui"; import Link from "next/link"; @@ -38,7 +37,7 @@ async function readJson(response: Response): Promise { return value; } -async function createCheckoutUrl(options: { teamId: string, productId: "team" | "growth", returnUrl: string }): Promise { +async function createCheckoutUrl(options: { teamId: string, productId: "team_pro" | "extra_seats", returnUrl: string }): Promise { const response = await fetch("/payments-demo/api/create-checkout-url", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -51,10 +50,17 @@ async function createCheckoutUrl(options: { teamId: string, productId: "team" | return data.url; } +function resolveTeamPlan(products: ReadonlyArray<{ id: string | null, type?: string }>): string { + const activeSubscriptionIds = new Set( + products.filter((p) => p.type === "subscription" && p.id != null).map((p) => p.id), + ); + return activeSubscriptionIds.has("team_pro") ? "team_pro" : "none"; +} + function ProductList(props: { team: ReturnType>["useTeams"]>[number] }) { const products = props.team.useProducts(); - const emails = props.team.useItem(ITEM_IDS.emailsPerMonth); - const activePlan = resolvePlanId(products); + const seats = props.team.useItem("seats"); + const activePlan = resolveTeamPlan(products); return ( @@ -73,10 +79,9 @@ function ProductList(props: { team: ReturnType -
- - - +
+ +
@@ -118,8 +123,8 @@ function ProductList(props: { team: ReturnType - - + + ); @@ -136,7 +141,7 @@ function Metric(props: { label: string, value: string }) { function CheckoutButton(props: { team: ReturnType>["useTeams"]>[number], - productId: "team" | "growth", + productId: "team_pro" | "extra_seats", label: string, }) { const [loading, setLoading] = useState(false); @@ -186,7 +191,7 @@ export default function PaymentsDemoPage() { await user.setSelectedTeam(team); setResult({ label: "Created team", - detail: `${team.displayName} (${team.id}). Free plan should appear after the billing grant job/webhook path catches up.`, + detail: `${team.displayName} (${team.id}). The team starts with no products — buy Team Pro below to start a subscription.`, }); setTeamName(`Payments demo ${new Date().toISOString()}`); }; @@ -224,7 +229,7 @@ export default function PaymentsDemoPage() {
Payments Demo - Manual test surface for Hexclave internal team plans, Stripe checkout, subscription ending, and email quota deductions. + Manual test surface for the demo team plans (Team Pro + Extra Seats add-on), Stripe checkout, subscription ending, and seat item quantities.
@@ -244,7 +249,7 @@ export default function PaymentsDemoPage() {
setEmailCount(e.target.value)} inputMode="numeric" /> - + @@ -253,7 +258,7 @@ export default function PaymentsDemoPage() {
Project: {project.displayName} ({project.id})
Selected team: {user.selectedTeam?.displayName ?? "none"}
-
Manual Stripe end test: buy Team/Growth, end that customer subscription in Stripe Connect, wait for the webhook, then refresh here. The paid plan should disappear and Free should return.
+
Manual Stripe end test: buy Team Pro, end that customer subscription in Stripe Connect, wait for the webhook, then refresh here. The paid plan should disappear.
{result && (