feat(vk): show a launch diagnostic on a direct /vk/ open instead of a guest
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 1m13s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m7s

Opening the dedicated /vk/ entry directly in an ordinary browser (no signed VK
launch) fell through to the web flow and silently started a throwaway guest,
which is wrong for a VK-only entry. Mirror the existing /telegram/ launch-error
behaviour: render a compact, shareable, privacy-safe diagnostic screen instead.

- Boot: a new branch `onVKPath() && !insideVK()` sets app.launchError and stops
  the fall-through, the VK counterpart of the /telegram/ diagnostic guard.
- Diagnostic (passive, no VK Bridge round-trip): reads the URL launch-parameter
  NAMES (never the `sign` value — auth material), whether the URL was signed,
  `vk_platform`, the iframe/referrer context, plus the shared client-environment
  lines. VK signs the launch URL at load, so — unlike Telegram's initData — the
  parameters never arrive late; hence the VK screen offers Share only, no Retry.
- Generalise the screen: TelegramLaunchError.svelte -> LaunchError.svelte, which
  renders a neutral pre-formatted report and a per-platform title, with Retry
  gated on a `retry` flag (Telegram true, VK false). app.launchError is now a
  neutral LaunchDiag { platform, report, retry }.
- New lib/launchdiag.ts holds the shared pieces both platforms reuse: the
  LaunchDiag shape, the client-environment lines, and the query field-NAME
  reader (moved out of telegram.ts so VK does not duplicate them).

Tests: pure vkDiagLines units, incl. a guard that the `sign` value never leaks
into the report. i18n: launch.errorTitleVk (en/ru). Docs: FUNCTIONAL (+ru) user
story and ARCHITECTURE entry-path note.
This commit is contained in:
Ilia Denisov
2026-07-13 23:25:26 +02:00
parent f6d256215a
commit cc34622630
12 changed files with 256 additions and 95 deletions
@@ -1,34 +1,20 @@
<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.
// Shown on a dedicated Mini App entry (/telegram/ or /vk/) opened without a valid launch — instead of
// bouncing to the marketing landing (Telegram) or silently proceeding as a guest (VK). It states the
// problem plainly and renders a compact, privacy-safe diagnostic snapshot (app.launchError, taken at
// the moment of failure) 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 the launch
// carried no sign-in data. The snapshot carries only presence / identification signals and field
// NAMES, never the signed launch data itself, nor an IP. Retry is shown only where a late-arriving
// launch can still succeed (Telegram initData); VK delivers its signed parameters in the URL at load,
// so it offers Share only.
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')
: '',
);
// The title names the platform the entry belongs to; the body and the report are platform-neutral.
const title = $derived(t(diag?.platform === 'vk' ? 'launch.errorTitleVk' : 'launch.errorTitle'));
let retrying = $state(false);
async function retry(): Promise<void> {
@@ -44,7 +30,8 @@
let copied = $state(false);
let copyTimer: ReturnType<typeof setTimeout> | undefined;
async function share(): Promise<void> {
const r = await shareText(report, t('launch.errorTitle'));
if (!diag) return;
const r = await shareText(diag.report, title);
// The OS share sheet is its own feedback; a desktop clipboard copy is silent, so confirm it.
if (r === 'copied') {
copied = true;
@@ -57,12 +44,14 @@
{#if diag}
<div class="boot">
<div class="card">
<h1>{t('launch.errorTitle')}</h1>
<h1>{title}</h1>
<p class="msg">{t('launch.errorBody')}</p>
<pre class="diag">{report}</pre>
<pre class="diag">{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>
{#if diag.retry}
<button class="retry" onclick={retry} disabled={retrying}>{t('common.retry')}</button>
{/if}
</div>
</div>
</div>