feat(ui): force Mini App fullscreen on mobile, not via the share link
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s
Replace the shared-link &mode=fullscreen (which would also force fullscreen on desktop) with an imperative requestFullscreen() on launch, gated to mobile clients (ios/android/android_x) — mirroring how Telegram's own Mini Apps go immersive on phones while desktop keeps the bot's full-size window. It triggers the existing fullscreenChanged -> safe-area resync; a no-op on clients predating Bot API 8.0.
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
telegramHaptic,
|
telegramHaptic,
|
||||||
telegramLaunch,
|
telegramLaunch,
|
||||||
telegramOnEvent,
|
telegramOnEvent,
|
||||||
|
telegramRequestFullscreen,
|
||||||
telegramSetChrome,
|
telegramSetChrome,
|
||||||
} from './telegram';
|
} from './telegram';
|
||||||
import { parseStartParam } from './deeplink';
|
import { parseStartParam } from './deeplink';
|
||||||
@@ -463,6 +464,10 @@ export async function bootstrap(): Promise<void> {
|
|||||||
telegramOnEvent('safeAreaChanged', syncTelegramSafeArea);
|
telegramOnEvent('safeAreaChanged', syncTelegramSafeArea);
|
||||||
telegramOnEvent('fullscreenChanged', syncTelegramSafeArea);
|
telegramOnEvent('fullscreenChanged', syncTelegramSafeArea);
|
||||||
telegramDisableVerticalSwipes();
|
telegramDisableVerticalSwipes();
|
||||||
|
// On mobile, go immersive fullscreen like Telegram's own Mini Apps; the fullscreenChanged
|
||||||
|
// 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 {
|
try {
|
||||||
await adoptSession(await gateway.authTelegram(launch.initData));
|
await adoptSession(await gateway.authTelegram(launch.initData));
|
||||||
// A blocked account skips deep-link routing — the blocked screen overlays every route.
|
// A blocked account skips deep-link routing — the blocked screen overlays every route.
|
||||||
|
|||||||
@@ -33,20 +33,20 @@ describe('shareLink', () => {
|
|||||||
|
|
||||||
it('wraps a payload in a startapp link', () => {
|
it('wraps a payload in a startapp link', () => {
|
||||||
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/bot/app');
|
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/bot/app');
|
||||||
expect(shareLink('f123456')).toBe('https://t.me/bot/app?startapp=f123456&mode=fullscreen');
|
expect(shareLink('f123456')).toBe('https://t.me/bot/app?startapp=f123456');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('picks the per-bot base by language', () => {
|
it('picks the per-bot base by language', () => {
|
||||||
vi.stubEnv('VITE_TELEGRAM_LINK_EN', 'https://t.me/enbot/app');
|
vi.stubEnv('VITE_TELEGRAM_LINK_EN', 'https://t.me/enbot/app');
|
||||||
vi.stubEnv('VITE_TELEGRAM_LINK_RU', 'https://t.me/rubot/app');
|
vi.stubEnv('VITE_TELEGRAM_LINK_RU', 'https://t.me/rubot/app');
|
||||||
expect(shareLink('f1', 'en')).toBe('https://t.me/enbot/app?startapp=f1&mode=fullscreen');
|
expect(shareLink('f1', 'en')).toBe('https://t.me/enbot/app?startapp=f1');
|
||||||
expect(shareLink('f1', 'ru')).toBe('https://t.me/rubot/app?startapp=f1&mode=fullscreen');
|
expect(shareLink('f1', 'ru')).toBe('https://t.me/rubot/app?startapp=f1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the language-agnostic base when the per-bot one is unset', () => {
|
it('falls back to the language-agnostic base when the per-bot one is unset', () => {
|
||||||
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/fallback/app');
|
vi.stubEnv('VITE_TELEGRAM_LINK', 'https://t.me/fallback/app');
|
||||||
vi.stubEnv('VITE_TELEGRAM_LINK_RU', '');
|
vi.stubEnv('VITE_TELEGRAM_LINK_RU', '');
|
||||||
expect(shareLink('f1', 'ru')).toBe('https://t.me/fallback/app?startapp=f1&mode=fullscreen');
|
expect(shareLink('f1', 'ru')).toBe('https://t.me/fallback/app?startapp=f1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,5 @@ export function shareLink(param: string, lang = ''): string | null {
|
|||||||
const base = telegramBase(lang);
|
const base = telegramBase(lang);
|
||||||
if (!base) return null;
|
if (!base) return null;
|
||||||
const sep = base.includes('?') ? '&' : '?';
|
const sep = base.includes('?') ? '&' : '?';
|
||||||
// mode=fullscreen opens the Mini App fullscreen, matching how the bot's own entry
|
return `${base}${sep}startapp=${encodeURIComponent(param)}`;
|
||||||
// opens it, so a shared link and the bot give the same experience.
|
|
||||||
return `${base}${sep}startapp=${encodeURIComponent(param)}&mode=fullscreen`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-14
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { insideTelegram, telegramClosingConfirmation, telegramLaunch } from './telegram';
|
import { insideTelegram, telegramClosingConfirmation, telegramLaunch, telegramRequestFullscreen } from './telegram';
|
||||||
|
|
||||||
function stubWebApp(initData: string, startParam?: string) {
|
function stubWebApp(initData: string, startParam?: string) {
|
||||||
vi.stubGlobal('window', {
|
vi.stubGlobal('window', {
|
||||||
@@ -38,22 +38,28 @@ describe('telegram launch detection', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// stubClient stands up a fake WebApp on the given platform with spies for the mobile-gated
|
||||||
|
// chrome toggles, so the platform gate can be asserted without a real Telegram client.
|
||||||
|
function stubClient(platform?: string) {
|
||||||
|
const enable = vi.fn();
|
||||||
|
const disable = vi.fn();
|
||||||
|
const requestFullscreen = vi.fn();
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
Telegram: {
|
||||||
|
WebApp: { platform, enableClosingConfirmation: enable, disableClosingConfirmation: disable, requestFullscreen },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { enable, disable, requestFullscreen };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mobilePlatforms = ['ios', 'android', 'android_x'];
|
||||||
|
const desktopPlatforms = ['tdesktop', 'macos', 'web', undefined];
|
||||||
|
|
||||||
describe('telegramClosingConfirmation', () => {
|
describe('telegramClosingConfirmation', () => {
|
||||||
afterEach(() => vi.unstubAllGlobals());
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
// stubClient stands up a fake WebApp on the given platform with spies for the close-guard
|
|
||||||
// toggles, so the platform gate can be asserted without a real Telegram client.
|
|
||||||
function stubClient(platform?: string) {
|
|
||||||
const enable = vi.fn();
|
|
||||||
const disable = vi.fn();
|
|
||||||
vi.stubGlobal('window', {
|
|
||||||
Telegram: { WebApp: { platform, enableClosingConfirmation: enable, disableClosingConfirmation: disable } },
|
|
||||||
});
|
|
||||||
return { enable, disable };
|
|
||||||
}
|
|
||||||
|
|
||||||
it('arms the close guard on mobile clients', () => {
|
it('arms the close guard on mobile clients', () => {
|
||||||
for (const p of ['ios', 'android']) {
|
for (const p of mobilePlatforms) {
|
||||||
const { enable } = stubClient(p);
|
const { enable } = stubClient(p);
|
||||||
telegramClosingConfirmation(true);
|
telegramClosingConfirmation(true);
|
||||||
expect(enable, `platform=${p}`).toHaveBeenCalledOnce();
|
expect(enable, `platform=${p}`).toHaveBeenCalledOnce();
|
||||||
@@ -61,7 +67,7 @@ describe('telegramClosingConfirmation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('skips the close guard on desktop clients (the dialog there is just noise)', () => {
|
it('skips the close guard on desktop clients (the dialog there is just noise)', () => {
|
||||||
for (const p of ['tdesktop', 'macos', 'web', undefined]) {
|
for (const p of desktopPlatforms) {
|
||||||
const { enable } = stubClient(p);
|
const { enable } = stubClient(p);
|
||||||
telegramClosingConfirmation(true);
|
telegramClosingConfirmation(true);
|
||||||
expect(enable, `platform=${p}`).not.toHaveBeenCalled();
|
expect(enable, `platform=${p}`).not.toHaveBeenCalled();
|
||||||
@@ -74,3 +80,23 @@ describe('telegramClosingConfirmation', () => {
|
|||||||
expect(disable).toHaveBeenCalledOnce();
|
expect(disable).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('telegramRequestFullscreen', () => {
|
||||||
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
|
it('goes immersive fullscreen on mobile clients', () => {
|
||||||
|
for (const p of mobilePlatforms) {
|
||||||
|
const { requestFullscreen } = stubClient(p);
|
||||||
|
telegramRequestFullscreen();
|
||||||
|
expect(requestFullscreen, `platform=${p}`).toHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves desktop clients as a standard window (the bot full-size setting fills it)', () => {
|
||||||
|
for (const p of desktopPlatforms) {
|
||||||
|
const { requestFullscreen } = stubClient(p);
|
||||||
|
telegramRequestFullscreen();
|
||||||
|
expect(requestFullscreen, `platform=${p}`).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+16
-4
@@ -16,6 +16,7 @@ interface TelegramWebApp {
|
|||||||
contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number };
|
contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number };
|
||||||
ready?: () => void;
|
ready?: () => void;
|
||||||
expand?: () => void;
|
expand?: () => void;
|
||||||
|
requestFullscreen?: () => void;
|
||||||
openTelegramLink?: (url: string) => void;
|
openTelegramLink?: (url: string) => void;
|
||||||
onEvent?: (event: string, handler: () => void) => void;
|
onEvent?: (event: string, handler: () => void) => void;
|
||||||
setHeaderColor?: (color: string) => void;
|
setHeaderColor?: (color: string) => void;
|
||||||
@@ -167,13 +168,24 @@ export function telegramHaptic(kind: Haptic): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isMobilePlatform reports whether the Mini App runs on a Telegram mobile client (iOS or
|
* isMobilePlatform reports whether the Mini App runs on a Telegram mobile client — iOS or
|
||||||
* Android), where a stray swipe-down can drop the app. Desktop clients (tdesktop, macOS,
|
* Android (the latter reported as 'android' by Telegram for Android and 'android_x' by
|
||||||
* web) report other values and close only on a deliberate window action.
|
* Telegram X). Desktop clients (tdesktop, macOS, web) report other values. Used to limit
|
||||||
|
* mobile-only chrome such as the close guard and immersive fullscreen.
|
||||||
*/
|
*/
|
||||||
function isMobilePlatform(): boolean {
|
function isMobilePlatform(): boolean {
|
||||||
const p = webApp()?.platform;
|
const p = webApp()?.platform;
|
||||||
return p === 'ios' || p === 'android';
|
return p === 'ios' || p === 'android' || p === 'android_x';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* telegramRequestFullscreen asks Telegram to open the Mini App in fullscreen (Bot API 8.0+),
|
||||||
|
* but only on mobile clients — mirroring how Telegram's own Mini Apps go immersive on phones
|
||||||
|
* while staying a standard window on desktop (where the bot's full-size setting already fills
|
||||||
|
* the window). A no-op outside Telegram, on desktop, or on clients predating the method.
|
||||||
|
*/
|
||||||
|
export function telegramRequestFullscreen(): void {
|
||||||
|
if (isMobilePlatform()) webApp()?.requestFullscreen?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user