Portiert mail-gateway/internal/license (Verify, Cache, Trial, Signature) + DB-Mirror (internal/services/license) + REST-Handler (status/verify/key/clear) + UI-Page /license (Activate, Status, Limits, Features, Re-verify) + <LicenseBanner /> neben UpdateBanner (trial-expiring, expired, verify-failed) + Scheduler: täglich Re-verify (24h-Tick) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
2.2 KiB
TypeScript
92 lines
2.2 KiB
TypeScript
import { Alert } from 'antd'
|
|
import { Link } from 'react-router-dom'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import apiClient, { isEnvelope } from '../api/client'
|
|
|
|
interface LicenseStatus {
|
|
license_key?: string
|
|
status: string
|
|
type?: string
|
|
valid_until?: string
|
|
expires_at?: string
|
|
last_error?: string | null
|
|
}
|
|
|
|
// LicenseBanner shows up in AppLayout next to UpdateBanner.
|
|
// Three visible states:
|
|
// - trial-expiring (≤14d remaining) → warning
|
|
// - expired / invalid → error
|
|
// - last verify failed → warning (transient)
|
|
// Everything else stays silent.
|
|
export default function LicenseBanner() {
|
|
const { t } = useTranslation()
|
|
|
|
const { data: s } = useQuery({
|
|
queryKey: ['license', 'status'],
|
|
queryFn: async () => {
|
|
const r = await apiClient.get('/license/status')
|
|
return isEnvelope(r.data) ? (r.data.data as LicenseStatus) : null
|
|
},
|
|
refetchInterval: 5 * 60 * 1000,
|
|
})
|
|
|
|
if (!s) return null
|
|
|
|
const expiry = s.valid_until || s.expires_at
|
|
const days = expiry ? Math.ceil((new Date(expiry).getTime() - Date.now()) / 86_400_000) : null
|
|
const isTrial = s.type === 'trial' || !s.license_key
|
|
|
|
if (s.status === 'expired' || s.status === 'invalid') {
|
|
return (
|
|
<Alert
|
|
type="error"
|
|
showIcon
|
|
banner
|
|
message={
|
|
<>
|
|
{t('licenseBanner.expired')}{' '}
|
|
<Link to="/license">{t('licenseBanner.cta')}</Link>
|
|
</>
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (isTrial && days !== null && days <= 14) {
|
|
return (
|
|
<Alert
|
|
type={days <= 3 ? 'error' : 'warning'}
|
|
showIcon
|
|
banner
|
|
message={
|
|
<>
|
|
{t('licenseBanner.trialExpiring', { days })}{' '}
|
|
<Link to="/license">{t('licenseBanner.cta')}</Link>
|
|
</>
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (s.last_error) {
|
|
return (
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
banner
|
|
message={
|
|
<>
|
|
{t('licenseBanner.verifyFailed')}: {s.last_error}{' '}
|
|
<Link to="/license">{t('licenseBanner.openPage')}</Link>
|
|
</>
|
|
}
|
|
closable
|
|
/>
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|