From ea931c6680485d300c5c7a8409e114dfa71ea007 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 24 Jun 2026 10:47:05 +0200 Subject: [PATCH] feat(telegram): native SettingsButton opens our Settings screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside the Mini App, reveal Telegram's native Settings button (Bot API 7.0) and route its taps to the Settings screen — the standard Mini App affordance. The in-app gear entry stays the primary path (two entry points by design). No-op outside Telegram or on a client predating the button. --- ui/src/lib/app.svelte.ts | 4 ++++ ui/src/lib/telegram.test.ts | 19 +++++++++++++++++++ ui/src/lib/telegram.ts | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 64bebe3..8b251fd 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -22,6 +22,7 @@ import { telegramContentSafeAreaTop, telegramSafeAreaInset, telegramDisableVerticalSwipes, + telegramShowSettingsButton, telegramHaptic, telegramLaunch, type TelegramLaunch, @@ -647,6 +648,9 @@ export async function bootstrap(): Promise { telegramOnEvent('fullscreenChanged', syncTelegramSafeArea); // Re-apply the theme live when the user switches Telegram's light/dark mode while the app is open. telegramOnEvent('themeChanged', () => syncTelegramTheme()); + // Telegram's native Settings button (Bot API 7.0) opens our Settings screen; the in-app gear + // entry stays the primary path. No-op on clients predating the button. + telegramShowSettingsButton(() => navigate('/settings')); await bootTelegram(launch); app.ready = true; return; diff --git a/ui/src/lib/telegram.test.ts b/ui/src/lib/telegram.test.ts index fec4340..35ceb3a 100644 --- a/ui/src/lib/telegram.test.ts +++ b/ui/src/lib/telegram.test.ts @@ -9,6 +9,7 @@ import { telegramOpenExternalLink, telegramThemeParams, telegramSafeAreaInset, + telegramShowSettingsButton, } from './telegram'; function stubWebApp(initData: string, startParam?: string) { @@ -77,6 +78,24 @@ describe('telegramOpenExternalLink', () => { }); }); +describe('telegramShowSettingsButton', () => { + afterEach(() => vi.unstubAllGlobals()); + + it('shows the native Settings button and wires its click inside Telegram', () => { + const onClick = vi.fn(); + const show = vi.fn(); + const handler = vi.fn(); + vi.stubGlobal('window', { Telegram: { WebApp: { SettingsButton: { onClick, show } } } }); + telegramShowSettingsButton(handler); + expect(onClick).toHaveBeenCalledWith(handler); + expect(show).toHaveBeenCalled(); + }); + + it('is a no-op without the SDK button (older client / outside Telegram)', () => { + expect(() => telegramShowSettingsButton(() => {})).not.toThrow(); + }); +}); + describe('routeExternalLinkInTelegram', () => { afterEach(() => vi.unstubAllGlobals()); diff --git a/ui/src/lib/telegram.ts b/ui/src/lib/telegram.ts index 40b3efe..9efbb92 100644 --- a/ui/src/lib/telegram.ts +++ b/ui/src/lib/telegram.ts @@ -41,6 +41,12 @@ interface TelegramWebApp { onClick?: (cb: () => void) => void; offClick?: (cb: () => void) => void; }; + SettingsButton?: { + show?: () => void; + hide?: () => void; + onClick?: (cb: () => void) => void; + offClick?: (cb: () => void) => void; + }; } function webApp(): TelegramWebApp | undefined { @@ -293,6 +299,19 @@ export function telegramDisableVerticalSwipes(): void { webApp()?.disableVerticalSwipes?.(); } +/** + * telegramShowSettingsButton reveals Telegram's native Settings button (in the Mini App's ⋮ menu, + * Bot API 7.0) and routes its taps to handler. A no-op outside Telegram or on a client predating + * the button, so the app's own in-app settings entry stays the primary path. The app registers it + * once per launch (Telegram hides the button when the Mini App closes), so there is no offClick. + */ +export function telegramShowSettingsButton(handler: () => void): void { + const b = webApp()?.SettingsButton; + if (!b?.show) return; + b.onClick?.(handler); + b.show(); +} + /** Haptic is the set of feedbacks the app triggers. */ export type Haptic = 'select' | 'success' | 'error' | 'warning' | 'light' | 'medium' | 'heavy';