feat(04-04): ONBD-03 home-screen hooks + the boot wiring + the ?dest= harness profile
- homescreen.ts: wireHomeScreen behind the BOTH-halves Bot API 8.0 guard; 'added' hides (no nag), 'unknown' (always iOS) shows the neutral prompt — indeterminate, never an error; homeScreenAdded hides; a refused addToHomeScreen is caught, the affordance stays; .btn-secondary in the existing strip — no new furniture; NO beforeinstallprompt/manifest/service worker (T-04-38) - main.ts: locale derivation moved above the bindings block; stage 13 reads BOTH destination carriers, seeds the memory-only holder, builds the destination/unavailable surface (null when no id — Phase 3 boot byte-identical, D-12); the live register shows the destination surface; the ARAHARAH-DEST state-token signal fires unconditionally - index.html: ?dest=<id> harness profile threaded to the mock's startParam; the block stays inside __ARAHARAH_HARNESS__ - HS1-HS9 green (54/54), web:typecheck green
This commit is contained in:
+10
-2
@@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
/* D-11 Mock-Telegram profile: ?mock=android|ios|tdesktop|macos|web + ?scheme=light|dark + ?pin=1 + ?locale=en|ms + ?fixdelay + ?sessiondelay
|
||||
/* D-11 Mock-Telegram profile: ?mock=android|ios|tdesktop|macos|web + ?scheme=light|dark + ?pin=1 + ?locale=en|ms + ?fixdelay + ?sessiondelay + ?dest=<id>
|
||||
Harness-only: served from web/dist-harness, never deployed.
|
||||
The WHOLE block below is inside `if (__ARAHARAH_HARNESS__)` — Vite's `define`
|
||||
replaces that identifier with `false` in the production build, so the branch is
|
||||
@@ -22,6 +22,13 @@
|
||||
const profile = q.get("mock") || "android";
|
||||
const scheme = q.get("scheme") === "dark" ? "dark" : "light";
|
||||
const locale = q.get("locale") === "ms" ? "ms" : "en";
|
||||
/* The ONBD-02 destination profile (?dest=<id>, 04-04): the value needs
|
||||
* NO stub on the reader path — page.goto preserves the query string, so
|
||||
* window.location.search already carries it and the app's real
|
||||
* readDestinationId parses it. It is threaded to the mock as startParam
|
||||
* so the SECONDARY carrier (the signed initData's start_param field) is
|
||||
* exercisable from the harness too. */
|
||||
const dest = q.get("dest") || null;
|
||||
/* Boot-stage photography knobs (harness-only): at localhost speeds the
|
||||
whole session→chain→fix sequence resolves in a few ms, so the staged
|
||||
BOOT-01 (validating) and BOOT-02 (acquiring) cards cannot hold on
|
||||
@@ -32,7 +39,7 @@
|
||||
const fixDelayMs = Number.parseInt(q.get("fixdelay") || "0", 10);
|
||||
|
||||
// Set BEFORE main.ts is imported — this is the marker main.ts's install guard checks.
|
||||
window.__ARAHARAH_MOCK__ = { profile, scheme, locale };
|
||||
window.__ARAHARAH_MOCK__ = { profile, scheme, locale, dest };
|
||||
|
||||
// Stub the endpoints the app calls, OUTSIDE any dead-code-eliminated branch. These
|
||||
// live in the runner page (not in main.ts) precisely so no import.meta.env guard can
|
||||
@@ -67,6 +74,7 @@
|
||||
grantLocation: profile !== "tdesktop",
|
||||
deliveryDelayMs: fixDelayMs > 0 ? fixDelayMs : undefined,
|
||||
telegramFix: { lat: 3.1513, lng: 101.6958, accuracyM: 12, headingDeg: null },
|
||||
startParam: dest ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+53
-5
@@ -41,6 +41,9 @@ import { requestChatFallback } from "./location/chat-pin.ts";
|
||||
import type { Fix, LocationState } from "./location/types.ts";
|
||||
import { localeFromInitData, type Locale } from "./i18n/strings.ts";
|
||||
import { installMockWebApp } from "./mock/telegram-mock.ts";
|
||||
import { readDestinationId, resolveDestination, setDestination } from "./search/resolve-destination.ts";
|
||||
import { buildDestinationCard, buildDestinationUnavailableCard } from "./search/destination-card.ts";
|
||||
import { wireHomeScreen } from "./telegram/homescreen.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -60,8 +63,15 @@ async function boot(): Promise<void> {
|
||||
// single-shell predicate for the no-dead-ends wire.
|
||||
const overlayPresent = (): boolean => false; // Phase 3: single shell state (UI-SPEC BackButton contract)
|
||||
|
||||
// 2 — Telegram bindings: theme slot switch + BackButton total wire.
|
||||
// 12 — the locale comes from the RAW initData string (the session POST
|
||||
// returns only {token} — the claim's language_code is not reachable
|
||||
// client-side). Derived BEFORE the Telegram-bindings block so the
|
||||
// home-screen wire (ONBD-03) receives it; the strip consumes it below
|
||||
// exactly where it always has.
|
||||
const wa = getWebApp();
|
||||
const locale: Locale = wa ? localeFromInitData(wa.initData) : "en";
|
||||
|
||||
// 2 — Telegram bindings: theme slot switch + BackButton total wire.
|
||||
if (wa) {
|
||||
watchTheme(wa);
|
||||
wireBackButton(wa, overlayPresent);
|
||||
@@ -89,11 +99,45 @@ async function boot(): Promise<void> {
|
||||
stack.className = "card-stack";
|
||||
mainEl.appendChild(stack);
|
||||
|
||||
// 12 — locale comes from the RAW initData string (the session POST returns
|
||||
// only {token} — the claim's language_code is not reachable client-side).
|
||||
const locale: Locale = wa ? localeFromInitData(wa.initData) : "en";
|
||||
// 12 — the strip consumes the locale derived above the bindings block.
|
||||
strip.setLocale(locale);
|
||||
|
||||
// 13 — the destination (ONBD-02). TWO carriers, ONE validation (mechanism
|
||||
// corrected per RESEARCH Pitfall 1): the URL's own query string first
|
||||
// (the inline-button path — a web_app button's WebAppInfo carries only
|
||||
// a `url`), then initData's start_param (the documented direct-link /
|
||||
// home-screen path). An unknown id yields the honest unavailable state
|
||||
// and the boot continues into the normal staged boot — never a blank
|
||||
// map. The id resolves against the webview's OWN build-time copy of
|
||||
// the shared gazetteer, and the result is held in MEMORY ONLY
|
||||
// (GEO-03/D-07) — seeded once here, no persistence anywhere.
|
||||
const destPayload = readDestinationId(window.location.search, wa?.initData ?? null);
|
||||
const destination = resolveDestination(destPayload.id);
|
||||
setDestination(destination);
|
||||
// The destination surface — the confirmation card when the id resolved, the
|
||||
// honest unavailable card when an id ARRIVED and resolved to nothing, and
|
||||
// null when no id arrived at all: the Phase 3 boot is then byte-identical
|
||||
// (D-12's startapp-less boot landing in the search-ready map is UNCHANGED).
|
||||
const destinationCard =
|
||||
destination !== null
|
||||
? buildDestinationCard(locale, destination)
|
||||
: destPayload.id !== null
|
||||
? buildDestinationUnavailableCard(locale)
|
||||
: null;
|
||||
|
||||
// ONBD-03 — the home-screen affordance behind the Bot API 8.0 capability
|
||||
// guard. Wired after the strip exists (it hosts the affordance) and after
|
||||
// the locale is known. Telegram-native methods only — no browser install
|
||||
// APIs exist in the webview (T-04-38).
|
||||
if (wa) wireHomeScreen(wa, locale);
|
||||
|
||||
// The destination capture signal — emitted UNCONDITIONALLY (outside any map
|
||||
// guard, the same shape as the ARAHARAH-LIVE token): a WebGL2-absent device
|
||||
// still resolves the race. A STATE TOKEN, never the id (D-15 hygiene: a
|
||||
// console line carries no place data); the capture leg pairs it with a DOM
|
||||
// check on the rendered card's text.
|
||||
console.log("ARAHARAH-DEST state=" + (destination ? "resolved" : "absent"));
|
||||
|
||||
const recenter = createRecenterControl(mainEl, locale, () => {
|
||||
if (lastFix !== null && map !== null) follow?.follow(lastFix);
|
||||
});
|
||||
@@ -196,7 +240,11 @@ async function boot(): Promise<void> {
|
||||
startGantry(gantryOf(bootCard2));
|
||||
break;
|
||||
case "live":
|
||||
showCard(null);
|
||||
// With a preloaded destination the destination surface IS the live
|
||||
// state's card (the visible confirmation); with an id that arrived
|
||||
// but resolved to nothing, the honest unavailable card holds the
|
||||
// register. No id → null → the stack hides exactly as Phase 3 left it.
|
||||
showCard(destinationCard);
|
||||
break;
|
||||
case "stale":
|
||||
showCard(lostCard, "bottom");
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* homescreen.ts — ONBD-03 (D-12): the minimal Telegram-native home-screen hooks.
|
||||
* ------------------------------------------------------------------------------
|
||||
* The affordance is `Telegram.WebApp.addToHomeScreen()` / `checkHomeScreenStatus()`
|
||||
* ONLY — Bot API 8.0+ methods on the narrow `TelegramWebAppLike` contract (the
|
||||
* same phase-scoped law as backbutton.ts: guard on the optional member, a
|
||||
* total-function handler, no new UI furniture).
|
||||
*
|
||||
* The capability guard is BOTH halves: the method may be absent on older
|
||||
* clients, AND the platform documents the API as Bot API 8.0+ — a no-op on an
|
||||
* unsupported client either way (T-04-36).
|
||||
*
|
||||
* Status handling (T-04-37): "added" is the ONLY status that hides the
|
||||
* affordance (the icon is there — do not nag). "unknown" — which iOS ALWAYS
|
||||
* reports (Telegram-iOS #1729, closed as expected platform behaviour) — means
|
||||
* "supported but indeterminate": the affordance SHOWS with the neutral prompt,
|
||||
* never an error and never "not added". "missed" shows the prompt.
|
||||
* "unsupported" shows nothing. A rejected addToHomeScreen() is caught and
|
||||
* ignored — the affordance stays available for a retry, and the boot never
|
||||
* awaits it.
|
||||
*
|
||||
* D-12 hard bans (T-04-38): no browser install prompt API, no web app
|
||||
* manifest, no service worker — none of it exists inside the Telegram
|
||||
* webview, and a second icon the Telegram client does not know about is
|
||||
* worse than none. The affordance lives in the EXISTING Phase 3 shell: a
|
||||
* `.btn-secondary` in the status strip (the plan's named host), styled
|
||||
* entirely by classes that already exist.
|
||||
*/
|
||||
import type { TelegramWebAppLike, HomeScreenStatus } from "./webapp.ts";
|
||||
import { strings, type Locale } from "../i18n/strings.ts";
|
||||
|
||||
export function wireHomeScreen(wa: TelegramWebAppLike, locale: Locale): void {
|
||||
if (typeof wa.addToHomeScreen !== "function" || !wa.isVersionAtLeast("8.0")) return;
|
||||
const install = wa.addToHomeScreen;
|
||||
|
||||
const host = document.querySelector(".strip") ?? document.body;
|
||||
let affordance: HTMLElement | null = null;
|
||||
|
||||
const hide = (): void => {
|
||||
affordance?.remove();
|
||||
affordance = null;
|
||||
};
|
||||
|
||||
const show = (): void => {
|
||||
if (affordance !== null) return; // idempotent — the status callback fires once, but stay safe
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "btn-secondary";
|
||||
btn.dataset.role = "home-screen";
|
||||
btn.textContent = strings[locale].addToHomeScreen;
|
||||
btn.addEventListener("click", () => {
|
||||
try {
|
||||
install.call(wa);
|
||||
} catch {
|
||||
/* best-effort (T-04-36): a refused call is NOT an error surface —
|
||||
* the affordance stays for a retry, the boot is unaffected */
|
||||
}
|
||||
});
|
||||
affordance = btn;
|
||||
host.appendChild(btn);
|
||||
};
|
||||
|
||||
// Telegram fires this after the icon actually lands — the affordance hides then.
|
||||
wa.onEvent?.("homeScreenAdded", hide);
|
||||
|
||||
const apply = (status: HomeScreenStatus): void => {
|
||||
if (status === "added") return; // the icon is already there — do not nag
|
||||
if (status === "unsupported") return; // nothing to offer on this client
|
||||
show(); // "unknown" (always iOS) and "missed" both SHOW — indeterminate is not an error
|
||||
};
|
||||
|
||||
if (typeof wa.checkHomeScreenStatus === "function") {
|
||||
wa.checkHomeScreenStatus(apply);
|
||||
} else {
|
||||
show(); // version >= 8.0 but the checker is absent — offer the affordance (best-effort)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user