Add public /install page with Obtainium button
Served unauthenticated at /install: one-tap "Add to Obtainium" deep link, how to get Obtainium, direct-APK fallback to the Forgejo releases, and iPhone Add-to-Home-Screen steps. Detects the visitor's platform and shows it first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
83e2af6ed3
commit
d7fbb2a154
2 changed files with 119 additions and 0 deletions
117
backend/src/routes/install.ts
Normal file
117
backend/src/routes/install.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import type { FastifyInstance } from "fastify";
|
||||
|
||||
// Public install landing page. Points Obtainium at the Forgejo repo and gives
|
||||
// iPhone PWA instructions. Served unauthenticated at /install.
|
||||
const REPO_URL = "https://git.mowden.top/Beartaria/CampgroundTickets";
|
||||
const OBTAINIUM_ADD = `obtainium://add/${REPO_URL}`;
|
||||
const RELEASES_URL = `${REPO_URL}/releases`;
|
||||
const OBTAINIUM_GET = "https://github.com/ImranR98/Obtainium/releases/latest";
|
||||
const PWA_URL = "https://scan.beartariacampgrounds.com/";
|
||||
|
||||
export async function installRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get("/install", async (_req, reply) => {
|
||||
reply.type("text/html").send(PAGE);
|
||||
});
|
||||
}
|
||||
|
||||
const PAGE = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0f1a12" />
|
||||
<title>Install Camp Scan</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: #0f1a12; color: #eaf2ec;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
line-height: 1.5; -webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.wrap { max-width: 560px; margin: 0 auto; padding: 28px 20px 56px; }
|
||||
header { text-align: center; margin-bottom: 28px; }
|
||||
.logo { font-size: 60px; line-height: 1; }
|
||||
h1 { font-size: 26px; margin: 10px 0 4px; }
|
||||
.sub { color: #9db3a4; font-size: 15px; margin: 0; }
|
||||
.card {
|
||||
background: #16241a; border: 1px solid #24382a; border-radius: 16px;
|
||||
padding: 20px; margin: 16px 0;
|
||||
}
|
||||
.card h2 { font-size: 19px; margin: 0 0 6px; display: flex; align-items: center; gap: 8px; }
|
||||
.card .lead { color: #c4d6c9; font-size: 14px; margin: 0 0 14px; }
|
||||
ol { margin: 0; padding-left: 20px; }
|
||||
ol li { margin: 8px 0; }
|
||||
a.btn {
|
||||
display: block; text-align: center; text-decoration: none;
|
||||
background: #25c05a; color: #06210f; font-weight: 800; font-size: 18px;
|
||||
padding: 16px; border-radius: 13px; margin: 14px 0;
|
||||
}
|
||||
a.btn.secondary { background: transparent; color: #eaf2ec; border: 1px solid #2e7d32; font-weight: 600; font-size: 15px; padding: 13px; }
|
||||
a.inline { color: #58d68d; text-decoration: none; font-weight: 600; }
|
||||
.note { font-size: 13px; color: #9db3a4; margin-top: 12px; }
|
||||
.divider { text-align: center; color: #6c8f74; font-size: 13px; margin: 8px 0; }
|
||||
code { background: #0f1a12; border: 1px solid #24382a; border-radius: 6px; padding: 1px 6px; font-size: 13px; word-break: break-all; }
|
||||
.platform-tag { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: #58d68d; }
|
||||
footer { text-align: center; color: #6c8f74; font-size: 12px; margin-top: 24px; }
|
||||
.hide { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="logo">🐻</div>
|
||||
<h1>Install Camp Scan</h1>
|
||||
<p class="sub">Ticket scanner for Beartaria Campgrounds gate staff</p>
|
||||
</header>
|
||||
|
||||
<!-- ANDROID -->
|
||||
<div class="card" id="android">
|
||||
<span class="platform-tag">Android</span>
|
||||
<h2>📲 Install & auto-update via Obtainium</h2>
|
||||
<p class="lead">Obtainium keeps the app updated straight from our server — no Play Store needed.</p>
|
||||
<ol>
|
||||
<li>Don't have Obtainium yet? <a class="inline" href="${OBTAINIUM_GET}">Download it here</a> and install the APK (you may need to allow "install unknown apps").</li>
|
||||
<li>Then tap the button below — it opens Obtainium with Camp Scan ready to add:</li>
|
||||
</ol>
|
||||
<a class="btn" href="${OBTAINIUM_ADD}">➕ Add Camp Scan to Obtainium</a>
|
||||
<p class="note">If the button doesn't open Obtainium: open Obtainium → <b>Add App</b> → paste <code>${REPO_URL}</code> → Add.</p>
|
||||
<div class="divider">— or —</div>
|
||||
<a class="btn secondary" href="${RELEASES_URL}">⬇︎ Download the APK directly</a>
|
||||
<p class="note">Direct installs won't auto-update — Obtainium is recommended.</p>
|
||||
</div>
|
||||
|
||||
<!-- IPHONE -->
|
||||
<div class="card" id="iphone">
|
||||
<span class="platform-tag">iPhone & iPad</span>
|
||||
<h2>🍎 Add to Home Screen</h2>
|
||||
<p class="lead">No App Store needed — it runs as a full-screen web app.</p>
|
||||
<ol>
|
||||
<li>Open <b>this page in Safari</b> (not Chrome): <code>${PWA_URL}install</code></li>
|
||||
<li>Tap the <b>Share</b> button, then <b>Add to Home Screen</b> → <b>Add</b>.</li>
|
||||
<li>Launch <b>Camp Scan</b> from your home screen and allow the camera.</li>
|
||||
</ol>
|
||||
<a class="btn secondary" href="${PWA_URL}">Open Camp Scan now</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>🔑 First launch</h2>
|
||||
<p class="lead" style="margin-bottom:0">Open the app, enter the gate <b>PIN</b>, then type <b>your name</b> (recorded with every check-in). You stay signed in for the event.</p>
|
||||
</div>
|
||||
|
||||
<footer>Beartaria Campgrounds · scan.beartariacampgrounds.com</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Reorder so the visitor's platform shows first.
|
||||
try {
|
||||
var ua = navigator.userAgent || "";
|
||||
var isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
if (isIOS) {
|
||||
var ip = document.getElementById("iphone");
|
||||
ip.parentNode.insertBefore(ip, document.getElementById("android"));
|
||||
}
|
||||
} catch (e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
|
@ -11,6 +11,7 @@ import { authRoutes } from "./routes/auth.js";
|
|||
import { webhookRoutes } from "./routes/webhook.js";
|
||||
import { ticketRoutes } from "./routes/tickets.js";
|
||||
import { testRoutes } from "./routes/test.js";
|
||||
import { installRoutes } from "./routes/install.js";
|
||||
|
||||
export async function build() {
|
||||
const config = loadConfig();
|
||||
|
|
@ -30,6 +31,7 @@ export async function build() {
|
|||
await app.register(webhookRoutes);
|
||||
await app.register(ticketRoutes);
|
||||
await app.register(testRoutes);
|
||||
await app.register(installRoutes);
|
||||
|
||||
// Serve the exported Expo web build (if present) with SPA fallback.
|
||||
const webDir = config.WEB_DIR ?? join(process.cwd(), "web");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue