209 lines
7.2 KiB
TypeScript
209 lines
7.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useLicenseStore } from '../store/licenseStore';
|
|
import { Key, Check, X, Loader2, Shield, Clock, AlertTriangle } from 'lucide-react';
|
|
|
|
export default function LicenseDialog() {
|
|
const { status, showDialog, setShowDialog, activateLicense } = useLicenseStore();
|
|
const [key, setKey] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [activating, setActivating] = useState(false);
|
|
|
|
const handleActivate = async () => {
|
|
if (!key.trim()) return;
|
|
setActivating(true);
|
|
setError(null);
|
|
|
|
const ok = await activateLicense(key.trim());
|
|
if (!ok) {
|
|
setError('Invalid license key. Check it was entered correctly.');
|
|
}
|
|
setActivating(false);
|
|
};
|
|
|
|
const formatDate = (ts: number) => {
|
|
const d = new Date(ts * 1000);
|
|
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
};
|
|
|
|
if (!status) return null;
|
|
|
|
if (status.tag === 'Licensed') {
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-50">
|
|
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-editor-surface border border-editor-border shadow-lg text-xs">
|
|
<Shield className="w-3.5 h-3.5 text-editor-success" />
|
|
<span className="text-editor-text-muted">
|
|
{status.license.tier === 'business' ? 'Business' : 'Pro'} — {status.license.customer_email}
|
|
</span>
|
|
<span className="text-editor-text-muted/50">
|
|
expires {formatDate(status.license.expires_at)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status.tag === 'Trial') {
|
|
return (
|
|
<>
|
|
<div className="fixed bottom-4 right-4 z-50">
|
|
<button
|
|
onClick={() => setShowDialog(true)}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-editor-surface border border-editor-border shadow-lg text-xs hover:bg-editor-bg transition-colors"
|
|
>
|
|
<Clock className="w-3.5 h-3.5 text-editor-accent" />
|
|
<span className="text-editor-text-muted">
|
|
Trial — {status.days_remaining} day{status.days_remaining !== 1 ? 's' : ''} left
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
{showDialog && (
|
|
<LicenseActivateDialog
|
|
onClose={() => setShowDialog(false)}
|
|
onActivate={handleActivate}
|
|
keyValue={key}
|
|
setKeyValue={setKey}
|
|
error={error}
|
|
activating={activating}
|
|
trialEnding={status.days_remaining <= 3}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Expired — show banner + activation dialog (both dismissible)
|
|
return (
|
|
<>
|
|
<ExpiredBanner onActivate={() => setShowDialog(true)} />
|
|
|
|
{showDialog && (
|
|
<LicenseActivateDialog
|
|
onClose={() => setShowDialog(false)}
|
|
onActivate={handleActivate}
|
|
keyValue={key}
|
|
setKeyValue={setKey}
|
|
error={error}
|
|
activating={activating}
|
|
expired
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/** Persistent top banner shown when trial expired — still allows export and loading */
|
|
function ExpiredBanner({ onActivate }: { onActivate: () => void }) {
|
|
return (
|
|
<div className="h-9 flex items-center justify-center gap-3 px-4 bg-red-500/15 border-b border-red-500/30 shrink-0">
|
|
<AlertTriangle className="w-3.5 h-3.5 text-red-400 shrink-0" />
|
|
<span className="text-xs text-red-300">
|
|
Trial expired — export and project loading still work.
|
|
<button onClick={onActivate} className="underline font-medium hover:text-red-200">
|
|
Activate license
|
|
</button>
|
|
to restore editing.
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LicenseActivateDialog({
|
|
onClose,
|
|
onActivate,
|
|
keyValue,
|
|
setKeyValue,
|
|
error,
|
|
activating,
|
|
trialEnding,
|
|
expired,
|
|
}: {
|
|
onClose: () => void;
|
|
onActivate: () => void;
|
|
keyValue: string;
|
|
setKeyValue: (v: string) => void;
|
|
error: string | null;
|
|
activating: boolean;
|
|
trialEnding?: boolean;
|
|
expired?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/60 px-4">
|
|
<div
|
|
className="w-full max-w-md rounded-xl border border-editor-border bg-editor-bg p-6 space-y-4"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Key className="w-5 h-5 text-editor-accent" />
|
|
<h3 className="text-sm font-semibold">
|
|
{expired ? 'Trial Expired' : 'Activate TalkEdit'}
|
|
</h3>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 rounded hover:bg-editor-surface text-editor-text-muted"
|
|
title="Close dialog"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{expired && (
|
|
<div className="text-xs text-editor-text-muted leading-relaxed space-y-1">
|
|
<p className="font-medium text-red-300">Your 30-day trial has ended.</p>
|
|
<p>
|
|
You can still <strong>export videos</strong> and <strong>load projects</strong>.
|
|
Enter a license key to restore editing, AI tools, and all other features.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{trialEnding && !expired && (
|
|
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30">
|
|
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
|
<p className="text-xs text-amber-300">Your trial ends soon. Activate now to keep using all features.</p>
|
|
</div>
|
|
)}
|
|
|
|
{!expired && !trialEnding && (
|
|
<p className="text-xs text-editor-text-muted leading-relaxed">
|
|
Enter your license key to activate TalkEdit Pro or Business.
|
|
You received this key by email after purchase.
|
|
</p>
|
|
)}
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs text-editor-text-muted font-medium">License Key</label>
|
|
<textarea
|
|
value={keyValue}
|
|
onChange={(e) => { setKeyValue(e.target.value); }}
|
|
placeholder="talkedit_v1_..."
|
|
rows={3}
|
|
className="w-full px-3 py-2 text-xs font-mono bg-editor-surface border border-editor-border rounded-lg text-editor-text placeholder:text-editor-text-muted/50 focus:outline-none focus:border-editor-accent resize-none"
|
|
/>
|
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
|
</div>
|
|
|
|
<button
|
|
onClick={onActivate}
|
|
disabled={activating || !keyValue.trim()}
|
|
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-editor-accent hover:bg-editor-accent-hover disabled:opacity-50 rounded-lg text-sm font-medium transition-colors"
|
|
>
|
|
{activating ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Check className="w-4 h-4" />
|
|
)}
|
|
Activate
|
|
</button>
|
|
|
|
<p className="text-[10px] text-editor-text-muted text-center">
|
|
No license? <a href="#" className="text-editor-accent hover:underline">Purchase at talked.it</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|