mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
На iOS кастомная схема (incy://, happ://, …) запускается только top-level навигацией, привязанной к жесту пользователя. Запуск из скрытого iframe iOS молча игнорирует — приложение не открывается даже если установлено, поэтому кнопка «Добавить подписку» на iPhone (Safari и TG-миниапп) не срабатывала. Заметнее всего на INCY: это iOS-first клиент, его аудитория целиком на iPhone. openAppScheme и public/miniapp/redirect.html теперь на iOS используют window.location.href (вызов синхронный, внутри обработчика клика — жест сохраняется), а contained-iframe остаётся для Android in-app браузеров, где top-level переход к нерешаемой схеме рисует полноэкранный net::ERR_UNKNOWN_URL_SCHEME и стирает fallback-UI (Telegram bug #654272). Добавлен юнит-тест на выбор пути: iOS и iPadOS-as-Mac → location.href, Android и desktop → iframe.
189 lines
6.9 KiB
HTML
189 lines
6.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Redirecting...</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
body {
|
|
background: linear-gradient(135deg, #f5f5f7 0%, #e5e7eb 100%);
|
|
color: #1d1d1f;
|
|
}
|
|
.spinner {
|
|
border-color: rgba(0,0,0,0.1);
|
|
border-top-color: #3b82f6;
|
|
}
|
|
.error {
|
|
background: rgba(0,0,0,0.05);
|
|
}
|
|
.btn {
|
|
background: #3b82f6;
|
|
color: #fff;
|
|
}
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
.spinner {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 4px solid rgba(255,255,255,0.2);
|
|
border-top-color: #fff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 1.5rem;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
h1 {
|
|
font-size: 1.25rem;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
p {
|
|
font-size: 0.875rem;
|
|
opacity: 0.7;
|
|
}
|
|
.error {
|
|
display: none;
|
|
margin-top: 1.5rem;
|
|
padding: 1rem;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 8px;
|
|
}
|
|
.error.show {
|
|
display: block;
|
|
}
|
|
.btn {
|
|
display: inline-block;
|
|
margin-top: 1rem;
|
|
padding: 0.75rem 1.5rem;
|
|
background: #fff;
|
|
color: #1a1a2e;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="spinner"></div>
|
|
<h1 id="title">Opening app...</h1>
|
|
<p id="subtitle">Please wait</p>
|
|
<div class="error" id="errorBlock">
|
|
<p id="errorText">If the app didn't open, click the button below:</p>
|
|
<a class="btn" id="manualBtn" href="#">Open App</a>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const url = params.get('url');
|
|
const lang = params.get('lang') || 'en';
|
|
|
|
const texts = {
|
|
en: {
|
|
title: 'Opening app...',
|
|
subtitle: 'Please wait',
|
|
error: "If the app didn't open, click the button below:",
|
|
btn: 'Open App',
|
|
noUrl: 'No URL provided'
|
|
},
|
|
ru: {
|
|
title: 'Открываем приложение...',
|
|
subtitle: 'Пожалуйста, подождите',
|
|
error: 'Если приложение не открылось, нажмите кнопку:',
|
|
btn: 'Открыть приложение',
|
|
noUrl: 'URL не указан'
|
|
}
|
|
};
|
|
|
|
const t = texts[lang] || texts.en;
|
|
|
|
document.getElementById('title').textContent = t.title;
|
|
document.getElementById('subtitle').textContent = t.subtitle;
|
|
document.getElementById('errorText').textContent = t.error;
|
|
document.getElementById('manualBtn').textContent = t.btn;
|
|
|
|
// SECURITY: this page navigates to a user-supplied `url`. Only open
|
|
// a custom app deep link. A bare `javascript:`/`data:`/`vbscript:`/
|
|
// `file:` URI (DOM-XSS) is not scheme://-shaped, and http(s)/intent
|
|
// are blocked (open redirect / intent abuse) — so only VPN app
|
|
// schemes (happ://, vless://, ...) pass. Mirrors the validation in
|
|
// src/pages/DeepLinkRedirect.tsx.
|
|
function isSafeAppLink(raw) {
|
|
var s = String(raw || '').trim().toLowerCase();
|
|
var m = s.match(/^([a-z][a-z0-9+.\-]*):\/\//);
|
|
if (!m) return false;
|
|
var blocked = ['http', 'https', 'file', 'blob', 'about', 'javascript', 'data', 'vbscript', 'intent', 'content'];
|
|
return blocked.indexOf(m[1]) === -1;
|
|
}
|
|
|
|
if (!url || !isSafeAppLink(url)) {
|
|
document.getElementById('title').textContent = t.noUrl;
|
|
document.getElementById('subtitle').textContent = '';
|
|
document.querySelector('.spinner').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const manualBtn = document.getElementById('manualBtn');
|
|
manualBtn.href = url;
|
|
|
|
// iPadOS 13+ reports itself as desktop Safari, so a touch-capable "Mac"
|
|
// is treated as an iPad.
|
|
function isIOS() {
|
|
var ua = navigator.userAgent || '';
|
|
if (/iP(ad|hone|od)/.test(ua)) return true;
|
|
return navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
|
}
|
|
|
|
// Attempt to auto-launch the app. Two opposite fixes:
|
|
// - iOS launches a custom scheme only from a top-level navigation; a hidden
|
|
// iframe is a silent no-op there, so use location.href.
|
|
// - Android in-app browsers (Telegram/Yandex/…) paint a full-page
|
|
// net::ERR_UNKNOWN_URL_SCHEME on a top-level nav to an unresolved scheme
|
|
// (Telegram bug #654272), so a contained hidden iframe is used instead.
|
|
// Either way the manual "Open app" button below stays as the reliable path.
|
|
if (isIOS()) {
|
|
window.location.href = url;
|
|
} else {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|