feat(settings): replace static license CTA with dynamic upgrade cards (#201)

Community users now see Personal Pro and Team Pro cards with feature
highlights and direct Lemon Squeezy checkout links. Personal Pro users
see only the Team Pro upgrade option. Trial and Team Pro users see no
upgrade cards since they already have full access.
This commit is contained in:
Anso
2026-03-27 23:49:11 -04:00
committed by GitHub
parent 0cc198f86d
commit d3828e885d
6 changed files with 116 additions and 59 deletions
+2
View File
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
* **settings/license:** replaced static "View Pricing" button with dynamic upgrade cards — Community users see Personal Pro and Team Pro options with feature highlights; Personal Pro users see Team Pro upgrade only; each card links directly to Lemon Squeezy checkout
* **docs:** comprehensive documentation audit — updated 11 pages with accurate content and 14 fresh screenshots
* **docs/configuration:** removed JWT_SECRET from required env vars (it's auto-generated), added compose directory reorganization explanation
* **docs/settings:** added missing License, Users, Webhooks, Support, and About sections; removed obsolete Appearance section
+13 -5
View File
@@ -21,16 +21,24 @@ Every new Sencho installation starts with a **14-day Pro trial** - no license ke
When the trial expires, Sencho automatically reverts to the Community tier. No data is lost.
## Purchasing a license
## Upgrading your plan
1. Visit [sencho.io](https://sencho.io/#pricing) and choose a plan.
2. Complete the checkout on Lemon Squeezy.
3. You'll receive a license key by email.
You can upgrade directly from **Settings > License** in your Sencho dashboard. The upgrade cards shown depend on your current tier:
- **Community users** see both **Personal Pro** and **Team Pro** options with feature highlights and direct checkout links.
- **Personal Pro users** see a **Team Pro** upgrade card for when you need unlimited accounts.
- **Team Pro users** are on the highest tier — no upgrade cards are shown.
Clicking an upgrade button opens the Lemon Squeezy checkout in a new tab. After completing the purchase, you'll receive a license key by email.
<Frame>
<img src="/images/license-settings.png" alt="License settings showing upgrade cards for Personal Pro and Team Pro" />
</Frame>
## Activating your license
1. Go to **Settings > License** in your Sencho dashboard.
2. Paste your key into the **License Key** field.
2. Paste your key into the **License Key** field at the bottom of the page.
3. Click **Activate**.
Sencho validates the key against Lemon Squeezy and activates Pro features immediately.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 66 KiB

+1 -1
View File
@@ -39,7 +39,7 @@ Manage your Sencho Pro license from this section.
| **Trial status** | If on a trial, shows remaining days |
| **Customer / Plan / Key** | Displayed when a Pro license is active |
| **Renews** | Next renewal date for active subscriptions |
| **View Pricing** | Opens the Sencho pricing page to purchase a license |
| **Upgrade cards** | Dynamic plan cards with feature highlights and direct Lemon Squeezy checkout links. Community users see Personal Pro and Team Pro; Personal Pro users see Team Pro only |
| **Activate** | Enter a license key to activate Pro |
| **Manage Subscription** | Opens the billing portal (active Pro only) |
| **Deactivate License** | Reverts to Community features (active Pro only) |
+99 -47
View File
@@ -18,7 +18,7 @@ import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Shield, Activity, Bell, Code, Server, Package, RefreshCw, Database, Info, Crown, CheckCircle, XCircle, Clock, Webhook, Copy, Trash2, Plus, ChevronDown, ChevronRight, History, Users, Pencil, ExternalLink, CreditCard, LifeBuoy, Book, Mail, Bug } from 'lucide-react';
import { Shield, Activity, Bell, Code, Server, Package, RefreshCw, Database, Info, Crown, CheckCircle, Check, XCircle, Clock, Webhook, Copy, Trash2, Plus, ChevronDown, ChevronRight, History, Users, Pencil, ExternalLink, CreditCard, LifeBuoy, Book, Mail, Bug, Zap } from 'lucide-react';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { NodeManager } from './NodeManager';
@@ -647,7 +647,7 @@ function UsersSection() {
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
const { activeNode } = useNodes();
const { isAdmin } = useAuth();
const { license, isPro, activate, deactivate, checkout } = useLicense();
const { license, isPro, activate, deactivate } = useLicense();
const isRemote = activeNode?.type === 'remote';
const [activeSection, setActiveSection] = useState<SectionId>('account');
const [licenseKeyInput, setLicenseKeyInput] = useState('');
@@ -1184,59 +1184,111 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
</div>
)}
{/* Subscribe / Activate (not active) */}
{license?.status !== 'active' && (
<div className="space-y-4">
<div className="space-y-1">
<Label className="text-base">Subscribe to Sencho Pro</Label>
<p className="text-xs text-muted-foreground">
Purchase a license key from our website, then activate it below.
</p>
</div>
<Button
size="sm"
onClick={() => checkout()}
>
<Crown className="w-4 h-4 mr-2" />
View Pricing
<ExternalLink className="w-3 h-3 ml-1.5 opacity-50" />
</Button>
{/* Upgrade Cards — Community: show both, Personal Pro: show Team only, Team Pro: none */}
{(license?.tier !== 'pro' || (license?.variant === 'personal' && license?.status === 'active')) && (
<div className="space-y-3">
<Label className="text-base">Upgrade your plan</Label>
<div className={`grid gap-3 ${license?.tier !== 'pro' ? 'grid-cols-1 sm:grid-cols-2' : 'grid-cols-1'}`}>
{/* Personal Pro Card — only for Community users */}
{license?.tier !== 'pro' && (
<div className="relative border border-border rounded-xl p-4 space-y-3 bg-muted/5">
<div className="flex items-center gap-2">
<Crown className="w-4 h-4 text-amber-500" />
<span className="font-semibold text-sm">Personal Pro</span>
<Badge variant="secondary" className="text-[10px] font-semibold uppercase px-1.5 py-0">Popular</Badge>
</div>
<p className="text-xs text-muted-foreground">Professional tools for solo operators.</p>
<ul className="space-y-1.5">
{['Fleet View with drill-down', 'RBAC viewer accounts (1 + 3)', 'Custom webhooks', 'Atomic deployment', 'Fleet-wide backups'].map((f) => (
<li key={f} className="flex items-center gap-2 text-xs text-muted-foreground">
<Check className="w-3 h-3 shrink-0 text-green-500" />
{f}
</li>
))}
</ul>
<Button
size="sm"
className="w-full"
onClick={() => window.open('https://saelix.lemonsqueezy.com/checkout/buy/f75bfb65-443a-46a0-abb1-981e0ff4b382', '_blank')}
>
<Zap className="w-4 h-4 mr-2" />
Get Personal Pro
<ExternalLink className="w-3 h-3 ml-1.5 opacity-50" />
</Button>
</div>
)}
{/* License key activation */}
<div className="border-t border-border pt-4 space-y-2">
<Label className="text-sm text-muted-foreground">Have a license key?</Label>
<div className="flex gap-2">
<Input
placeholder="XXXXX-XXXXX-XXXXX-XXXXX"
value={licenseKeyInput}
onChange={(e) => setLicenseKeyInput(e.target.value)}
className="font-mono"
/>
{/* Team Pro Card */}
<div className="border border-border rounded-xl p-4 space-y-3 bg-muted/5">
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-blue-500" />
<span className="font-semibold text-sm">Team Pro</span>
</div>
<p className="text-xs text-muted-foreground">For teams managing shared infrastructure.</p>
<ul className="space-y-1.5">
{[
...(license?.variant === 'personal' ? ['Everything in Personal Pro'] : ['Everything in Community']),
'Unlimited admin accounts',
'Unlimited viewer accounts',
...(license?.variant !== 'personal' ? ['Fleet View & webhooks', 'Atomic deployment & backups'] : []),
'Team onboarding assistance',
].map((f) => (
<li key={f} className="flex items-center gap-2 text-xs text-muted-foreground">
<Check className="w-3 h-3 shrink-0 text-green-500" />
{f}
</li>
))}
</ul>
<Button
variant="outline"
onClick={async () => {
if (!licenseKeyInput.trim()) return;
setIsActivating(true);
const result = await activate(licenseKeyInput.trim());
if (result.success) {
toast.success('License activated! Welcome to Sencho Pro.');
setLicenseKeyInput('');
} else {
toast.error(result.error || 'Activation failed');
}
setIsActivating(false);
}}
disabled={isActivating || !licenseKeyInput.trim()}
size="sm"
variant={license?.tier !== 'pro' ? 'outline' : 'default'}
className="w-full"
onClick={() => window.open('https://saelix.lemonsqueezy.com/checkout/buy/b049b824-176a-408d-a9d3-9365c979a61f', '_blank')}
>
{isActivating
? <><RefreshCw className="w-4 h-4 mr-2 animate-spin" />Activating...</>
: 'Activate'
}
<Zap className="w-4 h-4 mr-2" />
Get Team Pro
<ExternalLink className="w-3 h-3 ml-1.5 opacity-50" />
</Button>
</div>
</div>
</div>
)}
{/* License key activation — show when not active */}
{license?.status !== 'active' && (
<div className="border-t border-border pt-4 space-y-2">
<Label className="text-sm text-muted-foreground">Have a license key?</Label>
<div className="flex gap-2">
<Input
placeholder="XXXXX-XXXXX-XXXXX-XXXXX"
value={licenseKeyInput}
onChange={(e) => setLicenseKeyInput(e.target.value)}
className="font-mono"
/>
<Button
variant="outline"
onClick={async () => {
if (!licenseKeyInput.trim()) return;
setIsActivating(true);
const result = await activate(licenseKeyInput.trim());
if (result.success) {
toast.success('License activated! Welcome to Sencho Pro.');
setLicenseKeyInput('');
} else {
toast.error(result.error || 'Activation failed');
}
setIsActivating(false);
}}
disabled={isActivating || !licenseKeyInput.trim()}
>
{isActivating
? <><RefreshCw className="w-4 h-4 mr-2 animate-spin" />Activating...</>
: 'Activate'
}
</Button>
</div>
</div>
)}
</div>
)}
+1 -6
View File
@@ -26,7 +26,6 @@ interface LicenseContextType {
refresh: () => Promise<void>;
activate: (licenseKey: string) => Promise<{ success: boolean; error?: string }>;
deactivate: () => Promise<{ success: boolean; error?: string }>;
checkout: () => void;
}
const LicenseContext = createContext<LicenseContextType | undefined>(undefined);
@@ -88,14 +87,10 @@ export function LicenseProvider({ children }: { children: ReactNode }) {
}
}, []);
const checkout = useCallback(() => {
window.open('https://sencho.io/#pricing', '_blank');
}, []);
const isPro = license?.tier === 'pro';
return (
<LicenseContext.Provider value={{ license, isPro, loading, refresh, activate, deactivate, checkout }}>
<LicenseContext.Provider value={{ license, isPro, loading, refresh, activate, deactivate }}>
{children}
</LicenseContext.Provider>
);