Files
bedolaga-cabinet/src/utils/openAppScheme.test.ts
airp0wer 03aa2e6312 fix(connection): открывать deep-link приложения на iOS через top-level переход, а не iframe
На 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.
2026-07-21 10:36:42 +03:00

91 lines
3.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { openAppScheme } from './openAppScheme';
// openAppScheme touches window/document/navigator; the suite runs in a plain node
// environment, so stub just enough of the DOM to observe which launch path is taken.
interface FakeIframe {
style: { display: string };
src: string;
remove: () => void;
}
function setup(nav: { userAgent?: string; platform?: string; maxTouchPoints?: number }) {
const location = { href: '' };
const createdIframes: FakeIframe[] = [];
vi.stubGlobal('navigator', {
userAgent: nav.userAgent ?? '',
platform: nav.platform ?? '',
maxTouchPoints: nav.maxTouchPoints ?? 0,
});
vi.stubGlobal('window', {
location,
setTimeout: () => 0,
});
vi.stubGlobal('document', {
body: { appendChild: () => undefined },
createElement: () => {
const iframe: FakeIframe = { style: { display: '' }, src: '', remove: () => undefined };
createdIframes.push(iframe);
return iframe;
},
});
return { location, createdIframes };
}
describe('openAppScheme', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('navigates directly for http(s) links (no iframe)', () => {
const { location, createdIframes } = setup({ userAgent: 'Mozilla/5.0 (Android)' });
openAppScheme('https://example.com/sub');
expect(location.href).toBe('https://example.com/sub');
expect(createdIframes).toHaveLength(0);
});
it('opens a custom scheme via top-level navigation on iOS (iPhone), not via iframe', () => {
const { location, createdIframes } = setup({
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari/605.1',
});
openAppScheme('incy://import/https://sp.example.com/abc');
// iOS ignores iframe scheme launches — must be a real top-level navigation.
expect(location.href).toBe('incy://import/https://sp.example.com/abc');
expect(createdIframes).toHaveLength(0);
});
it('treats touch-capable iPadOS (reports as Mac) as iOS', () => {
const { location, createdIframes } = setup({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) Safari/605.1',
platform: 'MacIntel',
maxTouchPoints: 5,
});
openAppScheme('happ://add/https://sp.example.com/abc');
expect(location.href).toBe('happ://add/https://sp.example.com/abc');
expect(createdIframes).toHaveLength(0);
});
it('uses a contained iframe for custom schemes on Android (keeps page alive)', () => {
const { location, createdIframes } = setup({
userAgent: 'Mozilla/5.0 (Linux; Android 13) Chrome/120',
});
openAppScheme('incy://import/https://sp.example.com/abc');
// Android in-app browsers would paint a full-page error on a top-level nav.
expect(location.href).toBe('');
expect(createdIframes).toHaveLength(1);
expect(createdIframes[0].src).toBe('incy://import/https://sp.example.com/abc');
});
it('treats a non-touch Mac (desktop Safari) as non-iOS', () => {
const { createdIframes } = setup({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) Safari/605.1',
platform: 'MacIntel',
maxTouchPoints: 0,
});
openAppScheme('incy://import/https://sp.example.com/abc');
expect(createdIframes).toHaveLength(1);
});
});