// VK Mini App SDK access via @vkontakte/vk-bridge. The bridge is imported lazily inside the // functions that need it — not at module top level — because the SDK reads browser globals on // import: the lazy import keeps the pure URL helpers below importable in the node test environment, // and code-splits the bridge into a chunk loaded only on the /vk/ entry. This wraps the subset the // app uses: launch detection, the signed launch parameters (for auth.vk) and the user's display name // (VKWebAppGetUserInfo, since VK omits it from the signed launch params). Every helper is safe to // call outside VK. import type { Haptic } from './telegram'; async function bridge() { return (await import('@vkontakte/vk-bridge')).default; } /** * onVKPath reports whether the app is served under the dedicated VK entry path (/vk/). */ export function onVKPath(): boolean { if (typeof location === 'undefined') return false; return location.pathname.startsWith('/vk/'); } /** * vkLaunchParams returns the raw signed VK launch query string (the vk_* parameters plus sign) from * the page URL — the exact form the gateway verifies — or '' when the URL carries no signed launch * (an ordinary browser tab, or the /vk/ path opened directly). */ export function vkLaunchParams(): string { if (typeof location === 'undefined') return ''; const query = location.search.replace(/^\?/, ''); return new URLSearchParams(query).has('sign') ? query : ''; } /** * insideVK reports whether the app launched as a VK Mini App — the URL carries signed launch * parameters (an ordinary browser tab has none). */ export function insideVK(): boolean { return vkLaunchParams() !== ''; } /** * vkInit signals to the VK client that the Mini App has loaded (VKWebAppInit), dismissing VK's own * loading cover. Best-effort: it resolves even if the bridge is unavailable (outside VK), so the * caller can await it unconditionally. */ export async function vkInit(): Promise { try { await (await bridge()).send('VKWebAppInit', {}); } catch { // Outside VK there is no client to receive it; the launch continues regardless. } } /** * vkUserName fetches the launching user's display name via VKWebAppGetUserInfo, since VK omits it * from the signed launch params. Returns '' on any failure or outside VK, so the backend falls back * to a generated placeholder. The value is a cosmetic seed only — being unsigned, the gateway never * trusts it for identity. */ export async function vkUserName(): Promise { try { const u = await (await bridge()).send('VKWebAppGetUserInfo', {}); return [u.first_name, u.last_name].filter(Boolean).join(' ').trim(); } catch { return ''; } } /** * vkAppId returns the launching VK app id (vk_app_id) from the signed launch parameters in the URL — * so the app can build its own vk.com/app links without a VK API call — or '' outside a VK launch. */ export function vkAppId(): string { if (typeof location === 'undefined') return ''; return new URLSearchParams(location.search.replace(/^\?/, '')).get('vk_app_id') ?? ''; } /** * vkStartParam returns the VK direct-link deep-link payload. VK passes everything after the '#' in a * vk.com/app# link to the app as the `hash` query parameter (it is also in location.hash, * but that collides with the app's hash router, so the query parameter is the safe source). Empty when * the launch carried no deep link. */ export function vkStartParam(): string { if (typeof location === 'undefined') return ''; return new URLSearchParams(location.search.replace(/^\?/, '')).get('hash') ?? ''; } /** * vkShare opens VK's native share dialog for link (VKWebAppShare) — the in-iframe replacement for * navigator.share, which is unavailable in the desktop VK iframe. Resolves true when the share was * handled, false on any failure or outside VK. */ export async function vkShare(link: string): Promise { try { await (await bridge()).send('VKWebAppShare', { link }); return true; } catch { return false; } } /** * vkCopyText copies text to the clipboard via VKWebAppCopyText — which works inside the VK iframe, * where navigator.clipboard is blocked. Resolves true on success, false on any failure or outside VK. */ export async function vkCopyText(text: string): Promise { try { await (await bridge()).send('VKWebAppCopyText', { text }); return true; } catch { return false; } } /** * vkOnScheme subscribes to VK's appearance (VKWebAppUpdateConfig) and calls handler with the mapped * 'light' | 'dark' scheme on launch and whenever the user switches the VK client theme — so the app's * "auto" theme can follow the VK client instead of the (often wrong) webview prefers-color-scheme. * A no-op outside VK. */ export async function vkOnScheme(handler: (scheme: 'light' | 'dark') => void): Promise { try { const b = await bridge(); b.subscribe((e) => { const detail = (e as { detail?: { type?: string; data?: { scheme?: string; appearance?: string } } }).detail; if (detail?.type !== 'VKWebAppUpdateConfig') return; const scheme = detail.data?.scheme ?? detail.data?.appearance ?? ''; handler(/dark|space_gray/i.test(scheme) ? 'dark' : 'light'); }); } catch { // Outside VK / bridge unavailable: leave the app on its own theme. } } /** VKInsets is the device safe-area the VK client reports (px). */ export interface VKInsets { top: number; bottom: number; left: number; right: number; } /** * vkOnInsets subscribes to VK's safe-area insets and calls handler with them on launch and on change. * VK reports them via VKWebAppUpdateConfig (iOS) and the dedicated VKWebAppUpdateInsets event; the VK * mobile webview does not expose them through CSS env() the way iOS Safari does, so the app reads them * from the bridge to clear the home bar (notably on Android). A no-op outside VK. */ export async function vkOnInsets(handler: (insets: VKInsets) => void): Promise { try { const b = await bridge(); b.subscribe((e) => { const d = (e as { detail?: { type?: string; data?: { insets?: Partial } } }).detail; if (d?.type !== 'VKWebAppUpdateConfig' && d?.type !== 'VKWebAppUpdateInsets') return; const i = d.data?.insets; if (!i) return; handler({ top: i.top ?? 0, bottom: i.bottom ?? 0, left: i.left ?? 0, right: i.right ?? 0 }); }); } catch { // Outside VK / bridge unavailable: the CSS env() safe-area fallback applies. } } /** * vkHaptic fires a VK Bridge haptic mirroring the Telegram set: a selection tick * (VKWebAppTapticSelectionChanged), a success/warning/error notification * (VKWebAppTapticNotificationOccurred) or a light/medium/heavy impact (VKWebAppTapticImpactOccurred). * Best-effort and a no-op outside VK or on a client without taptic support (the send rejects and is * swallowed), so callers fire it unconditionally. */ export async function vkHaptic(kind: Haptic): Promise { try { const b = await bridge(); if (kind === 'select') { await b.send('VKWebAppTapticSelectionChanged', {}); } else if (kind === 'success' || kind === 'warning' || kind === 'error') { await b.send('VKWebAppTapticNotificationOccurred', { type: kind }); } else { await b.send('VKWebAppTapticImpactOccurred', { style: kind }); } } catch { // Outside VK / no taptic support: silent, like the Telegram haptic off-platform. } } /** * vkDisableSwipeBack turns off VK's horizontal swipe-back gesture (VKWebAppSetSwipeSettings with * history:false) so it does not fight the app's own edge-swipe-back and on-board tile drag — the * app owns navigation through its back chevron, as on Telegram (telegramDisableVerticalSwipes). * Best-effort; a no-op outside VK or on a client without the method. */ export async function vkDisableSwipeBack(): Promise { try { await (await bridge()).send('VKWebAppSetSwipeSettings', { history: false }); } catch { // Outside VK / unsupported: VK's default gesture handling stays as-is. } }