import { useEffect, useRef, useState } from "preact/hooks";
import { renderSVG } from "uqr";
import { localFetch, localGet } from "../lib/api.js";
const POLL_MS = 2000;
const IDLE_MS = 10000;
const TAIL_LINES = 12;
function QrCode({ url }) {
const svg = renderSVG(url, {
pixelSize: 4,
whiteColor: "#ffffff",
blackColor: "#0f1115",
});
return (
<div
class="install-qr"
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}
function origin() {
if (typeof window === "undefined") return "https://localhost";
return window.location.origin;
}
function BuildLog({ output }) {
const lines = output.slice(-TAIL_LINES);
if (lines.length === 0) return null;
return <pre class="install-log">{lines.join("\n")}</pre>;
}
function ApkSection({ apkUrl }) {
const [status, setStatus] = useState(null);
const [requestError, setRequestError] = useState(null);
const [starting, setStarting] = useState(false);
const poll = useRef(null);
useEffect(() => {
let live = true;
let timer = null;
const tick = async () => {
const next = await localGet("/api/install/apk/status").catch((e) => {
if (live)
setRequestError(
`Can't reach the server for build status: ${e.message}`,
);
return null;
});
if (!live) return;
if (next) {
setRequestError(null);
setStatus(next);
}
timer = setTimeout(tick, next?.phase === "running" ? POLL_MS : IDLE_MS);
};
poll.current = () => {
clearTimeout(timer);
tick();
};
tick();
return () => {
live = false;
poll.current = null;
clearTimeout(timer);
};
}, []);
const onBuild = async () => {
setStarting(true);
setRequestError(null);
const res = await localFetch("/api/install/apk/build", {
method: "POST",
}).catch((e) => {
setRequestError(`Couldn't start the build: ${e.message}`);
return null;
});
setStarting(false);
if (!res) return;
if (!res.ok && res.status !== 409) {
const body = await res.text().catch(() => "");
setRequestError(
`Couldn't start the build (HTTP ${res.status}). ${body}`.trim(),
);
return;
}
poll.current?.();
};
const phase = status?.phase;
const running = phase === "running";
const failed = phase === "failed";
const available = !!status?.apk_available;
const output = Array.isArray(status?.output) ? status.output : [];
const domain = status?.domain;
return (
<section class="install-card">
<h2>2. Install the app</h2>
{status === null && !requestError && (
<p class="install-hint">Checking…</p>
)}
{available && (
<>
<p class="install-lede">
Download the Android APK, or scan the QR with your phone.
</p>
<div class="install-grid">
<a
class="install-btn"
href="/install/mobux.apk"
download="mobux.apk"
>
Download APK
</a>
<QrCode url={apkUrl} />
</div>
</>
)}
{status !== null && !available && !running && (
<div class="install-apk-missing" role="alert">
<p class="install-apk-missing-title">
No package has been built for this server yet.
</p>
<p class="install-hint">
Building one takes a few minutes and signs it for{" "}
<code>{domain || "this server"}</code>.
</p>
</div>
)}
{running && (
<p class="install-lede">
Building the package for <code>{domain || "this server"}</code>. This
takes a few minutes — you can leave this page open.
</p>
)}
{failed && (
<div class="install-error" role="alert">
<strong>The package build failed.</strong>
<p>{status?.error || "The build exited without an error message."}</p>
<BuildLog output={output} />
</div>
)}
{requestError && (
<div class="install-error" role="alert">
<strong>{requestError}</strong>
</div>
)}
{status?.domain_error && (
<p class="install-hint">
The server can't work out which address to sign the package for:{" "}
{status.domain_error}. Set <code>MOBUX_DOMAIN</code> on the service.
</p>
)}
{status !== null && (
<div class="install-grid install-build-row">
<button
type="button"
class="install-btn"
onClick={onBuild}
disabled={running || starting || !!status?.domain_error}
>
{running
? "Building…"
: available
? "Rebuild package"
: "Generate package"}
</button>
{available && !running && (
<span class="install-hint">
Rebuild if you reach this server on a different address.
</span>
)}
</div>
)}
{running && <BuildLog output={output} />}
</section>
);
}
export function InstallPage() {
const base = origin();
const apkUrl = base + "/install/mobux.apk";
const caUrl = base + "/install/mobux-ca.crt";
return (
<main class="install-page">
<section class="install-card">
<h2>1. Install the CA certificate</h2>
<p class="install-lede">
Do this <strong>first</strong>. Without the CA, Android won't trust
this server, the APK download will be blocked, and the installed app
won't connect.
</p>
<div class="install-grid">
<a
class="install-btn"
href="/install/mobux-ca.crt"
download="mobux-ca.crt"
>
Download CA certificate
</a>
<QrCode url={caUrl} />
</div>
<p class="install-hint">
After downloading, install it through Android Settings:
</p>
<ol class="install-steps">
<li>Settings → Security & privacy (or just Security)</li>
<li>More security settings → Encryption & credentials</li>
<li>Install a certificate → CA certificate</li>
<li>
Acknowledge the warning, pick <code>mobux-ca.crt</code> from your
Downloads
</li>
</ol>
<p class="install-hint">
Running with ACME / a publicly-trusted cert? Skip this step — there's
no local CA to install.
</p>
</section>
<ApkSection apkUrl={apkUrl} />
</main>
);
}