Product-code half (fleet gate, capabilities endpoint, offline feature migration, PREMIUM_FEATURES completion) lives in the dashcaddy repo, branch dc/DC-165-uniformity — judged as one bundle. Site edits (all claims verified against product source, see EVIDENCE [9]): - template count 77 -> 78 EVERYWHERE (page.tsx, features, pricing, docs, blog bodies + manifest.ts frontmatter per three-way consistency rule, AppShowcase, DocsLayout, FaqSection). Source of truth: APP_TEMPLATES keys == 78. - internal-identifier leak sweep: 85 occurrences of *.sami (internal TLD), an internal Git host URL, a private venv path, and a server-local repo path in pricing comment -> replaced with the documented .home style / generic wording. status.sami replaced by dashcaddy.local (product default dashboardHost). Verified ZERO remaining in rebuilt out/. - advertise as Premium (owner decision 2026-10-09): pricing premium list + FAQ + docs/premium table + two new deep-dive sections (Workflow Automation: the five bundled ids from bundled-workflows.js; Scheduled Backups: schedule fields from the /backups/schedule schema, manual one-click + DR explicitly noted as free). - calendar widget + hosted-Radicale auto-wiring advertised (features page chip + new docs/catalog/radicale install guide following house template structure; syncthing page mirrored for the placeholder conventions). - Connect AI one-click key flow advertised (features chip). - MCP card softened: 'entire infrastructure' -> the real tool scope + pointer to GET /api/v1/capabilities (public ground-truth manifest). - llms.txt: capabilities endpoint pointer + 'prefer it over static prose' + MCP tool examples + premium gate list; robots.txt gains LLMs-Txt line. - NOTE: a parked unrelated work-in-progress (dc-seo-r8 blog edits) was stashed before the build and is NOT in this commit. Build gate: rm -rf .next out && npm run build -> clean, 123 static pages, out/ greps zero stale counts/leaks.
380 lines
20 KiB
TypeScript
380 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import Navbar from '@/components/Navbar';
|
|
import Footer from '@/components/Footer';
|
|
|
|
// ─── 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 (server-defined in the license service):
|
|
// 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 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', 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',
|
|
'78 pre-configured app templates',
|
|
'Automatic SSL via Caddy internal CA',
|
|
'DNS automation via Technitium DNS',
|
|
'Reverse proxy management + Caddyfile-as-Code',
|
|
'AI Intent Router — natural language commands',
|
|
'MCP Server — AI assistant integration',
|
|
'Security Center — unified event pipeline',
|
|
'Service Discovery — auto-detect containers',
|
|
'Smart Defaults Wizard — guided setup',
|
|
'WebSocket real-time dashboard updates',
|
|
'TOTP 2FA + multi-user admin & invites',
|
|
'Audit logging & encrypted credential vault',
|
|
'One-click backup & disaster recovery',
|
|
'Plugin/extension system',
|
|
'REST API & MCP Server for AI assistants',
|
|
'Prometheus metrics export',
|
|
'i18n — 31 languages (EN/ES/FR/DE/AR + 26 more)',
|
|
];
|
|
|
|
const premiumFeatures = [
|
|
'Everything in Core, plus:',
|
|
'Auto-Login SSO for deployed apps',
|
|
'Recipes — multi-container stack deployment',
|
|
'Swarm — Docker Swarm multi-node orchestration',
|
|
'Multi-Host Fleet Management — remote instance control',
|
|
'Workflow Automation — scripted multi-step operations',
|
|
'Scheduled Backups — automated backup schedules (manual one-click backup stays free)',
|
|
'Priority support',
|
|
'One active machine per license',
|
|
'7-day grace period on expiry',
|
|
'Fixed-duration license — runs for the duration you buy, then expires',
|
|
];
|
|
|
|
const faqs = [
|
|
{
|
|
question: 'How does the license work?',
|
|
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: '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, manual one-click backup, and all 78 app templates. Premium unlocks SSO, Recipes, Swarm orchestration, Fleet Management, workflow automation, and scheduled backups for advanced multi-node setups.',
|
|
},
|
|
{
|
|
question: 'How many machines can I activate?',
|
|
answer: 'One active machine at a time per license. You can deactivate and move to a new machine when needed.',
|
|
},
|
|
{
|
|
question: 'Is my data safe?',
|
|
answer: 'DashCaddy runs entirely on your infrastructure. Your data never leaves your server. We have no access to your applications, configurations, or data.',
|
|
},
|
|
{
|
|
question: 'What payment methods do you accept?',
|
|
answer: 'We use Stripe for secure payment processing. All major credit cards are accepted. Payments are processed through Stripe\'s encrypted checkout — we never see or store your card details.',
|
|
},
|
|
];
|
|
|
|
const [expandedFaq, setExpandedFaq] = useState<number | null>(0);
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen bg-surface-950 text-surface-50">
|
|
<Navbar />
|
|
|
|
{/* Hero */}
|
|
<section className="relative py-16 sm:py-20 lg:py-24">
|
|
<div className="absolute inset-0 -z-10">
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-brand-500/20 rounded-full blur-3xl opacity-30 animate-pulse" />
|
|
</div>
|
|
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
|
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold mb-6">
|
|
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 78 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>
|
|
|
|
{/* Two-column: Core vs Premium */}
|
|
<section className="relative py-12 sm:py-16 lg:py-20">
|
|
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
|
|
{/* Core Plan */}
|
|
<div className="relative rounded-2xl border border-surface-700/50 bg-surface-800/50 hover:border-surface-700 hover:bg-surface-800/80 transition-all">
|
|
<div className="p-8 sm:p-10">
|
|
<div className="mb-8">
|
|
<h3 className="text-2xl font-bold text-surface-50 mb-2">Core</h3>
|
|
<p className="text-surface-400 text-sm mb-6">Everything you need to deploy, manage, and secure self-hosted services — with AI built in.</p>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className="text-5xl font-bold text-surface-50">Free</span>
|
|
</div>
|
|
<p className="text-surface-500 text-sm mt-2">No license required, no credit card</p>
|
|
</div>
|
|
|
|
<Link
|
|
href="/docs/installation"
|
|
className="block w-full rounded-lg border border-surface-700 bg-surface-700/50 px-6 py-3 text-center font-semibold text-surface-50 hover:border-brand-400 hover:bg-surface-700 hover:text-brand-400 transition-all"
|
|
>
|
|
Read Install Guide
|
|
</Link>
|
|
|
|
<div className="border-t border-surface-700/50 pt-8 mt-8">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-surface-500 mb-4">Everything included — no limits</p>
|
|
<ul className="space-y-3">
|
|
{coreFeatures.map((feature, i) => (
|
|
<li key={i} className="flex items-start gap-3">
|
|
<svg className="h-5 w-5 flex-shrink-0 text-green-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
<span className="text-surface-300 text-sm">{feature}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Premium Plan */}
|
|
<div className="relative rounded-2xl border border-brand-500/50 bg-gradient-to-br from-surface-800 to-surface-900 shadow-2xl shadow-brand-500/20">
|
|
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
|
<span className="inline-block rounded-full bg-brand-500 px-4 py-1 text-xs font-bold uppercase tracking-wide text-white">
|
|
Premium
|
|
</span>
|
|
</div>
|
|
|
|
<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. Fixed-duration license — runs for the term you choose, then expires.</p>
|
|
|
|
{/* Plan selector */}
|
|
<div className="mb-4">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{planOptions.map((opt) => (
|
|
<button
|
|
key={opt.key}
|
|
onClick={() => setSelectedPlan(opt.key)}
|
|
className={`rounded-lg px-3 py-2 text-sm font-medium transition-all ${
|
|
selectedPlan === opt.key
|
|
? 'bg-brand-500 text-white'
|
|
: 'bg-surface-700/50 text-surface-400 hover:bg-surface-700 hover:text-surface-300'
|
|
}`}
|
|
>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<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">{selected.label.toLowerCase()} license</span>
|
|
</div>
|
|
<p className="text-brand-400 text-sm font-semibold mt-1">{selected.perDay}</p>
|
|
</div>
|
|
|
|
{/* 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"
|
|
>
|
|
{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 · Cancel subscription anytime
|
|
</p>
|
|
|
|
<div className="border-t border-surface-700/50 pt-8 mt-8">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-surface-500 mb-4">Everything in Core, plus</p>
|
|
<ul className="space-y-3">
|
|
{premiumFeatures.map((feature, i) => (
|
|
<li key={i} className="flex items-start gap-3">
|
|
{feature.startsWith('Everything') ? (
|
|
<span className="text-sm font-semibold text-surface-300">{feature}</span>
|
|
) : (
|
|
<>
|
|
<svg className="h-5 w-5 flex-shrink-0 text-brand-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
<span className="text-surface-300 text-sm">{feature}</span>
|
|
</>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* FAQ */}
|
|
<section className="relative py-16 sm:py-20 lg:py-24 bg-gradient-to-b from-surface-950 to-surface-900">
|
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
|
<div className="mb-12 text-center">
|
|
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4">
|
|
Frequently Asked <span className="text-brand-400">Questions</span>
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{faqs.map((faq, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="rounded-lg border border-surface-700/50 bg-surface-800/50 overflow-hidden transition-all hover:border-surface-700"
|
|
>
|
|
<button
|
|
onClick={() => setExpandedFaq(expandedFaq === idx ? null : idx)}
|
|
className="w-full px-6 py-4 flex items-center justify-between hover:bg-surface-800/70 transition-colors"
|
|
>
|
|
<h3 className="text-lg font-semibold text-surface-50 text-left">{faq.question}</h3>
|
|
<svg
|
|
className={`h-6 w-6 flex-shrink-0 text-brand-400 transition-transform ${expandedFaq === idx ? 'rotate-180' : ''}`}
|
|
fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
</svg>
|
|
</button>
|
|
{expandedFaq === idx && (
|
|
<div className="border-t border-surface-700/50 bg-surface-900/50 px-6 py-4">
|
|
<p className="text-surface-300 leading-relaxed">{faq.answer}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* CTA */}
|
|
<section className="relative py-16 sm:py-20 lg:py-24 bg-surface-950">
|
|
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
|
|
<h2 className="text-3xl sm:text-4xl font-bold mb-6">Ready to Get Started?</h2>
|
|
<p className="text-xl text-surface-300 mb-8 max-w-2xl mx-auto">
|
|
Install DashCaddy and start deploying services in minutes. The core platform is free forever.
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
<Link
|
|
href="/docs/installation"
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-8 py-3 text-base font-semibold text-white hover:bg-brand-600 transition-all hover:shadow-lg hover:shadow-brand-500/30"
|
|
>
|
|
Installation Guide
|
|
</Link>
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-surface-700 bg-surface-800/50 px-8 py-3 text-base font-semibold text-surface-50 hover:border-brand-400 hover:bg-surface-800 transition-colors"
|
|
>
|
|
Back to Home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|