fix(ui): route external links out of the Android VK WebView

The Android VK client's WebView ignores target=_blank and navigates the
Mini App's own window to the target, stranding the player outside the
game with no way back (the dictionary lookup, About/Feedback links, the
ad banner and the bot-link modal fallbacks). vk-bridge 3.x has no method
to open an external URL, so external links are routed through VK's own
leave-VK redirect (vk.com/away.php), which the client intercepts
natively and hands to the system browser.

onExternalLinkClick moves from lib/telegram to a new lib/links that
composes the Telegram and VK routers; iOS and desktop VK open _blank
correctly and are left alone.
This commit is contained in:
Ilia Denisov
2026-07-02 00:09:52 +02:00
parent c864147982
commit db17287113
11 changed files with 200 additions and 25 deletions
+55 -1
View File
@@ -6,7 +6,7 @@
// (VKWebAppGetUserInfo, since VK omits it from the signed launch params). Every helper is safe to
// call outside VK.
import type { Haptic } from './telegram';
import { isExternalHttpUrl, type Haptic } from './telegram';
async function bridge() {
return (await import('@vkontakte/vk-bridge')).default;
@@ -87,6 +87,60 @@ export function vkStartParam(): string {
return new URLSearchParams(location.search.replace(/^\?/, '')).get('hash') ?? '';
}
/**
* vkPlatform returns the VK client platform from the launch parameters (vk_platform:
* mobile_android, mobile_iphone, desktop_web, mobile_web, …), or '' outside a VK launch.
*/
export function vkPlatform(): string {
if (typeof location === 'undefined') return '';
return new URLSearchParams(location.search.replace(/^\?/, '')).get('vk_platform') ?? '';
}
/**
* vkAndroidWebView reports whether the app runs inside the Android VK client's WebView
* (vk_platform mobile_android / mobile_android_messenger) — the one environment whose WebView
* ignores target=_blank and navigates the app's own window instead of opening a browser.
*/
export function vkAndroidWebView(): boolean {
return insideVK() && vkPlatform().startsWith('mobile_android');
}
/**
* vkExternalBrowserUrl wraps url in VK's own leave-VK redirect (vk.com/away.php). The VK mobile
* client intercepts vk.com navigations natively and hands the redirect target to the system
* browser, so an external link escapes the Mini App WebView instead of replacing the game.
* vk-bridge (3.x) offers no method to open an external URL, so this redirect is the supported
* escape hatch. Pure, so it is unit-tested.
*/
export function vkExternalBrowserUrl(url: string): string {
return `https://vk.com/away.php?to=${encodeURIComponent(url)}`;
}
/**
* vkOpenExternalUrl opens url in the system browser from inside the Android VK client by routing
* it through vkExternalBrowserUrl. Returns false outside the Android VK WebView — iOS and the
* desktop iframe open target=_blank correctly and are left alone — so the caller falls back to
* the anchor's own navigation or a plain window.open.
*/
export function vkOpenExternalUrl(url: string): boolean {
if (typeof window === 'undefined' || !vkAndroidWebView()) return false;
window.open(vkExternalBrowserUrl(url), '_blank');
return true;
}
/**
* routeExternalLinkInVK decides whether a clicked anchor must be opened through VK's external
* redirect rather than the WebView's default navigation: only inside the Android VK client, and
* only for an external http(s) link opened in a new tab (target=_blank). Returns true when it
* opened the link — the caller should then preventDefault — and false to let the browser handle
* the click normally.
*/
export function routeExternalLinkInVK(anchor: { href: string; target: string }): boolean {
if (anchor.target !== '_blank') return false;
if (!isExternalHttpUrl(anchor.href)) return false;
return vkOpenExternalUrl(anchor.href);
}
/**
* 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