87 lines
3.9 KiB
TypeScript
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>;
|
|
}
|