feat(ui): diagnostic screen on /telegram/ instead of the landing bounce #126
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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'}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user