fix(connection): stop ERR_UNKNOWN_URL_SCHEME when opening app deep links on mobile

Telegram bug #654272: opening the connection app link (immediate-open / connect button)
showed a full-page net::ERR_UNKNOWN_URL_SCHEME on Android and silently failed on iOS, while
working on desktop.

Cause: a programmatic top-level navigation to a custom scheme (happ://, v2rayng://, …) via
window.location.href is rendered as a full-page error inside in-app WebViews (Telegram/Yandex)
on Android and does nothing on iOS — also wiping the fallback UI.

Add openAppScheme(): http(s) navigate normally; custom schemes launch via a hidden, contained
iframe so a failed launch never replaces the page — the app opens if installed, otherwise the
manual 'Open app' link stays usable (a user tap is the reliable trigger). Applied in
DeepLinkRedirect.tsx, Connection.tsx and the static public/miniapp/redirect.html (which now
also surfaces the manual button immediately instead of after 2s).
This commit is contained in:
c0mrade
2026-06-08 13:11:19 +03:00
parent d37872fd4f
commit 325e221e32
4 changed files with 70 additions and 11 deletions

View File

@@ -8,6 +8,7 @@ import { useTelegramSDK } from '../hooks/useTelegramSDK';
import { useHaptic } from '@/platform';
import { SettingsIcon } from '@/components/icons';
import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
import { openAppScheme } from '../utils/openAppScheme';
import { isHappCryptolinkMode, resolveConnectionUrlForUi } from '../utils/connectionLink';
import { useAuthStore } from '../store/auth';
import type { AppConfig, RemnawavePlatformData } from '../types';
@@ -133,8 +134,11 @@ export default function Connection() {
}
}
// In regular browsers open deeplink directly (without intermediate redirect page).
window.location.href = resolved;
// In regular browsers open the deeplink directly. openAppScheme uses a contained
// iframe for custom schemes so an unresolved scheme doesn't paint a full-page
// net::ERR_UNKNOWN_URL_SCHEME (Android) / silently fail (iOS); http(s) links
// still navigate normally. (Telegram bug #654272.)
openAppScheme(resolved);
},
[isTelegramWebApp, i18n.language, resolveUrl, connectionLink?.connect_mode, qrConnectionUrl],
);

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { brandingApi } from '../api/branding';
import { copyToClipboard } from '../utils/clipboard';
import { openAppScheme } from '../utils/openAppScheme';
import {
CheckIcon,
CopyIcon,
@@ -97,10 +98,12 @@ export default function DeepLinkRedirect() {
const appName = appInfo?.name || appParam || 'VPN';
const appIcon = appInfo?.icon || appName[0]?.toUpperCase() || 'V';
// Open deep link - same as miniapp, just window.location.href
// Open deep link via a contained iframe attempt so a custom scheme the in-app
// WebView can't resolve doesn't replace this page with net::ERR_UNKNOWN_URL_SCHEME
// (Android) / silently fail (iOS) and wipe the fallback UI. See openAppScheme.
const openDeepLink = useCallback(() => {
if (!deepLink || !isValidDeepLink(deepLink)) return;
window.location.href = deepLink;
openAppScheme(deepLink);
}, [deepLink]);
// Countdown timer effect

View File

@@ -0,0 +1,39 @@
/**
* Launch a custom-scheme app deep link (happ://, v2rayng://, vless://, …) without
* crashing the page inside in-app browsers.
*
* Why not `window.location.href = scheme`: a programmatic top-level navigation to a
* scheme the WebView can't resolve renders a full-page error — on Android in-app
* browsers (Telegram/Yandex/…) `net::ERR_UNKNOWN_URL_SCHEME`, on iOS it silently does
* nothing — which destroys the fallback UI (Telegram bug #654272). A hidden <iframe>
* navigation is contained: if the app is installed the OS intercepts it; if not, the
* failure stays inside the (invisible) frame and our page — with its manual "Open app"
* link — survives so the user can tap it (a real user gesture is the reliable trigger).
*
* http(s) links are normal navigations and are passed straight to location.href.
*/
export function openAppScheme(url: string): void {
const isHttp = /^https?:\/\//i.test(url);
if (isHttp) {
window.location.href = url;
return;
}
try {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
window.setTimeout(() => {
try {
iframe.remove();
} catch {
/* already detached */
}
}, 2000);
} catch {
// iframe creation blocked (very old/locked-down WebView) — fall back to direct
// navigation. Worst case this shows the same error the iframe avoided, never worse.
window.location.href = url;
}
}