e3899d4755
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 1m16s
The diagnostic showed only sdk: yes/no (window.Telegram presence), not why the SDK was absent. Capture how the dynamic telegram-web-app.js load resolved — present / loaded / no-webapp / error / timeout — and surface it as "sdk-load: <outcome>". error/timeout pinpoint a blocked or hanging telegram.org (the prime suspect for an empty launch); no-webapp a loaded-but-broken script. loadTelegramSDK records the outcome (telegramSdkOutcome); collectTelegramDiag carries it into the screen. Unit tests cover each outcome; the blocked-script e2e now asserts sdk-load: error.
134 lines
4.1 KiB
Svelte
134 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
// Shown on the dedicated /telegram/ entry when a Mini App launch carried no sign-in data (empty
|
|
// initData) — instead of bouncing the visitor to the marketing landing. It states the problem
|
|
// plainly and renders a compact, privacy-safe diagnostic snapshot (taken at the moment of
|
|
// failure, app.launchError) sized to fit one screenshot, with a Share button (the OS share sheet,
|
|
// or a clipboard copy on desktop) so a stuck user can send it to the developer to pinpoint why
|
|
// Telegram provided no initData — notably on some Android clients. The snapshot carries only
|
|
// presence / identification signals and field NAMES, never the signed initData itself, nor an IP.
|
|
import { app, retryTelegramLaunch } from '../lib/app.svelte';
|
|
import { shareText } from '../lib/share';
|
|
import { t } from '../lib/i18n/index.svelte';
|
|
|
|
const diag = $derived(app.launchError);
|
|
|
|
// One compact "key: value" line per fact; the field labels are fixed diagnostic tokens (not UI
|
|
// prose), so the developer reads the same report in any locale. Kept terse to fit one screenshot.
|
|
const report = $derived(
|
|
diag
|
|
? [
|
|
`sdk-load: ${diag.sdkLoad}`,
|
|
`sdk: ${diag.hasSDK ? 'yes' : 'no'} webapp: ${diag.hasWebApp ? 'yes' : 'no'}`,
|
|
`tg-platform: ${diag.platform || '—'} tg-version: ${diag.version || '—'}`,
|
|
`initData: ${diag.initDataLen > 0 ? diag.initDataLen : 'empty'} tgdata-in-url: ${diag.hashHadTgData ? 'yes' : 'no'}`,
|
|
`fields: ${diag.fieldsPresent.join(',') || '—'}`,
|
|
`missing: ${diag.fieldsMissing.join(',') || '—'}`,
|
|
`os: ${diag.osPlatform || '—'} mobile: ${diag.mobile || '—'}`,
|
|
`browser: ${diag.browser || '—'}`,
|
|
`ua: ${diag.userAgent || '—'}`,
|
|
].join('\n')
|
|
: '',
|
|
);
|
|
|
|
let retrying = $state(false);
|
|
async function retry(): Promise<void> {
|
|
if (retrying) return;
|
|
retrying = true;
|
|
try {
|
|
await retryTelegramLaunch();
|
|
} finally {
|
|
retrying = false;
|
|
}
|
|
}
|
|
|
|
let copied = $state(false);
|
|
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
async function share(): Promise<void> {
|
|
const r = await shareText(report, t('launch.errorTitle'));
|
|
// The OS share sheet is its own feedback; a desktop clipboard copy is silent, so confirm it.
|
|
if (r === 'copied') {
|
|
copied = true;
|
|
clearTimeout(copyTimer);
|
|
copyTimer = setTimeout(() => (copied = false), 1500);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if diag}
|
|
<div class="boot">
|
|
<div class="card">
|
|
<h1>{t('launch.errorTitle')}</h1>
|
|
<p class="msg">{t('launch.errorBody')}</p>
|
|
<pre class="diag">{report}</pre>
|
|
<div class="actions">
|
|
<button class="share" onclick={share}>{copied ? t('launch.copied') : t('launch.share')}</button>
|
|
<button class="retry" onclick={retry} disabled={retrying}>{t('common.retry')}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.boot {
|
|
height: 100%;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 16px;
|
|
background: var(--bg);
|
|
}
|
|
.card {
|
|
max-width: 32rem;
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
text-align: center;
|
|
color: var(--text);
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 1.1rem;
|
|
}
|
|
.msg {
|
|
margin: 0;
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
}
|
|
.diag {
|
|
margin: 0;
|
|
text-align: left;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
color: var(--text);
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 8px 10px;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
}
|
|
.share,
|
|
.retry {
|
|
padding: 8px 16px;
|
|
border-radius: var(--radius-sm);
|
|
border: 1px solid var(--accent);
|
|
}
|
|
.share {
|
|
background: var(--accent);
|
|
color: var(--accent-text);
|
|
}
|
|
.retry {
|
|
background: transparent;
|
|
color: var(--accent);
|
|
}
|
|
.retry:disabled {
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|