"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"; const formSchema = z.object({ licenseKey: z .string() .nonempty({ message: "License key is required" }) .max(255), agreeToTerms: z.boolean().refine((val) => val === true, { message: "You must agree to the license terms" }) }); 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 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: "Failed to load license keys", description: formatAxiosError( e, "An error occurred loading license keys" ) }); } } 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: "License key deleted", description: "The license key has been deleted" }); setIsDeleteModalOpen(false); } catch (e) { toast({ title: "Failed to delete license key", description: formatAxiosError( e, "An error occurred deleting license key" ) }); } 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: "License keys rechecked", description: "All license keys have been rechecked" }); } catch (e) { toast({ title: "Failed to recheck license keys", description: formatAxiosError( e, "An error occurred rechecking license keys" ) }); } 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: "License key activated", description: "The license key has been successfully activated." }); setIsCreateModalOpen(false); form.reset(); await loadLicenseKeys(); } catch (e) { toast({ variant: "destructive", title: "Failed to activate license key", description: formatAxiosError( e, "An error occurred while activating the license key." ) }); } finally { setIsActivatingLicense(false); } } if (isInitialLoading) { return null; } return ( <> { setIsPurchaseModalOpen(val); }} mode={purchaseMode} /> { setIsCreateModalOpen(val); form.reset(); }} > Activate License Key Enter a license key to activate it.
( License Key )} /> (
By checking this box, you confirm that you have read and agree to the license terms corresponding to the tier associated with your license key.
View Fossorial Commercial License & Subscription Terms
)} />
{selectedLicenseKey && ( { setIsDeleteModalOpen(val); setSelectedLicenseKey(null); }} dialog={

Are you sure you want to delete the license key{" "} {obfuscateLicenseKey( selectedLicenseKey.licenseKey )} ?

This will remove the license key and all associated permissions granted by it.

To confirm, please type the license key below.

} buttonText="Confirm Delete License Key" onConfirm={async () => deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted) } string={selectedLicenseKey.licenseKey} title="Delete License Key" /> )} About Licensing This is for business and enterprise users who are using Pangolin in a commercial environment. If you are using Pangolin for personal use, you can ignore this section. Host License Manage the main license key for the host.
{licenseStatus?.isLicenseValid ? (
{licenseStatus?.tier === "PROFESSIONAL" ? "Commercial License" : licenseStatus?.tier === "ENTERPRISE" ? "Commercial License" : "Licensed"}
) : (
{supporterStatus?.visible ? (
Community Edition
) : (
Community Edition
)}
)}
{licenseStatus?.hostId && (
Host ID
)} {hostLicense && (
License Key
)}
Sites Usage View the number of sites using this license.
{licenseStatus?.usedSites || 0}{" "} {licenseStatus?.usedSites === 1 ? "site" : "sites"}{" "} in system
{!licenseStatus?.isHostLicensed && (

There is no limit on the number of sites using an unlicensed host.

)} {licenseStatus?.maxSites && (
{licenseStatus.usedSites || 0} of{" "} {licenseStatus.maxSites} sites used {Math.round( ((licenseStatus.usedSites || 0) / licenseStatus.maxSites) * 100 )} %
)}
{!licenseStatus?.isHostLicensed ? ( <> ) : ( <> )}
{ setSelectedLicenseKey(key); setIsDeleteModalOpen(true); }} onCreate={() => setIsCreateModalOpen(true)} />
); }