import { useState } from 'react' import { Alert, Button, Card, Descriptions, Form, Input, Modal, Popconfirm, Space, Tag, Typography, message, } from 'antd' import { CrownOutlined, ReloadOutlined, SafetyCertificateOutlined } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import apiClient, { isEnvelope } from '../../api/client' import PageHeader from '../../components/PageHeader' const { Text, Paragraph } = Typography interface LicenseStatus { license_key?: string status: string type?: string valid?: boolean valid_until?: string expires_at?: string last_verified_at?: string last_verified_node?: string last_error?: string | null reason?: string payload?: { product?: string type?: string limits?: Record features?: Record } } function daysUntil(iso?: string): number | null { if (!iso) return null const ms = new Date(iso).getTime() - Date.now() return Math.ceil(ms / 86_400_000) } function statusTag(s: LicenseStatus, t: (k: string) => string) { if (s.status === 'active' && s.type === 'trial') return {t('license.tagTrial')} if (s.status === 'active') return {t('license.tagActive')} if (s.status === 'expired') return {t('license.tagExpired')} if (s.status === 'invalid') return {t('license.tagInvalid')} return {s.status} } export default function LicensePage() { const { t } = useTranslation() const qc = useQueryClient() const [open, setOpen] = useState(false) const [form] = Form.useForm<{ license_key: string }>() const { data: status, isLoading } = useQuery({ queryKey: ['license', 'status'], queryFn: async () => { const r = await apiClient.get('/license/status') return isEnvelope(r.data) ? (r.data.data as LicenseStatus) : null }, refetchInterval: 60_000, }) const setKey = useMutation({ mutationFn: async (key: string) => { const r = await apiClient.put('/license/key', { license_key: key }) return isEnvelope(r.data) ? r.data.data : r.data }, onSuccess: (res: { verify_error?: string }) => { if (res?.verify_error) { message.warning(t('license.savedButVerifyFailed') + ': ' + res.verify_error) } else { message.success(t('license.saved')) } setOpen(false) form.resetFields() qc.invalidateQueries({ queryKey: ['license'] }) }, onError: (e: Error) => message.error(e.message), }) const verify = useMutation({ mutationFn: async () => { await apiClient.post('/license/verify') }, onSuccess: () => { message.success(t('license.reverified')) qc.invalidateQueries({ queryKey: ['license'] }) }, onError: (e: Error) => message.error(e.message), }) const clear = useMutation({ mutationFn: async () => { await apiClient.delete('/license/key') }, onSuccess: () => { message.success(t('license.cleared')) qc.invalidateQueries({ queryKey: ['license'] }) }, onError: (e: Error) => message.error(e.message), }) const expiry = status?.valid_until ?? status?.expires_at const days = daysUntil(expiry) const isTrial = status?.type === 'trial' || (!status?.license_key && status?.status === 'active') return ( <> } extra={ } /> {isLoading && } {status?.last_error && ( )} {isTrial && days !== null && days <= 14 && ( )} {status ? statusTag(status, t) : '-'} {status?.payload?.product || (isTrial ? 'Trial' : '-')} {status?.license_key || '— ' + t('license.noKey') + ' —'} {expiry ? ( <> {new Date(expiry).toLocaleString()}{' '} {days !== null && ( {days < 0 ? t('license.expired') : t('license.daysLeft', { days })} )} ) : '-'} {status?.last_verified_at ? new Date(status.last_verified_at).toLocaleString() : '-'} {status?.last_verified_node && ( {status.last_verified_node} )} {status?.payload?.limits && Object.keys(status.payload.limits).length > 0 && ( <> {t('license.limits')} {Object.entries(status.payload.limits).map(([k, v]) => ( {v === 0 ? {t('license.unlimited')} : v} ))} )} {status?.payload?.features && Object.keys(status.payload.features).length > 0 && ( <> {t('license.features')} {Object.entries(status.payload.features).map(([k, v]) => ( {k} ))} )} {status?.license_key && (
clear.mutate()} >
)}
setOpen(false)} onOk={() => form.submit()} confirmLoading={setKey.isPending} okText={t('license.activate')} destroyOnHidden > {t('license.enterKeyHint')}
setKey.mutate(v.license_key.trim())}>
) }