--- phase: 07-hardening-private-launch plan: 01 subsystem: ui tags: [telegram, fullscreen, bot-api-8.0, safe-area, capability-guard, px-04] # Dependency graph requires: - phase: 03-shell-map-mini-app provides: the TelegramWebAppLike contract + getWebApp() one-shot accessor; the boot() choreography with the boot-time applySafeArea(resolveSafeInsets(getWebApp())) line - phase: 04-destination-search-kl-gazetteer provides: the homescreen.ts capability-guard shape (BOTH halves: method presence AND isVersionAtLeast("8.0")) copied verbatim into fullscreen.ts - phase: 06-turn-by-turn-guidance-live-driving provides: activateGuidance()/teardownGuidance()/resume()/stopGuidanceArtifacts() edges in route.ts with engageCloseProtection/releaseCloseProtection anchoring the wiring sites; the PX-03 safe-area resolve/apply pair provides: - web/src/telegram/fullscreen.ts — requestGuidanceFullscreen / exitGuidanceFullscreen / watchFullscreenEvents behind the both-halves 8.0 guard, every platform call in try/catch - TelegramWebAppLike grown with five OPTIONAL members: requestFullscreen/exitFullscreen/isFullscreen/lockOrientation/unlockOrientation (8.0+) — no existing member reshaped - watchSafeAreaChanges in safe-area.ts — insets re-resolve through the union authority on safeAreaChanged/contentSafeAreaChanged - route.ts wiring: request at both guidance-start edges (fresh + resumed), exit at both teardown edges (trip end + walk-back), watchFullscreenEvents registered exactly once at the guidance-start edge - main.ts boot-site call: watchSafeAreaChanges(getWebApp()) directly after the boot-time apply line - web/test/fullscreen.test.mjs — FS1..FS9 covering the guard, refusal path, old-client fallback, events, and re-resolution affects: [07-02 (failure-state matrix), 07-03 (_headers), launch-gate verification, any future orientation/fullscreen work] # Tech tracking tech-stack: added: [] patterns: - "both-halves capability guard (method presence AND isVersionAtLeast(\"8.0\")) before every platform call, try/catch on each call, boolean best-effort return the caller ignores" - "one-authority re-resolution: event handlers re-run the SAME resolveSafeInsets union instead of duplicating inset math" - "observability-only event registration: no-op callback, try/catch-wrapped observer, category-only (no logging) per D-14" key-files: created: - web/src/telegram/fullscreen.ts - web/test/fullscreen.test.mjs modified: - web/src/telegram/webapp.ts - web/src/telegram/safe-area.ts - web/src/mock/telegram-mock.ts - web/src/route/route.ts - web/src/main.ts key-decisions: - "Fullscreen is requested on the SYNCHRONOUS guidance-start transition (activateGuidance / resume), never an async callback — the transient user-activation window is exactly there (RESEARCH Pitfall 1); the boolean result is deliberately ignored at every call site" - "Orientation handling uses Telegram's own lockOrientation/unlockOrientation (locks the CURRENT orientation only); the DOM Fullscreen API and generic screen.orientation.lock are banned and pinned out by the FS9 source-scan" - "watchFullscreenEvents registered exactly ONCE, in activateGuidance only — not in resume() — so a guidance session registers one watcher; the callback is a deliberate no-op (events are observability, never correctness)" - "The safe-area watch call in main.ts uses the same one-shot getWebApp() as the adjacent apply line rather than the later const wa binding (avoids a forward reference; getWebApp() is idempotent)" patterns-established: - "Capability wrapper triad (request/exit/watch) mirrors the close-protection engage/release pairing on the same lifecycle edges" - "Mutation-check discipline: the guard tests were verified to bite by temporarily neutering the 8.0 guard (FS2/FS5 failed under mutation, restored, green again)" requirements-completed: [PX-04] # Metrics duration: ~25 min completed: 2026-10-25 --- # Phase 7 Plan 1: Fullscreen Capability Layer Summary **Telegram-native fullscreen (Bot API 8.0 capability-guarded) at the guidance-start edge with safe-area re-resolution on rotation/fullscreen — a refusal is a silent fallback, never a guidance failure.** ## Performance - **Duration:** ~25 min - **Tasks:** 4/4 completed - **Files modified:** 7 (2 created, 5 modified) ## Accomplishments - **Task 1** — `TelegramWebAppLike` grew with the five 8.0-optional fullscreen members (appended after `disableClosingConfirmation?`, before `onEvent?`; no existing member changed shape) and the new `web/src/telegram/fullscreen.ts` exports exactly `requestGuidanceFullscreen` / `exitGuidanceFullscreen` / `watchFullscreenEvents`, each behind the BOTH-halves guard (method presence AND `isVersionAtLeast("8.0")`), each platform call inside its own try/catch. Zero logging (T-07-09), no DOM-fullscreen and no generic orientation lock (T-07-10). - **Task 2** — `watchSafeAreaChanges(wa)` in `safe-area.ts`: registers `safeAreaChanged` and `contentSafeAreaChanged` handlers that each re-run `applySafeArea(resolveSafeInsets(wa))` — the same union authority, never a duplicated computation — wrapped in try/catch (T-07-06b). `resolveSafeInsets`/`applySafeArea` behaviour untouched; no storage writes. - **Task 3** — the harness mock satisfies the grown contract in the same change (counters + `__isFullscreen` state; version comes from the `?mock=` profile, never hard-coded), and `web/test/fullscreen.test.mjs` landed FS1–FS9: guard halves (FS1/FS2), exact-once call (FS3), throwing-platform refusal (FS4), the exit mirror (FS5), event categories + throwing-observer containment (FS6), the old-client fallback (FS7/SC2.e), safe-area re-resolution with a finally-restored document stub (FS8), and the comment-stripped source-scan ban (FS9) asserting the banned APIs are absent AND the 8.0 guard is present. - **Task 4** — wiring: `requestGuidanceFullscreen` at both guidance-start edges (`activateGuidance` + `resume`, directly after `engageCloseProtection`), `exitGuidanceFullscreen` at both teardown edges (`teardownGuidance` + `stopGuidanceArtifacts`, directly after `releaseCloseProtection`), `watchFullscreenEvents` registered exactly once at the guidance-start edge with a deliberate no-op callback, and `watchSafeAreaChanges(getWebApp())` called at the `main.ts` boot site directly after the boot-time apply line. No new logging anywhere. ## Verification - `npm run web:typecheck` — exit 0. - `npm test -w web` — **157 passed, 0 failed** (148 baseline + 9 new FS cases); FS1–FS9 all PASS. - All plan acceptance greps exact: `requestGuidanceFullscreen(` = 2, `exitGuidanceFullscreen(` = 2, `watchFullscreenEvents(` = 1, `engageCloseProtection(` = 2, `releaseCloseProtection(` = 2 in route.ts; `watchSafeAreaChanges(` = 1 in main.ts; anti-pattern grep in fullscreen.ts = 0; `console\.` = 0. - Mutation check: neutering the version guard made FS2/FS5 fail (the tests pin the guard), file restored, suite green again. ## Deviations from Plan None — plan executed as written. (One cosmetic self-correction during task 1: the fullscreen.ts header comment initially named the banned API strings verbatim, which would have tripped the plan's plain-grep `documentElement.requestFullscreen|screen.orientation.lock = 0` acceptance criterion; reworded to "the DOM Fullscreen API plus the generic screen-level orientation lock" — FS9's comment-stripped scan is unaffected since it reads the code, not the prose intent.) ## Auth Gates None. ## Known Stubs None — no placeholder values, no unwired data paths. The `watchFullscreenEvents` no-op callback is a DESIGN decision (RESEARCH Open Question 1: the events are observability, never correctness), not a stub. ## Self-Check: PASSED - FOUND: web/src/telegram/fullscreen.ts - FOUND: web/test/fullscreen.test.mjs - FOUND: af0c0e9 (task 1 — contract + wrapper) - FOUND: 95ad3ea (task 2 — safe-area watch) - FOUND: aec8b07 (task 3 — mock + FS suite) - FOUND: 5b124e0 (task 4 — edge wiring)