pricing: wire checkout to license server (one-time + subscription)

Replace hardcoded Stripe Payment Links with client-side fetch to
https://licenses.dashcaddy.net/api/checkout/{one-time,subscription}.
Add email capture input and two-button checkout (Buy once vs.
Subscribe & auto-renew). Plan codes: premium_30d/90d/180d/365d.
Update hero copy and FAQ to reflect both payment modes.
This commit is contained in:
Hermes Agent
2026-08-19 13:56:44 -07:00
parent 414c962d3c
commit e2b4ce4447
+114 -37
View File
@@ -5,38 +5,78 @@ import Link from 'next/link';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
// ─── Stripe Payment Links ───────────────────────────────────────────
// These are pre-generated checkout URLs from the Stripe Dashboard.
// Sami needs to provide his Stripe secret key so we can create these
// programmatically, or create them manually in the Stripe Dashboard.
// The product catalog (from src/billing/catalog.js):
// pro-30d → $20, 30-day license (one-time payment)
// pro-90d → $50, 90-day license (one-time payment)
// pro-180d → $70, 180-day license (one-time payment)
// pro-365d → $99, 365-day license (one-time payment)
// ─── License server checkout ────────────────────────────────────────
// The marketing site is a Next.js static export (no server runtime), so
// checkout is driven client-side by POSTing { planCode, customerEmail }
// to the license server at https://licenses.dashcaddy.net. The server
// creates a Stripe Checkout session (one-time OR subscription) and
// returns { url, sessionId }; we redirect the browser to that URL.
//
// Plan codes (see /root/dashcaddy-license-server/src/plans.js):
// premium_30d → $20, 30-day license, one-time or 1-month sub
// premium_90d → $50, 90-day license, one-time or 3-month sub
// premium_180d → $70, 180-day license, one-time or 6-month sub
// premium_365d → $99, 365-day license, one-time or 12-month sub
// ─────────────────────────────────────────────────────────────────────
const STRIPE_LINKS: Record<string, string> = {
'30d': 'https://buy.stripe.com/7sY9AVgJm35P6Uo5j904800',
'90d': 'https://buy.stripe.com/bJe6oJ0Ko7m5emQ4f504801',
'180d': 'https://buy.stripe.com/4gMeVfct635P2E826X04802',
'365d': 'https://buy.stripe.com/8x228tct65dXdiM9zp04803',
};
const LICENSE_SERVER = 'https://licenses.dashcaddy.net';
type CheckoutMode = 'one-time' | 'subscription';
type CheckoutError = { error: string; detail?: string };
async function startCheckout(mode: CheckoutMode, planCode: string, customerEmail: string): Promise<string> {
const endpoint = mode === 'subscription' ? '/api/checkout/subscription' : '/api/checkout/one-time';
const res = await fetch(`${LICENSE_SERVER}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planCode, customerEmail }),
});
const data = await res.json().catch(() => ({} as CheckoutError));
if (!res.ok || !data || typeof (data as { url?: string }).url !== 'string') {
const message = (data as CheckoutError).error || `Checkout failed (${res.status})`;
throw new Error(message);
}
return (data as { url: string }).url;
}
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
}
export default function PricingPage() {
const [selectedPlan, setSelectedPlan] = useState<'30d' | '90d' | '180d' | '365d'>('365d');
const [email, setEmail] = useState('');
const [busy, setBusy] = useState<CheckoutMode | null>(null);
const [error, setError] = useState<string | null>(null);
const planOptions = [
{ key: '30d', label: '30 Days', price: 20, productId: 'pro-30d', perDay: '$0.67/day' },
{ key: '90d', label: '90 Days', price: 50, productId: 'pro-90d', perDay: '$0.56/day' },
{ key: '180d', label: '180 Days', price: 70, productId: 'pro-180d', perDay: '$0.39/day' },
{ key: '365d', label: '365 Days', price: 99, productId: 'pro-365d', perDay: '$0.27/day' },
{ key: '30d', planCode: 'premium_30d', label: '30 Days', price: 20, perDay: '$0.67/day' },
{ key: '90d', planCode: 'premium_90d', label: '90 Days', price: 50, perDay: '$0.56/day' },
{ key: '180d', planCode: 'premium_180d', label: '180 Days', price: 70, perDay: '$0.39/day' },
{ key: '365d', planCode: 'premium_365d', label: '365 Days', price: 99, perDay: '$0.27/day' },
] as const;
const selected = planOptions.find(p => p.key === selectedPlan)!;
const emailOk = isValidEmail(email);
async function handleCheckout(mode: CheckoutMode) {
setError(null);
if (!emailOk) {
setError('Please enter a valid email address — that is where your license key will be delivered.');
return;
}
setBusy(mode);
try {
const url = await startCheckout(mode, selected.planCode, email.trim());
window.location.href = url;
} catch (e) {
setError(e instanceof Error ? e.message : 'Checkout failed');
setBusy(null);
}
}
const coreFeatures = [
'Dashboard & real-time service monitoring',
'93 pre-configured app templates',
'77 pre-configured app templates',
'Automatic SSL via Caddy internal CA',
'DNS automation via Technitium DNS',
'Reverse proxy management + Caddyfile-as-Code',
@@ -64,21 +104,25 @@ export default function PricingPage() {
'Priority support',
'One active machine per license',
'7-day grace period on expiry',
'One-time payment — no recurring billing',
'Fixed-duration license — runs for the duration you buy, then expires',
];
const faqs = [
{
question: 'How does the license work?',
answer: 'You pay once and receive a license code valid for the selected duration (30, 90, 180, or 365 days). Paste it into your DashCaddy dashboard under Admin → License to unlock all premium features. No recurring billing — when it expires, you simply purchase again if you want to continue.',
answer: 'Choose a duration (30, 90, 180, or 365 days) and a payment mode — one-time purchase (license runs for that duration, then expires) or subscription (auto-renews at the chosen interval, time is added to your existing license each renewal, cancel anytime). Enter your email at checkout, pay via Stripe, and your license key is delivered to that email. Paste the key into your DashCaddy dashboard under Admin → License to unlock all premium features for that duration.',
},
{
question: 'What happens when my license expires?',
answer: 'Premium features gracefully deactivate after a 7-day grace period. Your services keep running — only the premium-gated features (SSO, Recipes, Swarm, Fleet Management) become unavailable. Purchase a new license at any time to re-enable them.',
answer: 'One-time licenses: premium features gracefully deactivate after a 7-day grace period. Your services keep running — only the premium-gated features (SSO, Recipes, Swarm, Fleet Management) become unavailable. Purchase a new license at any time to re-enable them. Subscription licenses: renew automatically at the chosen interval — time is added to your existing license, no re-pasting required. Cancel from your Stripe customer portal at any time; the license stays active until the end of the current billing period.',
},
{
question: 'One-time or subscription — which should I pick?',
answer: 'Pick one-time if you want to pay once and not be billed again. Pick subscription if you want uninterrupted premium access with auto-renewal — at each renewal the next interval of time is added to your existing license, so you paste your key once and never re-paste on renewal. Both modes use the same $20/$50/$70/$99 price points.',
},
{
question: 'Can I use DashCaddy without Premium?',
answer: 'Absolutely. The core platform is fully functional without a license — including AI commands, the Security Center, service discovery, monitoring, backup, and all 93 app templates. Premium unlocks SSO, Recipes, Swarm orchestration, and Fleet Management for advanced multi-node setups.',
answer: 'Absolutely. The core platform is fully functional without a license — including AI commands, the Security Center, service discovery, monitoring, backup, and all 77 app templates. Premium unlocks SSO, Recipes, Swarm orchestration, and Fleet Management for advanced multi-node setups.',
},
{
question: 'How many machines can I activate?',
@@ -110,7 +154,7 @@ export default function PricingPage() {
Simple, Transparent <span className="text-brand-400">Pricing</span>
</h1>
<p className="text-xl text-surface-300 mb-8 max-w-2xl mx-auto">
The core platform is completely free including AI commands, Security Center, and all 93 app templates. Premium unlocks advanced orchestration with a one-time payment.
The core platform is completely free including AI commands, Security Center, and all 77 app templates. Premium unlocks SSO, Recipes, Swarm, and Fleet Management. Buy once for a fixed term, or subscribe and never worry about renewal your license is extended automatically.
</p>
</div>
</section>
@@ -166,7 +210,7 @@ export default function PricingPage() {
<div className="p-8 sm:p-10">
<div className="mb-8">
<h3 className="text-2xl font-bold text-surface-50 mb-2">Premium</h3>
<p className="text-surface-400 text-sm mb-6">Advanced orchestration, SSO, and fleet management. One-time payment no subscription.</p>
<p className="text-surface-400 text-sm mb-6">Advanced orchestration, SSO, and fleet management. Fixed-duration license runs for the term you choose, then expires.</p>
{/* Plan selector */}
<div className="mb-4">
@@ -189,22 +233,55 @@ export default function PricingPage() {
<div className="flex items-baseline gap-2">
<span className="text-5xl font-bold text-surface-50">${selected.price}</span>
<span className="text-surface-400 text-lg">one-time</span>
<span className="text-surface-400 text-lg">{selected.label.toLowerCase()} license</span>
</div>
<p className="text-brand-400 text-sm font-semibold mt-1">{selected.perDay} · {selected.productId}</p>
<p className="text-brand-400 text-sm font-semibold mt-1">{selected.perDay}</p>
</div>
{/* Subscribe button — Stripe Payment Link */}
<a
href={STRIPE_LINKS[selectedPlan]}
target="_blank"
rel="noopener noreferrer"
className="block w-full rounded-lg bg-brand-500 px-6 py-3 text-center font-semibold text-white hover:bg-brand-600 hover:shadow-lg hover:shadow-brand-500/30 transition-all"
{/* Email capture — license key is emailed here */}
<div className="mb-3">
<label htmlFor="checkout-email" className="block text-xs font-semibold uppercase tracking-wide text-surface-500 mb-2">
Your email (license key is delivered here)
</label>
<input
id="checkout-email"
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@example.com"
value={email}
onChange={(e) => { setEmail(e.target.value); if (error) setError(null); }}
disabled={busy !== null}
className="w-full rounded-lg border border-surface-700 bg-surface-900/60 px-4 py-3 text-sm text-surface-50 placeholder:text-surface-500 focus:border-brand-400 focus:outline-none focus:ring-2 focus:ring-brand-500/30 disabled:opacity-60"
/>
</div>
{/* Two-button checkout: one-time OR subscription */}
<div className="grid grid-cols-1 gap-2">
<button
type="button"
onClick={() => handleCheckout('one-time')}
disabled={busy !== null}
className="w-full rounded-lg bg-brand-500 px-6 py-3 text-center font-semibold text-white hover:bg-brand-600 hover:shadow-lg hover:shadow-brand-500/30 transition-all disabled:opacity-60 disabled:cursor-not-allowed"
>
Buy Premium License
</a>
{busy === 'one-time' ? 'Redirecting to Stripe…' : `Buy ${selected.label}$${selected.price}`}
</button>
<button
type="button"
onClick={() => handleCheckout('subscription')}
disabled={busy !== null}
className="w-full rounded-lg border border-brand-500/60 bg-brand-500/10 px-6 py-3 text-center font-semibold text-brand-300 hover:bg-brand-500/20 hover:border-brand-400 transition-all disabled:opacity-60 disabled:cursor-not-allowed"
>
{busy === 'subscription' ? 'Redirecting to Stripe…' : `Subscribe & auto-renew — $${selected.price}`}
</button>
</div>
{error && (
<p className="text-center text-xs text-red-400 mt-3" role="alert">
{error}
</p>
)}
<p className="text-center text-xs text-surface-500 mt-2">
🔒 Secure checkout via Stripe · One-time payment
🔒 Secure checkout via Stripe · Cancel subscription anytime
</p>
<div className="border-t border-surface-700/50 pt-8 mt-8">