feat(07-01): grow TelegramWebAppLike with 8.0-optional fullscreen members + capability wrapper

- TelegramWebAppLike gains requestFullscreen/exitFullscreen/isFullscreen/lockOrientation/unlockOrientation as OPTIONAL members only (no existing member reshaped)
- new web/src/telegram/fullscreen.ts: requestGuidanceFullscreen/exitGuidanceFullscreen/watchFullscreenEvents behind the BOTH-halves guard (method presence AND isVersionAtLeast("8.0"))
- every platform call in try/catch — a refused request is a supported degradation, never an error surface (T-07-06)
- no DOM Fullscreen API, no generic orientation lock, zero logging (T-07-10, T-07-09)
This commit is contained in:
araharah-executor
2026-10-25 03:42:37 +08:00
parent a7d8dbf345
commit af0c0e9de2
2 changed files with 118 additions and 2 deletions
+101
View File
@@ -0,0 +1,101 @@
/*
* fullscreen.ts — PX-04 (D-03): the Telegram-native fullscreen capability
* layer, guarded by BOTH halves of the Bot API 8.0 capability check.
* -----------------------------------------------------------------------------
* The affordance is `Telegram.WebApp.requestFullscreen()` / `exitFullscreen()`
* ONLY — Bot API 8.0+ methods on the narrow `TelegramWebAppLike` contract
* (the same phase-scoped law as homescreen.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 (the homescreen.ts precedent).
*
* A refused request is a SUPPORTED DEGRADATION, never an error surface: every
* platform call rides inside a try/catch, and the boolean returns are
* best-effort signals the caller deliberately ignores. The orientation lock
* locks the CURRENT orientation only — never force landscape — and the DOM
* Fullscreen API plus the generic screen-level orientation lock are
* hard-banned: the Telegram-native path is the
* only primary path (T-07-10, pinned by the FS9 source-scan ban).
*
* D-14 category-only law: this path logs NOTHING — no coordinates, no user
* ids, no route payloads. A `fullscreenFailed` is a category event.
*/
import type { TelegramWebAppLike } from "./webapp.ts";
/**
* Best-effort request for the Mini App's native fullscreen at the
* guidance-start edge. Returns true only when the platform call did not
* throw; false is the honest fallback on null WebApp, absent method, an
* old client, or a refusal — never an error surface.
*/
export function requestGuidanceFullscreen(wa: TelegramWebAppLike | null): boolean {
if (wa === null) return false;
if (typeof wa.requestFullscreen !== "function") return false;
if (typeof wa.isVersionAtLeast !== "function" || !wa.isVersionAtLeast("8.0")) return false;
try {
wa.requestFullscreen();
try {
// Telegram's lock pins the CURRENT orientation — never force landscape,
// and never the DOM's screen-level orientation lock (T-07-10).
wa.lockOrientation?.();
} catch {
/* best-effort: an orientation refusal is not a guidance failure */
}
return true;
} catch {
// the platform refused (FS4) — a supported degradation, never an error
return false;
}
}
/**
* The mirror of requestGuidanceFullscreen: releases fullscreen and the
* orientation lock at the trip-end / walk-back teardown edges, same
* both-halves guard, same try/catch law.
*/
export function exitGuidanceFullscreen(wa: TelegramWebAppLike | null): boolean {
if (wa === null) return false;
if (typeof wa.exitFullscreen !== "function") return false;
if (typeof wa.isVersionAtLeast !== "function" || !wa.isVersionAtLeast("8.0")) return false;
try {
wa.exitFullscreen?.();
try {
wa.unlockOrientation?.();
} catch {
/* best-effort: the lock release is not a guidance failure */
}
return true;
} catch {
return false;
}
}
/**
* Register the fullscreen observability listeners (Bot API 8.0+ clients
* fire `fullscreenChanged` / `fullscreenFailed`). The onChange callback is
* wrapped in try/catch so a throwing client callback can never break the
* guidance pump; the events are observability, never correctness, and this
* path logs NOTHING (the D-14 category-only law, T-07-09).
*/
export function watchFullscreenEvents(
wa: TelegramWebAppLike | null,
onChange: (isFullscreen: boolean) => void,
): void {
if (wa === null || typeof wa.onEvent !== "function") return;
wa.onEvent("fullscreenChanged", () => {
try {
onChange(true);
} catch {
/* a throwing observer must never break the guidance pump */
}
});
wa.onEvent("fullscreenFailed", () => {
try {
onChange(false);
} catch {
/* a throwing observer must never break the guidance pump */
}
});
}
+17 -2
View File
@@ -23,8 +23,12 @@
* `enableClosingConfirmation`/`disableClosingConfirmation` (6.2+) and
* `isActive` (8.0+) — the contract stays the contract, no existing member
* changes shape, every test fake and the harness mock satisfy exactly
* these members. `@types/telegram-web-app` remains deliberately
* NOT a dependency.
* these members. Phase 7 (PX-04/D-03) adds the native-fullscreen members as
* OPTIONAL only: requestFullscreen/exitFullscreen/isFullscreen/
* lockOrientation/unlockOrientation (8.0+) — the contract stays the contract,
* no existing member changes shape, every test fake and the harness mock
* satisfy exactly these members. `@types/telegram-web-app` remains
* deliberately NOT a dependency.
*/
export type HomeScreenStatus =
| "unsupported" // the feature is not supported here
@@ -71,6 +75,17 @@ export interface TelegramWebAppLike {
enableClosingConfirmation?: () => void;
/** Bot API 6.2+ — release the native close net (at trip end). */
disableClosingConfirmation?: () => void;
/** Bot API 8.0+ — request the Mini App's native fullscreen. Optional:
* absent on older clients; the 8.0 guard runs before every call. */
requestFullscreen?(): void;
/** Bot API 8.0+ — leave the Mini App's native fullscreen. Optional. */
exitFullscreen?(): void;
/** Bot API 8.0+ — whether the Mini App is currently fullscreen. Optional. */
isFullscreen?: boolean;
/** Bot API 8.0+ — lock the CURRENT orientation (never forces landscape). */
lockOrientation?(): void;
/** Bot API 8.0+ — release the orientation lock. */
unlockOrientation?: () => void;
onEvent?(event: string, cb: () => void): void;
offEvent?(event: string, cb: () => void): void;
BackButton?: { show(): void; hide(): void; onClick(cb: () => void): void; offClick(cb: () => void): void };