fix(license): shared invalid/grace banners + instance copy

Surface expired/over_instance/revoked red banners with CTAs via a
shared LicenseStatusBanners used by settings and ProGate. Hide seats
in product UI; keep HMAC dual-path and never silent-clear keys.
This commit is contained in:
opsx committed 2026-11-01 03:08:37 +08:00
1 parent af0298e994
commit 62d9b81a70
10 files changed
+265 -45

No files matched your search

+18 -3
View File
@@ -1,4 +1,4 @@
import { memo, useState } from 'react'
import { memo, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import { useTranslation } from 'react-i18next'
import { Lock, Sparkles } from 'lucide-react'
@@ -6,16 +6,23 @@ import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { LicenseStatusBanners } from '@/components/LicenseStatusBanners'
import { licenseApi } from '@/api/license'
import { useLicense } from '@/hooks/useLicense'
/** Pro 功能升级引导卡:免费版进入 Pro 页面时整页替代展示 */
export const ProGate = memo(function ProGate({ featureKey }: { featureKey: string }) {
const { t } = useTranslation('license')
const { reload } = useLicense()
const { pro, info, reload } = useLicense()
const navigate = useNavigate()
const [key, setKey] = useState('')
const [activating, setActivating] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
const focusKey = () => {
inputRef.current?.focus()
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
const activate = async () => {
if (!key.trim()) return
@@ -33,6 +40,7 @@ export const ProGate = memo(function ProGate({ featureKey }: { featureKey: strin
return (
<div className="mx-auto max-w-2xl space-y-4 pt-8">
<LicenseStatusBanners info={info} pro={pro} onAction={focusKey} />
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
@@ -50,6 +58,7 @@ export const ProGate = memo(function ProGate({ featureKey }: { featureKey: strin
</li>
))}
</ul>
{/* 价卡暂留;product/plan 建好后再对齐 trial/年付/永久 */}
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-lg border p-4">
<p className="text-sm font-semibold">{t('pricing.personalTitle')}</p>
@@ -63,7 +72,13 @@ export const ProGate = memo(function ProGate({ featureKey }: { featureKey: strin
</div>
</div>
<div className="flex gap-2">
<Input value={key} onChange={e => setKey(e.target.value)} placeholder={t('gate.keyPlaceholder')} className="font-mono text-sm" />
<Input
ref={inputRef}
value={key}
onChange={e => setKey(e.target.value)}
placeholder={t('gate.keyPlaceholder')}
className="font-mono text-sm"
/>
<Button onClick={() => void activate()} disabled={activating || !key.trim()}>{t('gate.activate')}</Button>
<Button variant="outline" onClick={() => navigate('/settings')}>{t('gate.toSettings')}</Button>
</div>