feat(ui): diagnostic screen on /telegram/ instead of the landing bounce
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
A Mini App launch on /telegram/ without sign-in data (empty initData) used to
location.replace('/') — bouncing the visitor to the marketing landing. That
destroyed all diagnosability and, on some Android clients that reach the entry
with no initData, simply looked like the app refusing to open.
Replace the bounce with a compact, privacy-safe launch-error screen
(screens/TelegramLaunchError.svelte) that renders a one-screenshot diagnostic
snapshot captured at the moment of failure: SDK/WebApp presence, Telegram
platform/version, whether initData is empty, whether the URL fragment carried
tgWebAppData, the initData field NAMES present/missing (never the signed
values, never an IP), and OS/mobile/browser via User-Agent Client Hints plus
the full User-Agent. A Share button delivers it through the OS share sheet
(clipboard copy on desktop, reusing the GCG no-webview-strand guard); Retry
re-checks in place to recover a late initData without a reload (which would
discard the launch fragment).
This is the instrument to root-cause the Android empty-initData failure, which
is not reproducible in Playwright.
Tests: collectTelegramDiag + shareText/pickTextShare unit tests; the /telegram/
e2e now asserts the diagnostic screen, not a redirect. Docs: ARCHITECTURE.md +
UI_DESIGN.md updated.
This commit is contained in:
@@ -9,6 +9,7 @@ interface TelegramWebApp {
|
||||
initData: string;
|
||||
initDataUnsafe?: { start_param?: string };
|
||||
platform?: string;
|
||||
version?: string;
|
||||
themeParams?: TelegramThemeParams;
|
||||
colorScheme?: 'light' | 'dark';
|
||||
isFullscreen?: boolean;
|
||||
@@ -302,6 +303,120 @@ export function onTelegramPath(): boolean {
|
||||
return location.pathname.startsWith('/telegram/');
|
||||
}
|
||||
|
||||
// --- Launch diagnostics (the /telegram/ entry without sign-in data) ---
|
||||
|
||||
/** The initData fields a valid Telegram launch is expected to carry; their absence is the signal
|
||||
* the launch-error screen reports. Only field names are ever inspected, never their values. */
|
||||
const expectedInitDataFields = ['user', 'auth_date', 'hash', 'signature'];
|
||||
|
||||
interface uaBrand {
|
||||
brand: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface uaDataValue {
|
||||
platform?: string;
|
||||
mobile?: boolean;
|
||||
brands?: uaBrand[];
|
||||
}
|
||||
|
||||
/** uaData returns the User-Agent Client Hints object (Chromium only — notably the Android Telegram
|
||||
* webview), or undefined where it is unavailable (iOS / Safari / Firefox). */
|
||||
function uaData(): uaDataValue | undefined {
|
||||
if (typeof navigator === 'undefined') return undefined;
|
||||
return (navigator as unknown as { userAgentData?: uaDataValue }).userAgentData;
|
||||
}
|
||||
|
||||
/** launchFragmentData returns the raw tgWebAppData carried in the URL fragment (the form Telegram
|
||||
* appends on launch), or '' when absent. With no SDK present a non-empty value means Telegram
|
||||
* delivered the data but telegram-web-app.js never ran to parse it. */
|
||||
function launchFragmentData(): string {
|
||||
if (typeof location === 'undefined') return '';
|
||||
const frag = location.hash.replace(/^#/, '');
|
||||
if (!frag) return '';
|
||||
try {
|
||||
return new URLSearchParams(frag).get('tgWebAppData') ?? '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** initDataFieldNames parses a Telegram initData (or raw fragment data) query string and returns
|
||||
* only its field NAMES — never the values, since the hash / signature are auth material. */
|
||||
function initDataFieldNames(raw: string): string[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
return [...new URLSearchParams(raw).keys()];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TelegramDiag is a privacy-safe snapshot of why a Mini App launch lacked sign-in data, taken at
|
||||
* the moment of failure and rendered on the launch-error screen so a stuck user can share it with
|
||||
* the developer. It carries no secret values — only presence flags, client / OS identification and
|
||||
* the field NAMES of the launch data, never the signed initData itself, and never an IP.
|
||||
*/
|
||||
export interface TelegramDiag {
|
||||
/** Whether window.Telegram (the telegram-web-app.js script) is present at all. */
|
||||
hasSDK: boolean;
|
||||
/** Whether window.Telegram.WebApp is present. */
|
||||
hasWebApp: boolean;
|
||||
/** Telegram's own platform string (ios | android | android_x | tdesktop | web | …), or ''. */
|
||||
platform: string;
|
||||
/** The Bot API version the client reports, or ''. */
|
||||
version: string;
|
||||
/** The length of WebApp.initData — 0 is the failure this screen reports. */
|
||||
initDataLen: number;
|
||||
/** Whether the URL fragment still carried tgWebAppData at launch; true with hasSDK false means
|
||||
* Telegram delivered the data but the SDK script did not load to parse it. */
|
||||
hashHadTgData: boolean;
|
||||
/** The field names present in the launch data (from initData, or the raw fragment when the SDK
|
||||
* left initData empty); values are never included. */
|
||||
fieldsPresent: string[];
|
||||
/** The expected field names absent from the launch data (a subset of expectedInitDataFields). */
|
||||
fieldsMissing: string[];
|
||||
/** The OS / platform per User-Agent Client Hints (else navigator.platform), e.g. 'Android', ''. */
|
||||
osPlatform: string;
|
||||
/** Whether the client reports itself mobile per Client Hints: 'yes' | 'no' | '' (unknown). */
|
||||
mobile: string;
|
||||
/** The browser brands + major versions per Client Hints (Chromium only), or ''. */
|
||||
browser: string;
|
||||
/** The full User-Agent string — the catch-all that also carries the OS version and webview build. */
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* collectTelegramDiag captures a TelegramDiag snapshot of the current launch state. Call it at the
|
||||
* point a /telegram/ launch is found to lack sign-in data, so the snapshot reflects that moment —
|
||||
* sign-in data arriving late would otherwise mask the failure.
|
||||
*/
|
||||
export function collectTelegramDiag(): TelegramDiag {
|
||||
const w = webApp();
|
||||
const sdk = typeof window !== 'undefined' && !!(window as unknown as { Telegram?: unknown }).Telegram;
|
||||
const initData = w?.initData ?? '';
|
||||
const fragData = initData ? '' : launchFragmentData();
|
||||
const present = initDataFieldNames(initData || fragData);
|
||||
const ua = uaData();
|
||||
const navPlatform =
|
||||
typeof navigator === 'undefined' ? '' : (navigator as unknown as { platform?: string }).platform ?? '';
|
||||
return {
|
||||
hasSDK: sdk,
|
||||
hasWebApp: !!w,
|
||||
platform: w?.platform ?? '',
|
||||
version: w?.version ?? '',
|
||||
initDataLen: initData.length,
|
||||
hashHadTgData: fragData.length > 0,
|
||||
fieldsPresent: present,
|
||||
fieldsMissing: expectedInitDataFields.filter((f) => !present.includes(f)),
|
||||
osPlatform: ua?.platform ?? navPlatform,
|
||||
mobile: ua?.mobile === undefined ? '' : ua.mobile ? 'yes' : 'no',
|
||||
browser: (ua?.brands ?? []).map((b) => `${b.brand} ${b.version}`).join(', '),
|
||||
userAgent: typeof navigator === 'undefined' ? '' : navigator.userAgent,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Login Widget (web sign-in for account linking) ---
|
||||
|
||||
// The Login Widget is the web (non-Mini-App) Telegram sign-in. It is used only to
|
||||
|
||||
Reference in New Issue
Block a user