diff --git a/adminsite/app/(customer)/instances/link/LinkForm.tsx b/adminsite/app/(customer)/instances/link/LinkForm.tsx index 4a4be8d..c036586 100644 --- a/adminsite/app/(customer)/instances/link/LinkForm.tsx +++ b/adminsite/app/(customer)/instances/link/LinkForm.tsx @@ -12,13 +12,12 @@ export function LinkForm({ claimId, }: { onLinked: (instanceId: string) => void; - // When set, this is a PAID placeholder awaiting its real install UUID: claim - // it in place rather than creating a fresh (Free) instance. The name was - // chosen at checkout, so it is not asked for again. - claimId?: string; + // The PAID placeholder awaiting its real install UUID: claim it in place. The + // name was chosen at checkout, so it is not asked for again. Self-hosted Free + // is created on the purchase page instead, not here. + claimId: string; }) { const [id, setId] = useState(""); - const [name, setName] = useState(""); const [error, setError] = useState(); const [busy, setBusy] = useState(false); @@ -37,9 +36,7 @@ export function LinkForm({ setBusy(true); setError(undefined); try { - const inst = claimId - ? await api.claimLink(claimId, value) - : await api.link(value, name.trim()); + const inst = await api.claimLink(claimId, value); onLinked(inst.instance_id); } catch (err) { setError( @@ -69,14 +66,6 @@ export function LinkForm({ } /> - {!claimId && ( - setName(e.target.value)} - hint="So you can tell it apart from your other installs." - /> - )} diff --git a/adminsite/app/(customer)/instances/link/page.tsx b/adminsite/app/(customer)/instances/link/page.tsx index 538328e..4e1889f 100644 --- a/adminsite/app/(customer)/instances/link/page.tsx +++ b/adminsite/app/(customer)/instances/link/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; import { LinkForm } from "./LinkForm"; @@ -8,10 +9,17 @@ import { PageHeader } from "@/components/PageHeader"; export default function LinkPage() { const router = useRouter(); const qc = useQueryClient(); - // A paid placeholder passes its id here so its install is claimed in place - // rather than a second, Free instance being created alongside it. + // This page only claims a PAID placeholder's real install UUID. Self-hosted + // Free is created on the purchase page, so with no placeholder to claim there + // is nothing to do here — send them there. const claimId = useSearchParams().get("claim") ?? undefined; + useEffect(() => { + if (!claimId) router.replace("/purchase"); + }, [claimId, router]); + + if (!claimId) return null; + return (
(null); - const router = useRouter(); - const qc = useQueryClient(); - - const create = useMutation({ - mutationFn: () => api.createInstance(name.trim()), - onSuccess: async () => { - await qc.invalidateQueries({ queryKey: ["account"] }); - router.push("/"); - }, - onError: (e) => - setError(e instanceof ApiError ? e.message : "Something went wrong. Try again."), - }); - - const slug = slugify(name); - - return ( -
{ - e.preventDefault(); - setError(null); - if (name.trim()) create.mutate(); - }} - > - setName(e.target.value)} - placeholder="Northgate Systems" - required - error={error ?? undefined} - hint={`${slug || "your-instance"}.vantage.hostxtra.co.uk`} - /> - -

- You sign in to it with this same email address and password. Changing your Vantage - HQ password changes it here too. -

- - - - ); -} diff --git a/adminsite/app/(customer)/instances/new/page.tsx b/adminsite/app/(customer)/instances/new/page.tsx deleted file mode 100644 index f1e05d2..0000000 --- a/adminsite/app/(customer)/instances/new/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Metadata } from "next"; -import { CreateForm } from "./CreateForm"; -import { PageHeader } from "@/components/PageHeader"; - -export const metadata: Metadata = { title: "New instance" }; - -export default function NewInstancePage() { - return ( -
- - -
- ); -} diff --git a/adminsite/app/(customer)/page.tsx b/adminsite/app/(customer)/page.tsx index 0bbc8a8..ad6b21d 100644 --- a/adminsite/app/(customer)/page.tsx +++ b/adminsite/app/(customer)/page.tsx @@ -55,7 +55,6 @@ export default function OverviewPage() { }); const pending = (people.data ?? []).filter((p) => !p.verified_at).length; - const hasFree = live.some((i) => i.tier === "free" && i.status !== "cancelled"); const subtitle = live.length === 0 @@ -71,14 +70,7 @@ export default function OverviewPage() { subtitle={subtitle} actions={ live.length > 0 ? ( - <> - {!hasFree && ( - - Create a free instance - - )} - Buy a plan - + Buy a plan ) : undefined } record={[ @@ -97,17 +89,11 @@ export default function OverviewPage() {

No instances yet

Create a free cloud instance and we host it, with your licence applied - automatically. Or buy a self-hosted licence, install Vantage on your own - server, and link it here to get your licence file. + automatically. Or run Vantage on your own server and get its licence — free + or paid — from the purchase page.

Buy a plan - - Create a free instance - - - Link an install -
) : ( @@ -189,14 +175,14 @@ export default function OverviewPage() {

- Link your own install to get its licence file. It keeps its own - users. + Get a licence for your own install — free or paid — from the + purchase page. It keeps its own users.

- Link an install + Get a licence
diff --git a/adminsite/app/(customer)/purchase/PurchaseForm.tsx b/adminsite/app/(customer)/purchase/PurchaseForm.tsx index d01d3f7..47471e5 100644 --- a/adminsite/app/(customer)/purchase/PurchaseForm.tsx +++ b/adminsite/app/(customer)/purchase/PurchaseForm.tsx @@ -10,6 +10,8 @@ import { initPaddle, previewPrices, type PricePreview } from "@/lib/paddle"; /* Tiers in the order a customer reads them, cheapest first. */ const TIER_ORDER: Tier[] = ["free", "professional", "enterprise"]; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + /* Human labels for feature keys. The catalogue names them by key; this is the * one place the customer-facing wording lives. */ const FEATURE_LABEL: Record = { @@ -137,6 +139,18 @@ export function PurchaseForm() { onError: (e) => setError(e instanceof ApiError ? e.message : "Could not create the instance."), }); + // Self-hosted Free binds to the install's own UUID: register the instance, + // then issue its Free licence in one action. + const createSelfHostedFree = useMutation({ + mutationFn: async () => { + const inst = await api.link(uuid.trim(), name.trim()); + await api.claimFree(inst.instance_id); + return inst.instance_id; + }, + onSuccess: (id) => router.push(`/instances/${id}`), + onError: (e) => setError(e instanceof ApiError ? e.message : "Could not create the licence."), + }); + const startCheckout = useMutation({ mutationFn: async () => { const trimmed = name.trim(); @@ -304,15 +318,25 @@ 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. -
- - Link an install - -
+ +
+

+ Install Vantage on your own server first, then paste the instance ID it + reports. We register it and issue your Free licence — nothing to pay. +

+
)} @@ -330,7 +354,7 @@ export function PurchaseForm() { {/* name + CTA */}
- {!pending && !selfHostedFree && ( + {!pending && (