fix(ui): retry Mini App launch on backend failure; hide account linking
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 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
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 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Inside Telegram, a failed initData authentication (e.g. the backend down during a deploy) dropped the user onto the web login screen — the /app/ experience, which has no place inside the Mini App. bootstrap now retries the launch a few times in silence and then renders a dedicated boot-error screen with a Retry button (new BootError.svelte, app.bootError), never falling back to the web sign-in. A blocked account is still terminal and goes straight to the blocked screen. The profile "Link an account" section (email + Telegram link) is hidden while sign-in is provider-only; the anonymous /app/ guest whose upgrade path this is comes later. The flow is kept wired (`hidden` on .emailbox) and its two e2e specs are skipped, both to be re-enabled together. Adds i18n boot.* copy (en/ru), a mock authTelegram failure hook plus an e2e covering the retry screen, and bakes both behaviours into FUNCTIONAL(.md/_ru).
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
telegramDisableVerticalSwipes,
|
||||
telegramHaptic,
|
||||
telegramLaunch,
|
||||
type TelegramLaunch,
|
||||
telegramOnEvent,
|
||||
telegramRequestFullscreen,
|
||||
telegramSetChrome,
|
||||
@@ -41,6 +42,10 @@ export interface Toast {
|
||||
|
||||
export const app = $state<{
|
||||
ready: boolean;
|
||||
/** Inside a Mini App, set when the launch failed to authenticate after its retries (e.g. the
|
||||
* backend was down during a deploy). App.svelte then renders the boot-error retry screen
|
||||
* instead of the web login — a Mini App has no manual sign-in to fall back to. */
|
||||
bootError: boolean;
|
||||
/** Whether the lobby's first cold load has settled (success or error). The loading splash
|
||||
* (components/Splash.svelte) watches it to know when to dismiss; set by screens/Lobby. */
|
||||
lobbyReady: boolean;
|
||||
@@ -90,6 +95,7 @@ export const app = $state<{
|
||||
resync: number;
|
||||
}>({
|
||||
ready: false,
|
||||
bootError: false,
|
||||
lobbyReady: false,
|
||||
splashDone: false,
|
||||
streamAlive: false,
|
||||
@@ -563,14 +569,7 @@ export async function bootstrap(): Promise<void> {
|
||||
// listener above then re-syncs the safe-area insets. Desktop keeps the bot's full-size
|
||||
// window. No-op on clients predating Bot API 8.0.
|
||||
telegramRequestFullscreen();
|
||||
try {
|
||||
await adoptSession(await gateway.authTelegram(launch.initData));
|
||||
// A blocked account skips deep-link routing — the blocked screen overlays every route.
|
||||
if (!app.blocked) await routeStartParam(launch.startParam);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
navigate('/login');
|
||||
}
|
||||
await bootTelegram(launch);
|
||||
app.ready = true;
|
||||
return;
|
||||
}
|
||||
@@ -585,6 +584,57 @@ export async function bootstrap(): Promise<void> {
|
||||
app.ready = true;
|
||||
}
|
||||
|
||||
// Inside a Mini App the only identity is the Telegram session, so a failed launch must never fall
|
||||
// back to the web login screen. A transient backend outage (a deploy rolling over) is retried a
|
||||
// few times in silence; only then does the boot-error screen surface, from which Retry re-runs the
|
||||
// same path (retryTelegramBoot).
|
||||
const TELEGRAM_BOOT_RETRIES = 2;
|
||||
const TELEGRAM_BOOT_RETRY_MS = 1200;
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* bootTelegram authenticates a Mini App launch from its initData and routes any deep-link start
|
||||
* parameter, retrying a few times on a transient failure before raising the boot-error screen
|
||||
* (app.bootError). A blocked account is terminal — it switches straight to the blocked screen
|
||||
* without retrying.
|
||||
*/
|
||||
async function bootTelegram(launch: TelegramLaunch): Promise<void> {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
await adoptSession(await gateway.authTelegram(launch.initData));
|
||||
// A blocked account skips deep-link routing — the blocked screen overlays every route.
|
||||
if (!app.blocked) await routeStartParam(launch.startParam);
|
||||
app.bootError = false;
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err instanceof GatewayError && err.code === 'account_blocked') {
|
||||
await enterBlocked();
|
||||
return;
|
||||
}
|
||||
if (attempt >= TELEGRAM_BOOT_RETRIES) {
|
||||
app.bootError = true;
|
||||
return;
|
||||
}
|
||||
await delay(TELEGRAM_BOOT_RETRY_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* retryTelegramBoot re-attempts the Mini App launch from the boot-error screen's Retry button. It
|
||||
* clears the error and shows the loading state again, then runs the same retrying boot; on success
|
||||
* the app renders normally, otherwise the boot-error screen returns.
|
||||
*/
|
||||
export async function retryTelegramBoot(): Promise<void> {
|
||||
app.bootError = false;
|
||||
app.ready = false;
|
||||
await bootTelegram(telegramLaunch());
|
||||
app.ready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* routeStartParam navigates a Telegram deep-link start parameter to its target: a
|
||||
* specific game, the friends screen with a friend-code redemption, or the lobby
|
||||
|
||||
@@ -11,6 +11,9 @@ export const en = {
|
||||
'blocked.temporary': 'Your account is blocked until {until}.',
|
||||
'blocked.reason': 'Reason:',
|
||||
|
||||
'boot.errorTitle': "Couldn't load the game",
|
||||
'boot.errorBody': 'Please try again in a moment.',
|
||||
|
||||
'common.back': 'Back',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.ok': 'OK',
|
||||
|
||||
@@ -12,6 +12,9 @@ export const ru: Record<MessageKey, string> = {
|
||||
'blocked.temporary': 'Ваша учётная запись заблокирована до {until}.',
|
||||
'blocked.reason': 'Причина:',
|
||||
|
||||
'boot.errorTitle': 'Не удалось загрузить игру',
|
||||
'boot.errorBody': 'Попробуйте ещё раз или зайдите позже.',
|
||||
|
||||
'common.back': 'Назад',
|
||||
'common.cancel': 'Отмена',
|
||||
'common.ok': 'ОК',
|
||||
|
||||
@@ -136,7 +136,10 @@ export class MockGateway implements GatewayClient {
|
||||
}
|
||||
|
||||
// --- auth ---
|
||||
async authTelegram(): Promise<Session> {
|
||||
async authTelegram(initData: string): Promise<Session> {
|
||||
// e2e hook: an initData carrying this sentinel simulates a backend that rejects the launch,
|
||||
// so the Mini App boot-failure path (silent retries → boot-error screen) can be exercised.
|
||||
if (initData.includes('bootfail')) throw new GatewayError('unavailable');
|
||||
return { ...SESSION, isGuest: false };
|
||||
}
|
||||
async authGuest(): Promise<Session> {
|
||||
|
||||
Reference in New Issue
Block a user