diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fa5361f..99d0faf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1040,8 +1040,9 @@ a dedicated redeem sub-limit or a longer code is the hardening step if abuse app Single public origin, path-routed. The Vite build has two entries: a lightweight **landing page** and the game **SPA**. The gateway **embeds** the SPA build (`go:embed`, baked in by a node stage in `gateway/Dockerfile`) and serves it at -`/app/` (web) and `/telegram/` (the Telegram Mini App; outside Telegram that path -redirects to the root — the client-side guard); a stray hit on the gateway's `/` +`/app/` (web) and `/telegram/` (the Telegram Mini App; on that path without sign-in data +— no `initData` — the client renders a compact, shareable launch-diagnostic screen instead +of redirecting away); a stray hit on the gateway's `/` 308-redirects to `/app/`. The **landing** ships in its own static container: the `landing` target of `gateway/Dockerfile` (caddy:2-alpine + the same Vite build, `deploy/landing/Caddyfile`) serves it at `/`, so stray public traffic is absorbed by diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index 3c24066..298f2d2 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -8,7 +8,13 @@ emoji glyphs. Tokens are CSS custom properties (`ui/src/app.css`), light/dark vi `prefers-color-scheme` or an explicit Settings choice, and **Telegram-themed**: on a Telegram Mini App launch — the app is served under `/telegram/` and detects the launch by `Telegram.WebApp.initData` — the SDK's `themeParams` override the tokens at -runtime; opened outside Telegram, the `/telegram/` path redirects to the site root. +runtime; on that path without sign-in data (no `initData` — outside Telegram, or a Mini App +launch that delivered none, as seen on some Android clients) the app renders a compact, +shareable launch-diagnostic screen (`screens/TelegramLaunchError.svelte`) rather than +redirecting to the site root. `telegram-web-app.js` is loaded **dynamically with a timeout**, +only on a Telegram entry — not a render-blocking ` + { void bootstrap(); @@ -84,6 +85,11 @@ {#if !routeIsLobby}
{t('common.loading')}
{/if} +{:else if app.launchError} + + {:else if app.bootError} @@ -128,7 +134,7 @@ -{#if routeIsLobby && !app.splashDone && !app.blocked && !app.bootError} +{#if routeIsLobby && !app.splashDone && !app.blocked && !app.bootError && !app.launchError} {/if} diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index c96a82f..32a9663 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -12,7 +12,11 @@ import { languageNeedsServerSync } from './language'; import { applyReduceMotion, applyTelegramTheme, applyTheme, type ThemePref } from './theme'; import { insideTelegram, + collectTelegramDiag, + type TelegramDiag, onTelegramPath, + hasLaunchFragment, + loadTelegramSDK, telegramColorScheme, telegramContentSafeAreaTop, telegramSafeAreaTop, @@ -47,6 +51,11 @@ export const app = $state<{ * backend was down during a deploy). App.svelte then renders the boot-error retry screen * instead of the web login — a Mini App has no manual sign-in to fall back to. */ bootError: boolean; + /** On the dedicated /telegram/ entry, set to a privacy-safe diagnostic snapshot when a Mini App + * launch carried no sign-in data (empty initData). App.svelte then renders the compact + * launch-error screen (screens/TelegramLaunchError) — a shareable probe for why Telegram + * delivered no initData (seen on some Android clients) — instead of bouncing to the landing. */ + launchError: TelegramDiag | null; /** Whether the lobby's first cold load has settled (success or error). The loading splash * (components/Splash.svelte) watches it to know when to dismiss; set by screens/Lobby. */ lobbyReady: boolean; @@ -96,6 +105,7 @@ export const app = $state<{ }>({ ready: false, bootError: false, + launchError: null, lobbyReady: false, splashDone: false, streamAlive: false, @@ -530,6 +540,35 @@ function syncViewportHeight(): void { if (h > 0) document.documentElement.style.setProperty('--vvh', `${h}px`); } +/** + * applyTelegramChrome applies a Mini App launch's visual integration: Telegram's authoritative + * colour scheme and theme, the matching header / background / bottom chrome, the safe-area insets, + * the swipe-down guard, and immersive fullscreen on mobile. It is idempotent, so both the initial + * bootstrap and a manual launch retry call it. + */ +function applyTelegramChrome(launch: TelegramLaunch): void { + if (launch.theme) applyTelegramTheme(launch.theme); + // Inside Telegram the colour scheme is Telegram's to decide; force it explicitly so the OS + // prefers-color-scheme (which leaks into the Telegram Desktop webview) cannot fight it. Falls + // back to the stored preference when the SDK omits it. + applyTheme(telegramColorScheme() ?? app.theme); + // Match Telegram's chrome to the app and stop its swipe-down-to-minimise from fighting tile + // drag / board scroll. + syncTelegramChrome(); + syncTelegramSafeArea(); + telegramDisableVerticalSwipes(); + // On mobile, go immersive fullscreen like Telegram's own Mini Apps; the fullscreenChanged + // listener (registered at bootstrap) then re-syncs the safe-area insets. Desktop keeps the bot's + // full-size window. No-op on clients predating Bot API 8.0. + telegramRequestFullscreen(); +} + +/** How long to wait for the dynamically loaded Telegram Mini App SDK before giving up and showing + * the launch-error screen. A network that blocks telegram.org makes the script hang rather than + * fail fast (a connection refusal resolves immediately via the script's error event), so this only + * bounds a true hang; it is generous enough not to misfire on a slow but working network. */ +const TELEGRAM_SDK_TIMEOUT_MS = 10000; + export async function bootstrap(): Promise { const prefs = await loadPrefs(); app.theme = prefs.theme ?? 'auto'; @@ -553,33 +592,30 @@ export async function bootstrap(): Promise { window.visualViewport.addEventListener('scroll', syncViewportHeight); } - // Telegram Mini App launch: apply the platform theme, authenticate via initData, - // and route any deep-link start parameter. On the dedicated /telegram/ entry path - // outside Telegram (no initData), refuse to render and send the visitor to the - // site root. + // Load the Telegram Mini App SDK dynamically, with a timeout, on a Telegram entry — it is no + // longer a render-blocking + +{#if diag} +
+
+

{t('launch.errorTitle')}

+

{t('launch.errorBody')}

+
{report}
+
+ + +
+
+
+{/if} + +