import { useState } from "react"; import { useTranslation } from "react-i18next"; import { openUrl } from "@tauri-apps/plugin-opener"; import { KeyRound, CheckCircle, AlertCircle, Loader2, ExternalLink, Monitor, ChevronDown, ChevronUp } from "lucide-react"; import { licenseIdentity, useLicenseContext } from "../../contexts/LicenseContext"; import { useLicenseMachines } from "../../hooks/useLicenseMachines"; import { parseLicenseError, type LicenseError } from "../../services/licenseService"; import { purchaseUrl } from "../../shared/purchaseUrl"; import { licenseExpiryDisplay } from "../../shared/licenseExpiry"; import { logError } from "../../services/logService"; import LicenseKeyForm from "./LicenseKeyForm"; /** * License card, keyed by the stored license (#347): another key has its own activation * and machines, so the body remounts. That drops the previous key's status and list, * and re-reads the activation, which `store_license` has just cleared for a new key. */ export default function LicenseCard() { const { info } = useLicenseContext(); return ; } function LicenseCardBody() { const { t, i18n } = useTranslation(); const { status: licenseStatus, edition, info, error, validating, validationError, submitKey } = useLicenseContext(); const [keyInput, setKeyInput] = useState(""); const [showInput, setShowInput] = useState(false); const hasLicense = edition !== "free"; const { state: machines, revoked, activate, deactivate, toggleList, showEmptyState, canDeactivate, canActivate, } = useLicenseMachines(hasLicense); const dateLocale = i18n.language === "fr" ? "fr-CA" : "en-CA"; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = keyInput.trim(); if (!trimmed) return; const result = await submitKey(trimmed); if (result.ok) { setKeyInput(""); setShowInput(false); } }; const handlePurchase = () => { void openUrl(purchaseUrl(i18n.language)).catch((e) => logError("Product page: openUrl failed", e), ); }; const errorMessage = (err: LicenseError) => t(err.i18nKey, err.params); // A revoked key is named once, at card level (#347): not repeated under Machines. Since // #365 the revocation is persisted and the edition of a revoked key is Free, so the // Machines section is gone with it; these branches only still show under a paid edition // that comes from the Maximus account while a revoked key is stored. const shownUnderMachines = (err: LicenseError | null) => err && err.code !== "license_revoked" ? err : null; const activationError = shownUnderMachines(machines.activationError); const listError = shownUnderMachines(machines.listError); const editionLabel = t(`license.editions.${edition}`); // No expiry date for a revoked key: it would read as a date until which it works. const expiry = info && !revoked && info.expires_at > 0 ? licenseExpiryDisplay(info.expires_at, Math.floor(Date.now() / 1000), dateLocale) : null; return (

{t("license.title")}

{t("license.currentEdition")}

{editionLabel} {edition !== "free" && (revoked ? ( ) : ( ))}

{expiry && (
{expiry.kind === "date" ? ( <>

{t("license.expiresAt")}

{expiry.text}

) : (

{t("license.noExpiry")}

)}
)}
{revoked && (

{t("license.errors.licenseRevoked")}

)} {licenseStatus === "error" && error && (

{errorMessage(parseLicenseError(error))}

)} {!showInput && (
{edition === "free" && ( )}
)} {showInput && ( { setShowInput(false); setKeyInput(""); }} validating={validating} busy={machines.activating} validationError={validationError} /> )} {hasLicense && (

{t("license.machines.title")}

{machines.activation && !machines.activation.is_activated && ( <> {t("license.machines.notActivated")} )} {/* A revoked key has no activated machine left on the server. */} {machines.activation?.is_activated && !revoked && ( {t("license.machines.activated")} )}
{activationError && (

{errorMessage(activationError)}

)} {machines.listOpen && (
{machines.listLoading && machines.machines.length === 0 && (
{t("common.loading")}
)} {listError && (

{errorMessage(listError)}

)} {showEmptyState && (

{t("license.machines.noMachines")}

)} {/* The limit comes from the signed key: no count without one (an account Premium has no stored key). An empty list says so on its own line. */} {info && machines.listLoaded && machines.listError === null && machines.machines.length > 0 && (

{t("license.machines.count", { count: machines.machines.length, limit: info.machine_limit, })}

)} {machines.machines.map((m) => { const isThis = m.is_this_machine; const deactivating = machines.deactivatingId === m.machine_id; return (
{m.machine_name || m.machine_id.slice(0, 12)} {isThis && ( ({t("license.machines.thisMachine")}) )}

{new Date(m.activated_at).toLocaleDateString(dateLocale)}

); })}
)}
)}
); }