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 (
{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")}
)}
)}
{/* 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 && (