From 325e221e3209b430a7f5fd95df325802dae5db97 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Mon, 8 Jun 2026 13:11:19 +0300 Subject: [PATCH] fix(connection): stop ERR_UNKNOWN_URL_SCHEME when opening app deep links on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- public/miniapp/redirect.html | 27 +++++++++++++++++------ src/pages/Connection.tsx | 8 +++++-- src/pages/DeepLinkRedirect.tsx | 7 ++++-- src/utils/openAppScheme.ts | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 src/utils/openAppScheme.ts diff --git a/public/miniapp/redirect.html b/public/miniapp/redirect.html index b6e5151..cca3db1 100644 --- a/public/miniapp/redirect.html +++ b/public/miniapp/redirect.html @@ -148,14 +148,27 @@ const manualBtn = document.getElementById('manualBtn'); manualBtn.href = url; - // Try to open the URL scheme directly - // This page is opened in external browser, so custom schemes work - window.location.href = url; + // Attempt to launch the app WITHOUT a top-level navigation. A direct + // `location.href = scheme` paints a full-page net::ERR_UNKNOWN_URL_SCHEME + // inside Android in-app browsers (Telegram/Yandex/…) and silently fails on + // iOS — hiding the manual button (Telegram bug #654272). A hidden iframe is + // contained: the app opens if installed, otherwise the failure stays inside + // the (invisible) frame and the button below stays usable. + try { + var frame = document.createElement('iframe'); + frame.style.display = 'none'; + frame.src = url; + document.body.appendChild(frame); + setTimeout(function() { + try { frame.parentNode.removeChild(frame); } catch (e) {} + }, 2000); + } catch (e) { + window.location.href = url; + } - // Show manual button after a delay in case redirect didn't work - setTimeout(function() { - document.getElementById('errorBlock').classList.add('show'); - }, 2000); + // Programmatic opening is unreliable in in-app browsers, so surface the + // manual "Open app" button immediately — a user tap is the reliable trigger. + document.getElementById('errorBlock').classList.add('show'); })(); diff --git a/src/pages/Connection.tsx b/src/pages/Connection.tsx index f9b3833..5a3f7b7 100644 --- a/src/pages/Connection.tsx +++ b/src/pages/Connection.tsx @@ -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], ); diff --git a/src/pages/DeepLinkRedirect.tsx b/src/pages/DeepLinkRedirect.tsx index 4479b28..471e7e5 100644 --- a/src/pages/DeepLinkRedirect.tsx +++ b/src/pages/DeepLinkRedirect.tsx @@ -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 diff --git a/src/utils/openAppScheme.ts b/src/utils/openAppScheme.ts new file mode 100644 index 0000000..6f60bc1 --- /dev/null +++ b/src/utils/openAppScheme.ts @@ -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