docs: add comprehensive Stripe billing guide
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s

Covers architecture, configuration steps, user flows, API routes,
webhooks, pricing, testing with Stripe CLI, production checklist,
and troubleshooting.
This commit is contained in:
Antigravity
2026-05-16 21:10:26 +00:00
parent aa12d2226f
commit bb75b2e763
36 changed files with 2099 additions and 735 deletions

View File

@@ -62,7 +62,7 @@ export function TrashClient({
const all: TrashItem[] = [
...notes.map(n => ({
id: n.id,
title: n.title || t('notes.untitled') || 'Untitled',
title: n.title || t('notes.untitled'),
itemType: 'note' as const,
deletedAt: (n as any).trashedAt || null,
content: n.content,
@@ -101,9 +101,9 @@ export function TrashClient({
await restoreNb(item.id)
setNotebooks(prev => prev.filter((nb: any) => nb.id !== item.id))
}
toast.success(t('trash.restoreSuccess') || 'Restored successfully')
toast.success(t('trash.restoreSuccess'))
} catch {
toast.error(t('trash.restoreError') || 'Failed to restore')
toast.error(t('trash.restoreError'))
}
}
@@ -116,22 +116,22 @@ export function TrashClient({
await permDeleteNb(item.id)
setNotebooks(prev => prev.filter((nb: any) => nb.id !== item.id))
}
toast.success(t('trash.permanentDeleteSuccess') || 'Permanently deleted')
toast.success(t('trash.permanentDeleteSuccess'))
} catch {
toast.error(t('trash.deleteError') || 'Failed to delete')
toast.error(t('trash.deleteError'))
}
}
const handleEmptyTrash = async () => {
if (!window.confirm(t('trash.emptyTrashConfirm') || 'Empty trash? This is irreversible.')) return
if (!window.confirm(t('trash.emptyTrashConfirm'))) return
setIsEmptying(true)
try {
await emptyTrash()
setNotes([])
setNotebooks([])
toast.success(t('trash.emptyTrashSuccess') || 'Trash emptied')
toast.success(t('trash.emptyTrashSuccess'))
} catch {
toast.error(t('trash.deleteError') || 'Failed to empty trash')
toast.error(t('trash.deleteError'))
} finally {
setIsEmptying(false)
}
@@ -143,10 +143,10 @@ export function TrashClient({
<div className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-4xl font-memento-serif font-medium text-foreground flex items-center gap-4">
{t('sidebar.trash') || 'Trash'} <Trash2 size={28} className="text-rose-400 opacity-40" />
{t('sidebar.trash')} <Trash2 size={28} className="text-rose-400 opacity-40" />
</h1>
<p className="text-[10px] text-muted-foreground font-bold uppercase tracking-[0.3em] opacity-60">
{t('trash.autoDelete30') || 'Auto-delete after 30 days'}
{t('trash.autoDelete30')}
</p>
</div>
@@ -156,7 +156,7 @@ export function TrashClient({
disabled={isEmptying}
className="px-6 py-3 bg-card border border-border text-rose-500 rounded-2xl text-[10px] font-bold uppercase tracking-widest hover:bg-rose-50 hover:border-rose-100 transition-all shadow-sm disabled:opacity-50"
>
{t('trash.emptyTrash') || 'Empty all'}
{t('trash.emptyTrash')}
</button>
)}
</div>
@@ -166,7 +166,7 @@ export function TrashClient({
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-foreground transition-colors" size={16} />
<input
type="text"
placeholder={t('common.search') || 'Search...'}
placeholder={t('common.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white dark:bg-white/5 border border-border/40 rounded-2xl pl-12 pr-6 py-4 text-sm outline-none focus:ring-4 ring-foreground/5 transition-all shadow-sm"
@@ -181,7 +181,7 @@ export function TrashClient({
className={`px-6 py-3 rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all
${filterType === type ? 'bg-foreground text-background shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
>
{type === 'all' ? (t('trash.filterAll') || 'All') : type === 'notes' ? (t('nav.notes') || 'Notes') : (t('nav.notebooks') || 'Notebooks')}
{type === 'all' ? t('trash.filterAll') : type === 'notes' ? t('nav.notes') : t('nav.notebooks')}
</button>
))}
</div>
@@ -220,12 +220,12 @@ export function TrashClient({
onClick={() => handleRestore(item)}
className="flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-xl text-[10px] font-bold uppercase tracking-widest hover:bg-emerald-100 transition-colors"
>
<RotateCcw size={12} /> {t('trash.restore') || 'Restore'}
<RotateCcw size={12} /> {t('trash.restore')}
</button>
<button
onClick={() => handlePermanentDelete(item)}
className="p-2 hover:bg-rose-50 text-rose-500 rounded-xl transition-colors"
title={t('trash.permanentDelete') || 'Delete permanently'}
title={t('trash.permanentDelete')}
>
<X size={16} />
</button>
@@ -238,7 +238,7 @@ export function TrashClient({
</h3>
<div className="flex items-center gap-3">
<div className={`text-[9px] font-bold uppercase tracking-widest px-2 py-0.5 rounded border ${daysLeft < 5 ? 'border-rose-200 text-rose-500 bg-rose-50' : 'border-terreo/20 text-terreo bg-terreo/5'}`}>
{daysLeft} {t('trash.daysRemaining') || 'DAYS LEFT'}
{daysLeft} {t('trash.daysRemaining')}
</div>
{item.deletedAt && (
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-tight flex items-center gap-1">
@@ -255,7 +255,7 @@ export function TrashClient({
) : (
<div className="border-t border-border/40 pt-4">
<div className="text-[9px] font-bold text-muted-foreground/40 uppercase tracking-widest">
{t('trash.notebookContentPreserved') || 'Notebook content preserved'}
{t('trash.notebookContentPreserved')}
</div>
</div>
)}
@@ -271,10 +271,10 @@ export function TrashClient({
</div>
<div className="space-y-2">
<h2 className="text-2xl font-memento-serif text-foreground italic">
{t('trash.empty') || 'Trash is empty'}
{t('trash.empty')}
</h2>
<p className="text-sm text-muted-foreground max-w-xs">
{t('trash.emptyDescription') || 'Deleted items will appear here. They are kept for 30 days before permanent deletion.'}
{t('trash.emptyDescription')}
</p>
</div>
</div>
@@ -284,7 +284,7 @@ export function TrashClient({
<footer className="px-12 py-6 bg-white/50 dark:bg-white/5 border-t border-border flex items-center gap-4">
<AlertCircle size={14} className="text-muted-foreground" />
<p className="text-[10px] text-muted-foreground font-medium uppercase tracking-widest">
{t('trash.notebookRestoreHint') || 'Restoring a notebook also restores all its notes.'}
{t('trash.notebookRestoreHint')}
</p>
</footer>
</div>