"use client"; import { useState, useEffect } from "react"; import { LicenseKeyCache } from "@server/license/license"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; import { LicenseKeysDataTable } from "./LicenseKeysDataTable"; import { AxiosResponse } from "axios"; import { Button } from "@app/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { useRouter } from "next/navigation"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { SettingsContainer, SettingsSectionTitle as SSTitle, SettingsSection, SettingsSectionDescription, SettingsSectionGrid, SettingsSectionHeader, SettingsSectionFooter } from "@app/components/Settings"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Badge } from "@app/components/ui/badge"; import { Check, Heart, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; import CopyTextBox from "@app/components/CopyTextBox"; import { Progress } from "@app/components/ui/progress"; import { MinusCircle, PlusCircle } from "lucide-react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { SitePriceCalculator } from "./components/SitePriceCalculator"; import Link from "next/link"; import { Checkbox } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; import { useTranslations } from "next-intl"; function obfuscateLicenseKey(key: string): string { if (key.length <= 8) return key; const firstPart = key.substring(0, 4); const lastPart = key.substring(key.length - 4); return `${firstPart}••••••••••••••••••••${lastPart}`; } export default function LicensePage() { const api = createApiClient(useEnvContext()); const [rows, setRows] = useState([]); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedLicenseKey, setSelectedLicenseKey] = useState(null); const router = useRouter(); const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext(); const [hostLicense, setHostLicense] = useState(null); const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false); const [purchaseMode, setPurchaseMode] = useState< "license" | "additional-sites" >("license"); // Separate loading states for different actions const [isInitialLoading, setIsInitialLoading] = useState(true); const [isActivatingLicense, setIsActivatingLicense] = useState(false); const [isDeletingLicense, setIsDeletingLicense] = useState(false); const [isRecheckingLicense, setIsRecheckingLicense] = useState(false); const { supporterStatus } = useSupporterStatusContext(); const t = useTranslations(); const formSchema = z.object({ licenseKey: z .string() .nonempty({ message: t('licenseKeyRequired') }) .max(255), agreeToTerms: z.boolean().refine((val) => val === true, { message: t('licenseTermsAgree') }) }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { licenseKey: "", agreeToTerms: false } }); useEffect(() => { async function load() { setIsInitialLoading(true); await loadLicenseKeys(); setIsInitialLoading(false); } load(); }, []); async function loadLicenseKeys() { try { const response = await api.get>( "/license/keys" ); const keys = response.data.data; setRows(keys); const hostKey = keys.find((key) => key.type === "HOST"); if (hostKey) { setHostLicense(hostKey.licenseKey); } else { setHostLicense(null); } } catch (e) { toast({ title: t('licenseErrorKeyLoad'), description: formatAxiosError( e, t('licenseErrorKeyLoadDescription') ) }); } } async function deleteLicenseKey(key: string) { try { setIsDeletingLicense(true); const encodedKey = encodeURIComponent(key); const res = await api.delete(`/license/${encodedKey}`); if (res.data.data) { updateLicenseStatus(res.data.data); } await loadLicenseKeys(); toast({ title: t('licenseKeyDeleted'), description: t('licenseKeyDeletedDescription') }); setIsDeleteModalOpen(false); } catch (e) { toast({ title: t('licenseErrorKeyDelete'), description: formatAxiosError( e, t('licenseErrorKeyDeleteDescription') ) }); } finally { setIsDeletingLicense(false); } } async function recheck() { try { setIsRecheckingLicense(true); const res = await api.post(`/license/recheck`); if (res.data.data) { updateLicenseStatus(res.data.data); } await loadLicenseKeys(); toast({ title: t('licenseErrorKeyRechecked'), description: t('licenseErrorKeyRecheckedDescription') }); } catch (e) { toast({ title: t('licenseErrorKeyRecheck'), description: formatAxiosError( e, t('licenseErrorKeyRecheckDescription') ) }); } finally { setIsRecheckingLicense(false); } } async function onSubmit(values: z.infer) { try { setIsActivatingLicense(true); const res = await api.post("/license/activate", { licenseKey: values.licenseKey }); if (res.data.data) { updateLicenseStatus(res.data.data); } toast({ title: t('licenseKeyActivated'), description: t('licenseKeyActivatedDescription') }); setIsCreateModalOpen(false); form.reset(); await loadLicenseKeys(); } catch (e) { toast({ variant: "destructive", title: t('licenseErrorKeyActivate'), description: formatAxiosError( e, t('licenseErrorKeyActivateDescription') ) }); } finally { setIsActivatingLicense(false); } } if (isInitialLoading) { return null; } return ( <> { setIsPurchaseModalOpen(val); }} mode={purchaseMode} /> { setIsCreateModalOpen(val); form.reset(); }} > {t('licenseActivateKey')} {t('licenseActivateKeyDescription')}
( {t('licenseKey')} )} /> (
{t('licenseAgreement')} {/*
*/} {/* */} {/* {t('fossorialLicense')} */} {/* */}
)} />
{selectedLicenseKey && ( { setIsDeleteModalOpen(val); setSelectedLicenseKey(null); }} dialog={

{t('licenseQuestionRemove', {selectedKey: obfuscateLicenseKey(selectedLicenseKey.licenseKey)})}

{t('licenseMessageRemove')}

{t('licenseMessageConfirm')}

} buttonText={t('licenseKeyDeleteConfirm')} onConfirm={async () => deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted) } string={selectedLicenseKey.licenseKey} title={t('licenseKeyDelete')} /> )} {t('licenseAbout')} {t('licenseAboutDescription')} {t('licenseHost')} {t('licenseHostDescription')}
{licenseStatus?.isLicenseValid ? (
{licenseStatus?.tier === "PROFESSIONAL" ? t('licenseTierCommercial') : licenseStatus?.tier === "ENTERPRISE" ? t('licenseTierCommercial') : t('licensed')}
) : (
{supporterStatus?.visible ? (
{t('communityEdition')}
) : (
{t('communityEdition')}
)}
)}
{licenseStatus?.hostId && (
{t('hostId')}
)} {hostLicense && (
{t('licenseKey')}
)}
{t('licenseSiteUsage')} {t('licenseSiteUsageDecsription')}
{t('licenseSitesUsed', {count: licenseStatus?.usedSites || 0})}
{!licenseStatus?.isHostLicensed && (

{t('licenseNoSiteLimit')}

)} {licenseStatus?.maxSites && (
{t('licenseSitesUsedMax', {usedSites: licenseStatus.usedSites || 0, maxSites: licenseStatus.maxSites})} {Math.round( ((licenseStatus.usedSites || 0) / licenseStatus.maxSites) * 100 )} %
)}
{/* */} {/* {!licenseStatus?.isHostLicensed ? ( */} {/* <> */} {/* */} {/* */} {/* ) : ( */} {/* <> */} {/* */} {/* */} {/* )} */} {/* */}
{ setSelectedLicenseKey(key); setIsDeleteModalOpen(true); }} onCreate={() => setIsCreateModalOpen(true)} />
); }