feat(web): Pro is available — /buy hands off to Microsoft Marketplace and Cleverbridge (no price on the page), the plan cards show the list price of €149 / month, the self-service 30-day trial opens in the portal Plan section, and the get-started sign-in waits for a Terms of Use + DPA tick (D-251)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-11 18:45:56 +02:00
parent a22c26eb9d
commit 5d532d3dbc
10 changed files with 136 additions and 95 deletions
@@ -4,7 +4,7 @@ import { SITE_URL } from "@/utils/config";
export const metadata: Metadata = {
title: "Get Pro – Purchase Options",
description:
"How to purchase Autopilot Monitor Pro: direct online purchase and the Microsoft commercial marketplace — both coming soon. Pricing will be announced; Community stays free.",
"How to purchase Autopilot Monitor Pro: through Microsoft Marketplace or online via Cleverbridge — or try it first with a free 30-day trial. Community stays free.",
keywords: [
"Autopilot Monitor Pro",
"Autopilot Monitor buy",
@@ -15,13 +15,13 @@ export const metadata: Metadata = {
openGraph: {
title: "Get Pro – Autopilot Monitor",
description:
"How to purchase Autopilot Monitor Pro: direct online purchase and the Microsoft commercial marketplace — both coming soon.",
"How to purchase Autopilot Monitor Pro: through Microsoft Marketplace or online via Cleverbridge.",
url: `${SITE_URL}/buy`,
},
twitter: {
title: "Get Pro – Autopilot Monitor",
description:
"How to purchase Autopilot Monitor Pro: direct online purchase and the Microsoft commercial marketplace — both coming soon.",
"How to purchase Autopilot Monitor Pro: through Microsoft Marketplace or online via Cleverbridge.",
},
alternates: {
canonical: `${SITE_URL}/buy`,
+41 -28
View File
@@ -1,26 +1,32 @@
import Link from "next/link";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL } from "@/utils/config";
/**
* Purchase page for Pro. Linked from the public /plans page and from the
* portal's Plan section (absolute www URL — /buy is public surface).
*
* Both purchase channels are honest "coming soon" placeholders until the
* checkout links exist. When they do: the provider host must be registered in
* utils/config.ts, the CSP in staticwebapp.config.json, the swaConfig guard
* test, and the dev CSP in next.config.ts — see tasks/todo.md follow-up.
* Purchase page for Pro: hands the buyer off to one of the two purchase channels. Linked from
* the public /plans page and from the portal's Plan section (absolute www URL — /buy is public
* surface). It names no price on purpose: the plan cards carry the list price, the channel's
* checkout the binding one. Plain outbound links, so neither host needs a CSP entry.
*/
const PURCHASE_GUIDE_URL = `${DOCS_URL}/troubleshooting-and-support/how-to-purchase`;
const CHANNELS = [
{
label: "Microsoft Marketplace",
description:
"Purchase through the Microsoft commercial marketplace using your organization's existing Microsoft billing relationship.",
"Subscribe through the Microsoft commercial marketplace and pay through your organization's existing Azure billing.",
href: "https://marketplace.microsoft.com/en-us/product/saas/glueckkanja-gabag.autopilot-monitor-transactable-prod?tab=Overview",
cta: "Buy on Microsoft Marketplace",
guide: `${PURCHASE_GUIDE_URL}/microsoft-marketplace`,
},
{
label: "Cleverbridge",
description:
"Buy Pro online with a credit card or on invoice — a fast, self-service checkout for a single organization.",
"Buy Pro online by credit card, PayPal or bank transfer — a fast, self-service checkout for a single organization.",
href: "https://www.cleverbridge.com/306/purl-Autopilot-Monitor-Buy-Y",
cta: "Buy with Cleverbridge",
guide: `${PURCHASE_GUIDE_URL}/cleverbridge`,
},
];
@@ -35,9 +41,8 @@ export default function BuyPage() {
Get Autopilot Monitor Pro
</h1>
<p className="mt-4 max-w-2xl text-[15px] text-gray-600 leading-relaxed">
Pro will be available through two purchase channels. Neither is open yet — pricing
and availability will be announced. Until then, Community is the full product, free
for everyone.
Pick the channel that fits how your organization buys software. Both lead to the same
Pro plan on your existing tenant — your data stays where it is.
</p>
</div>
</header>
@@ -48,34 +53,42 @@ export default function BuyPage() {
{CHANNELS.map((channel) => (
<div key={channel.label} className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8 flex flex-col">
<h2 className="text-lg font-bold text-gray-900 mb-3">{channel.label}</h2>
<p className="text-sm text-gray-600 leading-relaxed mb-4 flex-1">{channel.description}</p>
<button
type="button"
disabled
title="Available once Pro pricing is announced."
className="w-full rounded-lg border border-[var(--lp-line-soft)] bg-[var(--lp-surface-2)] px-4 py-2.5 text-sm font-semibold text-[var(--lp-ink-faint)] cursor-not-allowed"
<p className="text-sm text-gray-600 leading-relaxed mb-5 flex-1">{channel.description}</p>
<a
href={channel.href}
target="_blank"
rel="noopener noreferrer"
className="block w-full text-center text-sm font-semibold text-white bg-purple-600 rounded-lg px-4 py-2.5 hover:bg-purple-700 transition-colors"
>
Coming soon
</button>
{channel.cta}
</a>
<a
href={channel.guide}
target="_blank"
rel="noopener noreferrer"
className="mt-3 text-center text-xs text-gray-600 hover:text-gray-900 underline"
>
Purchase guide: prerequisites, payment and cancellation
</a>
</div>
))}
</section>
{/* Interim path */}
{/* Trial + questions */}
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">Questions About Pro?</h2>
<h2 className="text-xl font-bold text-gray-900 mb-4">Try Pro First</h2>
<p className="text-gray-700 leading-relaxed">
Interested in Pro for your organization or an MSP scenario, or want to know when it
opens? Reach out through any channel on the{" "}
A tenant administrator can start a one-time, free 30-day Pro trial in the portal under
Settings → Tenant → Plan. When it ends, the tenant returns to Community automatically.
Questions about Pro or an MSP scenario? Reach out through any channel on the{" "}
<Link href="/help" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">
Help &amp; Support
</Link>{" "}
page — you&apos;ll talk directly to the person building the platform. And if you
haven&apos;t compared the plans yet, the{" "}
page, or compare the plans in the{" "}
<Link href="/plans" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">
plans overview
</Link>{" "}
shows exactly what Pro adds.
</Link>
.
</p>
</section>
</main>
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { LoginButton } from "../../components/landing/LoginButton";
import { SignupConsentCta } from "../../components/landing/SignupConsentCta";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL, SITE_URL } from "@/utils/config";
@@ -85,11 +85,8 @@ export default function GetStartedPage() {
</ol>
</div>
{/* CTA */}
<div className="mt-14 flex flex-wrap items-center gap-3">
<LoginButton signup className="px-7 py-3 rounded-lg bg-[var(--lp-accent-ink)] hover:brightness-110 hover:shadow-lg text-white font-semibold shadow-md transition-all">
Sign in to get started
</LoginButton>
{/* CTA — sign-in waits for the Terms + DPA tick */}
<SignupConsentCta>
<a
href={DOCS_URL}
target="_blank"
@@ -98,7 +95,7 @@ export default function GetStartedPage() {
>
Read the docs
</a>
</div>
</SignupConsentCta>
<p className="mt-6 text-sm text-[var(--lp-ink-faint)] leading-relaxed max-w-xl">
When you need more later: the{" "}
@@ -39,7 +39,7 @@ export default function PlansPage() {
}
proBadge={
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700 border border-purple-200">
Coming soon
Free 30-day trial
</span>
}
proCta={
@@ -53,7 +53,8 @@ export default function PlansPage() {
/>
<p className="text-sm text-gray-600">
Pricing and availability for Pro will be announced. Community stays free.{" "}
Pro is sold through Microsoft Marketplace and Cleverbridge; a tenant administrator can
try it free for 30 days from the portal. Community stays free.{" "}
For a feature-by-feature reference, see the{" "}
<a
href={`${DOCS_URL}/plans`}
@@ -9,14 +9,6 @@ import { SITE_URL } from "@/utils/config";
import { SectionCardHeader } from "@/components/SectionCardHeader";
import { DOCS_PATHS } from "@/lib/docsPaths";
/**
* Self-service Pro trial switch. The Pro feature set is not finalized yet, so the
* trial CTA is teased but not actionable. Flip this to `true` (a one-line change) to open the
* self-service 30-day trial — the backend POST /trial endpoint and the startTrial() wiring are
* already in place; only this gate keeps the button inert.
*/
const TRIAL_SELF_SERVICE_ENABLED = false;
/**
* Plan section: the shared Community/Pro comparison cards (components/plans/PlanCards —
* also rendered on the public /plans page) with the tenant's current plan highlighted,
@@ -55,38 +47,22 @@ export function SectionPlan() {
? `Trial — ${daysLeft} day${daysLeft === 1 ? "" : "s"} left`
: "Current plan"}
</span>
) : (
) : editionInfo.trialAvailable ? (
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700 border border-purple-200">
Coming soon
Free 30-day trial
</span>
);
) : undefined;
// CTA — only meaningful while the tenant is on Community
const proCta = !isPro ? (
<>
{trialConsumed ? (
<p className="text-sm text-gray-600">This tenant has already used its Pro trial.</p>
) : !canStartTrial ? (
<p className="text-sm text-gray-600">
This tenant has already used its Pro trial. To move to Pro,{" "}
<a
href="https://github.com/okieselbach/AutopilotMonitor/issues"
target="_blank"
rel="noopener noreferrer"
className="text-purple-700 hover:underline"
>
get in touch
</a>
.
A tenant administrator can start the one-time 30-day Pro trial here.
</p>
) : !TRIAL_SELF_SERVICE_ENABLED ? (
<button
type="button"
disabled
title="Available soon — the Pro trial opens once the feature set is finalized."
className="w-full text-sm font-medium text-white bg-purple-400 rounded-lg px-4 py-2.5 cursor-not-allowed opacity-70"
>
Start 30-day Pro trial — coming soon
</button>
) : canStartTrial && missingProfile.length > 0 ? (
) : missingProfile.length > 0 ? (
// Pro-requires-contact-profile gate (backend enforces the same via 409
// ContactProfileRequired — this branch just makes the path obvious).
<div className="text-sm bg-amber-50 border border-amber-200 rounded-lg p-3 text-amber-900 dark:bg-amber-950/30 dark:border-amber-700/50 dark:text-amber-200">
@@ -97,7 +73,7 @@ export function SectionPlan() {
</Link>{" "}
so we can reach and identify you for service or security matters — then start your trial here.
</div>
) : canStartTrial && !confirming ? (
) : !confirming ? (
<button
type="button"
onClick={() => setConfirming(true)}
@@ -105,7 +81,7 @@ export function SectionPlan() {
>
Start 30-day Pro trial
</button>
) : canStartTrial && confirming ? (
) : (
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-600">One-time trial — start now?</span>
<button
@@ -128,16 +104,16 @@ export function SectionPlan() {
Cancel
</button>
</div>
) : null}
)}
<p className="mt-3 text-xs text-gray-600">
Curious how Pro will be sold?{" "}
Ready to buy? Pro is sold through Microsoft Marketplace and Cleverbridge —{" "}
<a
href={`${SITE_URL}/buy`}
target="_blank"
rel="noopener noreferrer"
className="text-purple-700 hover:underline dark:text-purple-300"
>
View purchase options
view purchase options
</a>
.
</p>
@@ -167,7 +143,7 @@ export function SectionPlan() {
<p className="text-xs text-gray-600 mt-5">
{isPro && editionInfo.isTrial
? "When the trial ends, the tenant returns to Community automatically."
: "Scope, pricing and timeline for Pro will be announced. Community stays free."}
: "After a purchase this tenant moves to Pro and keeps its data. Community stays free."}
</p>
</div>
</div>
@@ -8,9 +8,12 @@ export function LoginButton({
className,
children,
signup = false,
disabled = false,
}: {
className?: string;
children: React.ReactNode;
/** Inert until the caller's precondition holds (get-started: Terms + DPA accepted). */
disabled?: boolean;
/**
* Signup-funnel CTA (get-started "Sign in to get started"): routes the login through the
* PRIMARY app registration so brand-new tenants consent the NEW app as part of the expected
@@ -43,7 +46,7 @@ export function LoginButton({
};
return (
<button onClick={handleClick} className={className}>
<button type="button" onClick={handleClick} disabled={disabled} className={className}>
{children}
</button>
);
@@ -0,0 +1,56 @@
"use client";
import { useState } from "react";
import { LoginButton } from "./LoginButton";
import { DOCS_URL } from "@/utils/config";
const LINK_CLASS = "text-[var(--lp-accent-ink)] hover:opacity-80 underline";
/**
* Get-started CTA: the signup sign-in stays inert until the visitor ticks that they accept the
* Terms of Use and the Data Processing Agreement. A client-side gate only — nothing is recorded
* here. Both links open in a new tab so the tick survives reading them.
*/
export function SignupConsentCta({ children }: { children?: React.ReactNode }) {
const [accepted, setAccepted] = useState(false);
return (
<div className="mt-14">
<label className="flex items-start gap-3 max-w-xl text-sm text-[var(--lp-ink-soft)] leading-relaxed cursor-pointer select-none">
<input
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
className="mt-0.5 h-4 w-4 shrink-0 cursor-pointer accent-[var(--lp-accent-ink)]"
/>
<span>
I agree to the{" "}
<a href="/terms" target="_blank" rel="noopener noreferrer" className={LINK_CLASS}>
Terms of Use
</a>{" "}
and the{" "}
<a
href={`${DOCS_URL}/legal/data-privacy-agreement-dpa`}
target="_blank"
rel="noopener noreferrer"
className={LINK_CLASS}
>
Data Processing Agreement
</a>
.
</span>
</label>
<div className="mt-5 flex flex-wrap items-center gap-3">
<LoginButton
signup
disabled={!accepted}
className="px-7 py-3 rounded-lg bg-[var(--lp-accent-ink)] text-white font-semibold shadow-md transition-all enabled:hover:brightness-110 enabled:hover:shadow-lg disabled:opacity-40 disabled:shadow-none disabled:cursor-not-allowed"
>
Sign in to get started
</LoginButton>
{children}
</div>
</div>
);
}
@@ -128,17 +128,10 @@ export function PlanCards({ surface, highlight = null, communityBadge, proBadge,
<div className="mt-4 mb-5">
{proPrice ?? (
PRO_PRICE ? (
<>
<span className="text-2xl font-bold text-purple-900">{PRO_PRICE.amount}</span>
<span className="text-sm text-gray-600"> {PRO_PRICE.suffix}</span>
</>
) : (
<>
<span className="text-2xl font-bold text-purple-900">Pricing</span>
<span className="text-sm text-gray-600"> — announced soon</span>
</>
)
<>
<span className="text-2xl font-bold text-purple-900">{PRO_PRICE.amount}</span>
<span className="text-sm text-gray-600"> {PRO_PRICE.suffix}</span>
</>
)}
</div>
@@ -39,9 +39,11 @@ export const proExtras = [
];
/**
* Pro price. Stays null until pricing is announced — PlanCards then renders the
* "Pricing — announced soon" teaser. Once the price is final, set it here (e.g.
* `{ amount: "€X", suffix: "per device / month" }`) and every surface — landing
* and portal — updates in the same one-line change.
* Pro list price, shown on the plan cards only (public /plans and the portal Plan section).
* The /buy page deliberately names no price — the checkout of the purchase channel carries
* the binding one.
*/
export const PRO_PRICE: { amount: string; suffix: string } | null = null;
export const PRO_PRICE: { amount: string; suffix: string } = {
amount: "€149",
suffix: "/ month, excl. VAT",
};