release: v1.4.0 — Telegram launch diagnostic + dynamic SDK load #127

Merged
developer merged 4 commits from development into master 2026-06-23 08:40:10 +00:00
4 changed files with 72 additions and 10 deletions
Showing only changes of commit e3899d4755 - Show all commits
+2 -1
View File
@@ -131,5 +131,6 @@ test('a blocked telegram-web-app.js does not hang the diagnostic screen', async
await page.goto('/telegram/'); await page.goto('/telegram/');
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await expect(page.getByText('sdk: no')).toBeVisible(); // The diagnostic names the load outcome: a failed fetch reads as sdk-load: error.
await expect(page.getByText('sdk-load: error')).toBeVisible();
}); });
+34 -3
View File
@@ -3,6 +3,7 @@ import {
collectTelegramDiag, collectTelegramDiag,
insideTelegram, insideTelegram,
loadTelegramSDK, loadTelegramSDK,
telegramSdkOutcome,
routeExternalLinkInTelegram, routeExternalLinkInTelegram,
telegramClosingConfirmation, telegramClosingConfirmation,
telegramLaunch, telegramLaunch,
@@ -205,13 +206,41 @@ describe('loadTelegramSDK', () => {
vi.useRealTimers(); vi.useRealTimers();
}); });
it('resolves true immediately when the SDK is already present', async () => { it('resolves true and records "present" when the SDK is already there', async () => {
vi.stubGlobal('window', { Telegram: { WebApp: { initData: '' } } }); vi.stubGlobal('window', { Telegram: { WebApp: { initData: '' } } });
vi.stubGlobal('document', { createElement: vi.fn(), head: { appendChild: vi.fn() } }); vi.stubGlobal('document', { createElement: vi.fn(), head: { appendChild: vi.fn() } });
await expect(loadTelegramSDK(10000)).resolves.toBe(true); await expect(loadTelegramSDK(10000)).resolves.toBe(true);
expect(telegramSdkOutcome()).toBe('present');
}); });
it('resolves false when the script errors (telegram.org unreachable)', async () => { it('records "loaded" when the script defines the WebApp', async () => {
const script: Record<string, unknown> = {};
vi.stubGlobal('window', {});
vi.stubGlobal('document', {
createElement: () => script,
head: {
appendChild: () => {
(window as unknown as { Telegram: unknown }).Telegram = { WebApp: { initData: '' } };
(script.onload as () => void)();
},
},
});
await expect(loadTelegramSDK(10000)).resolves.toBe(true);
expect(telegramSdkOutcome()).toBe('loaded');
});
it('records "no-webapp" when the script loads but defines nothing', async () => {
const script: Record<string, unknown> = {};
vi.stubGlobal('window', {});
vi.stubGlobal('document', {
createElement: () => script,
head: { appendChild: () => (script.onload as () => void)() },
});
await expect(loadTelegramSDK(10000)).resolves.toBe(false);
expect(telegramSdkOutcome()).toBe('no-webapp');
});
it('records "error" when the script fails to load (telegram.org unreachable)', async () => {
const script: Record<string, unknown> = {}; const script: Record<string, unknown> = {};
vi.stubGlobal('window', {}); vi.stubGlobal('window', {});
vi.stubGlobal('document', { vi.stubGlobal('document', {
@@ -219,14 +248,16 @@ describe('loadTelegramSDK', () => {
head: { appendChild: () => (script.onerror as () => void)() }, head: { appendChild: () => (script.onerror as () => void)() },
}); });
await expect(loadTelegramSDK(10000)).resolves.toBe(false); await expect(loadTelegramSDK(10000)).resolves.toBe(false);
expect(telegramSdkOutcome()).toBe('error');
}); });
it('resolves false when the script hangs past the timeout', async () => { it('records "timeout" when the script neither loads nor fails in time', async () => {
vi.useFakeTimers(); vi.useFakeTimers();
vi.stubGlobal('window', {}); vi.stubGlobal('window', {});
vi.stubGlobal('document', { createElement: () => ({}), head: { appendChild: vi.fn() } }); vi.stubGlobal('document', { createElement: () => ({}), head: { appendChild: vi.fn() } });
const p = loadTelegramSDK(10000); const p = loadTelegramSDK(10000);
await vi.advanceTimersByTimeAsync(10000); await vi.advanceTimersByTimeAsync(10000);
await expect(p).resolves.toBe(false); await expect(p).resolves.toBe(false);
expect(telegramSdkOutcome()).toBe('timeout');
}); });
}); });
+35 -6
View File
@@ -60,6 +60,27 @@ export function insideTelegram(): boolean {
// safe-area insets, vertical-swipe guard, …) are available. Bump it when Telegram bumps theirs. // safe-area insets, vertical-swipe guard, …) are available. Bump it when Telegram bumps theirs.
const sdkScriptSrc = 'https://telegram.org/js/telegram-web-app.js?62'; const sdkScriptSrc = 'https://telegram.org/js/telegram-web-app.js?62';
/**
* TelegramSdkOutcome records how the dynamic telegram-web-app.js load resolved, surfaced on the
* launch-error screen to tell the failure modes apart — notably 'error' / 'timeout', which mean the
* network could not reach telegram.org (the script blocked or hung):
*
* not-attempted — loadTelegramSDK was never called (a plain web / native entry)
* present — window.Telegram.WebApp was already there (a cached load or native injection)
* loaded — the script loaded and defined window.Telegram.WebApp
* no-webapp — the script loaded (HTTP 200) but did not define window.Telegram.WebApp
* error — the script failed to load (telegram.org unreachable / blocked, a fast failure)
* timeout — the script neither loaded nor failed within the timeout (a blocked, hanging fetch)
*/
export type TelegramSdkOutcome = 'not-attempted' | 'present' | 'loaded' | 'no-webapp' | 'error' | 'timeout';
let sdkLoadOutcome: TelegramSdkOutcome = 'not-attempted';
/** telegramSdkOutcome returns how the last loadTelegramSDK attempt resolved (see TelegramSdkOutcome). */
export function telegramSdkOutcome(): TelegramSdkOutcome {
return sdkLoadOutcome;
}
/** /**
* loadTelegramSDK injects the official telegram-web-app.js and resolves true once * loadTelegramSDK injects the official telegram-web-app.js and resolves true once
* window.Telegram.WebApp is available, or false if the script errors or does not load within * window.Telegram.WebApp is available, or false if the script errors or does not load within
@@ -72,25 +93,29 @@ const sdkScriptSrc = 'https://telegram.org/js/telegram-web-app.js?62';
*/ */
export function loadTelegramSDK(timeoutMs: number): Promise<boolean> { export function loadTelegramSDK(timeoutMs: number): Promise<boolean> {
if (typeof document === 'undefined') return Promise.resolve(false); if (typeof document === 'undefined') return Promise.resolve(false);
if (webApp()) return Promise.resolve(true); if (webApp()) {
sdkLoadOutcome = 'present';
return Promise.resolve(true);
}
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
let done = false; let done = false;
const finish = (ok: boolean): void => { const finish = (outcome: TelegramSdkOutcome): void => {
if (done) return; if (done) return;
done = true; done = true;
resolve(ok); sdkLoadOutcome = outcome;
resolve(outcome === 'loaded');
}; };
const timer = setTimeout(() => finish(!!webApp()), timeoutMs); const timer = setTimeout(() => finish(webApp() ? 'loaded' : 'timeout'), timeoutMs);
const s = document.createElement('script'); const s = document.createElement('script');
s.src = sdkScriptSrc; s.src = sdkScriptSrc;
s.async = true; s.async = true;
s.onload = () => { s.onload = () => {
clearTimeout(timer); clearTimeout(timer);
finish(!!webApp()); finish(webApp() ? 'loaded' : 'no-webapp');
}; };
s.onerror = () => { s.onerror = () => {
clearTimeout(timer); clearTimeout(timer);
finish(false); finish('error');
}; };
document.head.appendChild(s); document.head.appendChild(s);
}); });
@@ -413,6 +438,9 @@ export interface TelegramDiag {
hasSDK: boolean; hasSDK: boolean;
/** Whether window.Telegram.WebApp is present. */ /** Whether window.Telegram.WebApp is present. */
hasWebApp: boolean; hasWebApp: boolean;
/** How the dynamic telegram-web-app.js load resolved (see TelegramSdkOutcome) — 'error' /
* 'timeout' mean telegram.org was unreachable, the prime suspect for an empty launch. */
sdkLoad: TelegramSdkOutcome;
/** Telegram's own platform string (ios | android | android_x | tdesktop | web | …), or ''. */ /** Telegram's own platform string (ios | android | android_x | tdesktop | web | …), or ''. */
platform: string; platform: string;
/** The Bot API version the client reports, or ''. */ /** The Bot API version the client reports, or ''. */
@@ -454,6 +482,7 @@ export function collectTelegramDiag(): TelegramDiag {
return { return {
hasSDK: sdk, hasSDK: sdk,
hasWebApp: !!w, hasWebApp: !!w,
sdkLoad: sdkLoadOutcome,
platform: w?.platform ?? '', platform: w?.platform ?? '',
version: w?.version ?? '', version: w?.version ?? '',
initDataLen: initData.length, initDataLen: initData.length,
@@ -17,6 +17,7 @@
const report = $derived( const report = $derived(
diag diag
? [ ? [
`sdk-load: ${diag.sdkLoad}`,
`sdk: ${diag.hasSDK ? 'yes' : 'no'} webapp: ${diag.hasWebApp ? 'yes' : 'no'}`, `sdk: ${diag.hasSDK ? 'yes' : 'no'} webapp: ${diag.hasWebApp ? 'yes' : 'no'}`,
`tg-platform: ${diag.platform || '—'} tg-version: ${diag.version || '—'}`, `tg-platform: ${diag.platform || '—'} tg-version: ${diag.version || '—'}`,
`initData: ${diag.initDataLen > 0 ? diag.initDataLen : 'empty'} tgdata-in-url: ${diag.hashHadTgData ? 'yes' : 'no'}`, `initData: ${diag.initDataLen > 0 ? diag.initDataLen : 'empty'} tgdata-in-url: ${diag.hashHadTgData ? 'yes' : 'no'}`,