From fdfe8e8e460becbf1faa0fa44c09b8ca3258fe1e Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 27 Jul 2026 15:26:34 +0100 Subject: [PATCH] fix: Fixed oidc purchase description --- .../app/(customer)/purchase/PurchaseForm.tsx | 363 ++++-------------- 1 file changed, 72 insertions(+), 291 deletions(-) diff --git a/adminsite/app/(customer)/purchase/PurchaseForm.tsx b/adminsite/app/(customer)/purchase/PurchaseForm.tsx index 48d0560..d01d3f7 100644 --- a/adminsite/app/(customer)/purchase/PurchaseForm.tsx +++ b/adminsite/app/(customer)/purchase/PurchaseForm.tsx @@ -4,17 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { - ApiError, - api, - lineItemsFor, - type CatalogueRow, - type CheckoutOptions, - type Deployment, - type Plan, - type Term, - type Tier, -} from "@/lib/api"; +import { ApiError, api, lineItemsFor, type CatalogueRow, type CheckoutOptions, type Deployment, type Plan, type Term, type Tier } from "@/lib/api"; import { initPaddle, previewPrices, type PricePreview } from "@/lib/paddle"; /* Tiers in the order a customer reads them, cheapest first. */ @@ -24,11 +14,11 @@ const TIER_ORDER: Tier[] = ["free", "professional", "enterprise"]; * one place the customer-facing wording lives. */ const FEATURE_LABEL: Record = { console: "Browser console", - sso: "Single sign-on", + oidc: "Single sign-on", }; const FEATURE_DESC: Record = { console: "In-browser SSH, RDP and VNC sessions", - sso: "OIDC sign-in for your whole team", + oidc: "OIDC sign-in for your whole team", }; function featureLabel(key: string) { return FEATURE_LABEL[key] ?? key; @@ -45,13 +35,7 @@ interface Choice { * absent. Drives both the tier cards and the configurator toggles. */ type FeatureState = "included" | "addon" | "absent"; -function featureStateFor( - plan: Plan | undefined, - rows: CatalogueRow[], - env: string, - term: Term, - key: string, -): FeatureState { +function featureStateFor(plan: Plan | undefined, rows: CatalogueRow[], env: string, term: Term, key: string): FeatureState { if (plan?.base_features.includes(key)) return "included"; const row = rows.find((r) => r.kind === "feature" && r.feature_key === key); const priced = Boolean(row?.price_ids?.[env]?.[term]); @@ -95,31 +79,16 @@ export function PurchaseForm() { return [...keys]; }, [options, dep]); - const activePlans = useMemo( - () => - (options?.plans ?? []) - .filter((p) => p.deployment === dep && p.active) - .sort((a, b) => TIER_ORDER.indexOf(a.tier) - TIER_ORDER.indexOf(b.tier)), - [options, dep], - ); + const activePlans = useMemo(() => (options?.plans ?? []).filter((p) => p.deployment === dep && p.active).sort((a, b) => TIER_ORDER.indexOf(a.tier) - TIER_ORDER.indexOf(b.tier)), [options, dep]); const plan = activePlans.find((p) => p.tier === choice.tier); const baseServers = plan?.base_limits.max_servers ?? 0; const unlimited = baseServers === -1; - const rows = useMemo( - () => - (options?.catalogue ?? []).filter( - (r) => r.deployment === dep && r.tier === choice.tier, - ), - [options, dep, choice.tier], - ); + const rows = useMemo(() => (options?.catalogue ?? []).filter((r) => r.deployment === dep && r.tier === choice.tier), [options, dep, choice.tier]); // Real line items for the current configuration — the same builder the // checkout uses, so the summary can never disagree with the overlay. - const items = useMemo( - () => (options ? lineItemsFor(options, choice, dep) : []), - [options, choice, dep], - ); + const items = useMemo(() => (options ? lineItemsFor(options, choice, dep) : []), [options, choice, dep]); // Real, localised prices from Paddle for those items. const [receiptPrice, setReceiptPrice] = useState(null); @@ -139,26 +108,22 @@ export function PurchaseForm() { if (!options) return; const baseItems: { priceId: string; quantity: number; tier: Tier }[] = []; for (const p of activePlans) { - const row = options.catalogue.find( - (r) => r.deployment === dep && r.tier === p.tier && r.kind === "base", - ); + const row = options.catalogue.find((r) => r.deployment === dep && r.tier === p.tier && r.kind === "base"); const id = row?.price_ids?.[options.env]?.[choice.term]; if (id) baseItems.push({ priceId: id, quantity: 1, tier: p.tier }); } let live = true; - previewPrices(baseItems.map(({ priceId, quantity }) => ({ priceId, quantity }))).then( - (p) => { - if (!live) return; - const next: Record = {}; - if (p) { - for (const bi of baseItems) { - const line = p.lines[bi.priceId]; - if (line) next[bi.tier] = line.total; - } + previewPrices(baseItems.map(({ priceId, quantity }) => ({ priceId, quantity }))).then((p) => { + if (!live) return; + const next: Record = {}; + if (p) { + for (const bi of baseItems) { + const line = p.lines[bi.priceId]; + if (line) next[bi.tier] = line.total; } - setBasePrices(next); - }, - ); + } + setBasePrices(next); + }); return () => { live = false; }; @@ -169,17 +134,13 @@ export function PurchaseForm() { const createFree = useMutation({ mutationFn: () => api.createInstance(name.trim()), onSuccess: () => router.push("/"), - onError: (e) => - setError(e instanceof ApiError ? e.message : "Could not create the instance."), + onError: (e) => setError(e instanceof ApiError ? e.message : "Could not create the instance."), }); const startCheckout = useMutation({ mutationFn: async () => { const trimmed = name.trim(); - const r = - dep === "cloud" - ? await api.createCloudCheckout(trimmed) - : await api.createSelfHosted(trimmed); + const r = dep === "cloud" ? await api.createCloudCheckout(trimmed) : await api.createSelfHosted(trimmed); return r.instance_id; }, onSuccess: async (instanceId) => { @@ -190,8 +151,7 @@ export function PurchaseForm() { customData: { account_id: accountId, instance_id: instanceId }, }); }, - onError: (e) => - setError(e instanceof ApiError ? e.message : "Could not start checkout."), + onError: (e) => setError(e instanceof ApiError ? e.message : "Could not start checkout."), }); const claim = useMutation({ @@ -277,9 +237,7 @@ export function PurchaseForm() { headline={p.tier === "free" ? "£0" : basePrices[p.tier]} cycleLabel={cycleShort(dep, choice.term)} featureKeys={featureKeys} - catalogue={options.catalogue.filter( - (r) => r.deployment === dep && r.tier === p.tier, - )} + catalogue={options.catalogue.filter((r) => r.deployment === dep && r.tier === p.tier)} env={options.env} term={choice.term} onSelect={() => @@ -287,20 +245,12 @@ export function PurchaseForm() { ...c, tier: p.tier, // Moving tier moves the floor; clamp up. - servers: Math.max( - c.servers, - p.base_limits.max_servers === -1 - ? c.servers - : p.base_limits.max_servers, - ), + servers: Math.max(c.servers, p.base_limits.max_servers === -1 ? c.servers : p.base_limits.max_servers), // Drop add-ons the new tier does not sell. features: c.features.filter((k) => { const st = featureStateFor( p, - options.catalogue.filter( - (r) => - r.deployment === dep && r.tier === p.tier, - ), + options.catalogue.filter((r) => r.deployment === dep && r.tier === p.tier), options.env, c.term, k, @@ -318,61 +268,30 @@ export function PurchaseForm() {
{/* servers */} - + {unlimited ? ( - - Unlimited - + Unlimited ) : ( - setChoice((c) => ({ ...c, servers }))} - /> + setChoice((c) => ({ ...c, servers }))} /> )} {/* features */} {featureKeys.map((key) => { - const st = featureStateFor( - plan, - rows, - options.env, - choice.term, - key, - ); + const st = featureStateFor(plan, rows, options.env, choice.term, key); return ( - + {st === "included" ? ( - - Included - + Included ) : st === "absent" ? ( - - Not in this plan - + Not in this plan ) : ( setChoice((c) => ({ ...c, - features: on - ? [...c.features, key] - : c.features.filter((f) => f !== key), + features: on ? [...c.features, key] : c.features.filter((f) => f !== key), })) } /> @@ -387,14 +306,10 @@ export function PurchaseForm() { {selfHostedFree && (
- Free self-hosted starts with your install.{" "} - Install Vantage on your own server, link the instance ID it reports, then - claim your free licence — there is nothing to pay for here. + Free self-hosted starts with your install. Install Vantage on your own server, link the instance ID it reports, then claim your free + licence — there is nothing to pay for here.
- + Link an install
@@ -408,27 +323,16 @@ export function PurchaseForm() {

Order summary

- - {dep === "cloud" ? "Cloud" : "Self-hosted"} - + {dep === "cloud" ? "Cloud" : "Self-hosted"}
- + {/* name + CTA */}
{!pending && !selfHostedFree && (