Files
hope-events/frontend/src/components/shared/LicenceBanner.tsx
T
joshuaandClaude Opus 5.5 e58787ae7a Licence: no licence is read-only, editing events blocked, heartbeat 30 s after start
- A site the Hub answers INSTALL_NOT_FOUND for drops its install and licence, so it
  no longer shows "Active" on the old token.
- Unlicensed is read-only like suspended; read-only also blocks editing events,
  options and variants. LICENCE_CHECK_DISABLED (ignored in production) for e2e.
- Heartbeat 30 s after the server starts and after a licence key is connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:45:38 +02:00

63 lines
2.7 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { ShieldAlert, KeyRound, ArrowUpCircle } from "lucide-react";
import { apiFetch } from "@/lib/api";
import { useAuth } from "@/hooks/useAuth";
import type { HubStatus } from "@/components/admin/LicenceTab";
/**
* Licence and update notices for **site admins only** (never supervisors, staff, users or the
* public site): grace period, suspended/revoked, not connected, update available.
*/
export function LicenceBanner() {
const { user, token } = useAuth();
const [status, setStatus] = useState<HubStatus | null>(null);
const isAdmin = user?.role === "admin";
useEffect(() => {
if (!isAdmin || !token) return;
apiFetch<HubStatus>("/api/hub/status", { authToken: token }).then(setStatus).catch(() => {});
}, [isAdmin, token]);
if (!isAdmin || !status) return null;
const link = <Link href="/dashboard/admin/settings?tab=licence" className="font-medium underline">Licence settings</Link>;
if (status.state === "grace") {
return (
<div role="status" className="bg-amber-50 border-b border-amber-200 text-amber-900 text-sm px-4 py-2 flex items-center gap-2">
<ShieldAlert className="w-4 h-4 shrink-0" />
<span>Licence needs renewing — {status.daysLeft} day{status.daysLeft === 1 ? "" : "s"} left. {link}</span>
</div>
);
}
if (status.state === "suspended" || status.state === "revoked") {
return (
<div role="alert" className="bg-red-50 border-b border-red-200 text-red-800 text-sm px-4 py-2 flex items-center gap-2">
<ShieldAlert className="w-4 h-4 shrink-0" />
<span>
The licence is {status.state === "revoked" ? "revoked" : "suspended"}: the site is read-only — events can&apos;t be created or edited, and no new registrations or payments. Tickets still scan and all data can be exported. {link}
</span>
</div>
);
}
if (status.state === "unlicensed" && status.readOnly) {
return (
<div role="alert" className="bg-red-50 border-b border-red-200 text-red-800 text-sm px-4 py-2 flex items-center gap-2">
<KeyRound className="w-4 h-4 shrink-0" />
<span>This site has no licence, so it&apos;s read-only — events can&apos;t be created or edited. Connect your licence key in {link}</span>
</div>
);
}
if (status.updateAvailable) {
return (
<div role="status" className="bg-brand-50 border-b border-brand-100 text-brand-800 text-sm px-4 py-2 flex items-center gap-2">
<ArrowUpCircle className="w-4 h-4 shrink-0" />
<span>Hope Events {status.latestVersion} is available (this site runs {status.version}). {link}</span>
</div>
);
}
return null;
}