Files
vantage-app/adminsite/components/LicenceDelivery.tsx
T
2026-08-11 10:32:59 +01:00

87 lines
3.9 KiB
TypeScript

"use client";
import { useState } from "react";
import { Panel } from "./Panel";
/*
* A licence blob is signed public data, not a secret — it is useless on any
* instance other than the one it names. So it is safe to show inline, and
* showing it is what stops a blocked download from blocking a paying customer.
* That is also why it is never collapsed behind a toggle: someone whose
* clipboard and download are both blocked has to be able to select it by hand.
*
* It is evidence rather than content, so it is set in a well with a keyed strip
* saying what it is and how much of it there is, and given a fixed height. It
* used to run to 250px of base64 and was the largest thing on the page, which
* is a strange amount of room to give a string nobody reads.
*
* The download lives in the page header beside Renew, not here — it was in both
* places, which is one button too many for one file.
*/
export function LicenceDelivery({ blob }: { instanceId: string; blob: string; downloadUrl: string }) {
const [copied, setCopied] = useState(false);
async function copy() {
try {
await navigator.clipboard.writeText(blob);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Clipboard is refused without a secure context or a gesture the
// browser trusts. The blob is on screen and selectable either way,
// so this needs no error state.
}
}
const steps = [
<>
Open <Code>Settings Licence</Code> on your install.
</>,
<>Paste the licence into the box and save.</>,
<>
The page reports <Code>Valid</Code> straight away no restart.
</>,
];
return (
<Panel title="Your licence" meta="Paste into your install">
<div className="grid gap-2">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<span className="font-mono text-[0.64rem] uppercase tracking-[0.14em] text-ink-3">Licence key</span>
<span className="font-mono text-[0.64rem] uppercase tracking-[0.12em] text-ink-3">{blob.length.toLocaleString()} characters</span>
</div>
<div className="relative">
{/* Dashed, because this is data to be carried somewhere else
rather than a surface to read. */}
<pre className="max-h-32 overflow-y-auto whitespace-pre-wrap break-all rounded border border-dashed border-rule bg-panel-2 p-3 pr-24 font-mono text-[0.7rem] leading-relaxed text-ink-2">
{blob}
</pre>
<button
type="button"
onClick={copy}
className="absolute right-2 top-2 rounded border border-rule bg-panel px-2.5 py-1 font-mono text-[0.66rem] uppercase tracking-[0.1em] text-ink-2 hover:border-accent hover:text-accent"
>
{copied ? "Copied" : "Copy"}
</button>
</div>
</div>
{/* Numbered because this is an actual sequence — each step is only
possible once the one before it is done. */}
<ol className="grid gap-2">
{steps.map((body, i) => (
<li key={i} className="grid grid-cols-[1.5rem_1fr] items-start gap-3 text-[0.84rem] text-ink-2">
<span className="grid h-[1.4rem] place-items-center rounded-sm border border-rule font-mono text-[0.68rem] text-accent">{i + 1}</span>
<span className="leading-[1.4rem]">{body}</span>
</li>
))}
</ol>
</Panel>
);
}
function Code({ children }: { children: React.ReactNode }) {
return <code className="rounded-sm bg-accent-wash px-1 font-mono text-[0.8rem] text-ink">{children}</code>;
}