Как приблизить веб-звонки к нативному UX
- пятница, 13 февраля 2026 г. в 00:00:05
Проблема 1. Зависимость аудиоустройств от активности экрана.
Проблема 2. Непреднамеренное взаимодействие с элементами интерфейса веб-приложения.
Проблема 3. Непреднамеренное взаимодействие с элементами браузера.
На сегодняшний день браузеры всё ещё не позволяют сделать звонки из веба такими же удобными, как в нативных приложениях.
Поэтому некоторые популярные сервисы вообще запрещают звонки из мобильного браузера и просто отправляют пользователя устанавливать нативное приложение.
В данной статье мы рассмотрим проблемы и решения, связанные непосредственно со звонками с мобильных устройств в вебе, когда когда пользователь держит устройство у уха, как обычный телефон.
Представим, что у нас есть веб-приложение с функцией звонков.
Пользователю нужно подключиться к звонку, но нативной версии приложения у него нет (мы её не сделали, не успел установить, нет места или просто удобнее веб).
Значит, единственный вариант – использовать браузер.
Если экран устройства автоматически гаснет после периода бездействия, страница может перейти в фоновый режим. В зависимости от операционной системы и браузера это приводит к приостановке работы аудиостека: микрофон перестаёт передавать звук, а воспроизведение может отключиться или «зависнуть».
Решение – использовать браузерное API navigator.wakeLock, чтобы не давать экрану гаснуть во время активного звонка.
Можно поискать подходящее решение на Npm, либо написать самому.
Пример простейшей реализации хука (здесь и в дальнейшем примеры будут показаны для React):
/* useWakeLock.ts */ import * as React from 'react'; // Keeps the screen awake export const useWakeLock = () => { React.useEffect(() => { if (!('wakeLock' in navigator)) return undefined; const abortController = new AbortController(); let wakeLock: WakeLockSentinel | null = null; const requestWakeLock = async () => { if (wakeLock && !wakeLock.released) return; try { wakeLock = await navigator.wakeLock.request('screen'); wakeLock.addEventListener( 'release', () => { wakeLock = null; }, { signal: abortController.signal }, ); } catch (_err) { // Wake lock request may fail (low battery, background tab, or permissions) } }; requestWakeLock(); // The browser automatically releases the wake lock when the document becomes hidden, // so we need to request it again when visibility changes back to 'visible'. document.addEventListener( 'visibilitychange', () => { if (document.visibilityState === 'visible') { requestWakeLock(); } }, { signal: abortController.signal }, ); return () => { abortController.abort(); wakeLock?.release().catch(_err => { // Ignore release errors }); }; }, []); };
Теперь достаточно использовать этот хук в компоненте звонка. Пока компонент смонтирован, экран в большинстве поддерживаемых браузеров не будет гаснуть.
При использовании телефона «у уха» пользователь фактически прижимает экран щекой. В нативных приложениях эту проблему обычно решает proximity sensor – экран гаснет и блокируется автоматически. В браузере такого механизма нет.
В результате пользователь может случайно нажать на элементы интерфейса: завершить звонок, выключить микрофон, открыть ссылку или активировать другие действия. Любое из этих взаимодействий вызовет негативный опыт у пользователя, и он может больше никогда не захотеть пользоваться эти веб-приложением таким образом.
Решение – добавить ручную блокировку интерфейса.
По нажатию на кнопку поверх приложения отображается тёмный оверлей, который перехватывает все события ввода. Разблокировка должна требовать «осознанного» действия – например, слайдера (по аналогии с нативным «slide to unlock»), чтобы избежать случайных касаний.
Для этого необходимо:
1) Определиться, при каких условиях мы даем заблокировать экран.
Мы хотим решить проблему звонков в режиме «у уха», соответственно, это мобильное устройство. Значит будем отображать интерфейс управления блокировкой экрана только если устройство мобильное.
/* src/components/ScreenLock/constants.ts */ import { isMobile } from 'react-device-detect'; export const canLockScreen = isMobile;
2) Отобразить кнопку блокировки экрана.
Если позволяет макет, эту кнопку можно отобразить рядом с кнопками микрофона и камеры для быстрого доступа.
/* src/components/ScreenLock/ScreenLockAction.tsx */ import * as React from 'react'; type Props = { onLock: () => void; }; export const ScreenLockAction = ({ onLock }: Props) => ( <button type='button' onClick={onLock}> Lock screen </button> );
3) Отобразить оверлей с возможностью разблокировки экрана.
Чтобы не разблокировать экран случайно щекой или ухом, следует сделать контрол разблокировки сложнее, чем просто кнопкой. Хорошим и нативным решением будет использовать слайдер (разумеется, со стилизацией под свое приложение).
/* src/components/ScreenLock/ScreenLockOverlay.tsx */ import * as React from 'react'; type Props = { onUnlock: () => void; }; export const ScreenLockOverlay = ({ onUnlock }: Props) => { const [value, setValue] = React.useState(0); const onTouchEnd = () => { if (value === 100) { onUnlock(); } setValue(0); }; return ( <div style={{ position: 'fixed', top: 0, right: 0, bottom: 0, left: 0, background: '#000000', }} > <p>Slide to unlock</p> <input type='range' min={0} max={100} value={value} onChange={e => setValue(parseFloat(e.target.value))} onTouchEnd={onTouchEnd} /> </div> ); };
4) Использовать созданные элементы в компоненте звонка.
/* src/components/Call/Call.tsx */ import * as React from 'react'; import { ScreenLockAction, ScreenLockOverlay, canLockScreen, } from 'components/ScreenLock'; const Call = () => { const [isScreenLocked, setIsScreenLocked] = React.useState(false); return ( <div> {/* Your call layout */} {canLockScreen && ( <> <ScreenLockAction onLock={() => setIsScreenLocked(true)} /> {isScreenLocked && ( <ScreenLockOverlay onUnlock={() => setIsScreenLocked(false)} /> )} </> )} </div> ); };
Теперь, когда пользователь захочет приложить телефон к уху, он сможет в один-два клика заблокировать экран и не переживать, что случайно что-то нажмет. А когда ему потребуется его разблокировать, он интуитивно сделает слайд и продолжит пользоваться приложением.
Даже если мы защитили интерфейс приложения оверлеем, остаётся ещё одна проблема – элементы самого браузера. На мобильных устройствах это адресная строка, кнопки «назад», «вперёд», жесты навигации и другие системные контролы.
Когда пользователь прикладывает телефон к уху, экран остаётся активным, и он может случайно покинуть страницу или выполнить навигационное действие. При этом он может даже не сразу понять, что звонок оборвался.
В отличие от предыдущего случая, мы не можем управлять этими элементами из JavaScript. Поэтому нужен другой подход.
Решение – сделать поддержку PWA.
Если пользователь установит веб-приложение на домашний экран в standalone-режиме, оно будет открываться без браузерных панелей и занимать весь экран, практически как нативное приложение. Это существенно снижает риск случайной навигации.
Чтобы у пользователя была возможность установить ваше веб-приложение к себе на экран телефон, необходимо создать manifest.json.
Задайте параметры под ваше приложение и добавьте иконки в хорошем качестве. Даже минимальной конфигурации достаточно, чтобы система позволила установить приложение на экран устройства.
/* public/manifest.json */ { "background_color": "#000000", "display": "standalone", "name": "My app", "short_name": "My App", "start_url": "/", "theme_color": "#000000", "icons": [ { "purpose": "maskable", "src": "/assets/favicon-192.png", "sizes": "192x192", "type": "image/png" }, { "purpose": "maskable", "src": "/assets/favicon-512.png", "sizes": "512x512", "type": "image/png" }, { "purpose": "any", "src": "/assets/favicon-512.png", "sizes": "512x512", "type": "image/png" } ] }
Также, некоторые браузеры требуют, чтобы был создан service-worker файл с обработкой события fetch.
Далее необходимо подсказать пользователю, как использовать эту технологию.
В решении предыдущей проблемы был добавлен оверлей для блокировки интерфейса приложения.
Будет отличным решением, добавить подсказку об установке приложения на этот оверлей, если пользователь еще не установил приложение на домашний экран.
1) Создаем несколько функций, которые помогут определить, хоть и не на всех, но на многих устройствах, возможен ли вызов диалога установки приложения из js.
/* src/components/ScreenHomeInstruction/utils.ts */ let pwaInstallPrompt: BeforeInstallPromptEvent | null = null; export const pwaBeforeInstallListenerCreate = () => { // Available only for Chromium Android browsers as experimental function. window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); pwaInstallPrompt = e as BeforeInstallPromptEvent; }); }; export const getHasPwaInstallPrompt = () => !!pwaInstallPrompt; export const runPwaInstallPrompt = () => { if (!pwaInstallPrompt) return; pwaInstallPrompt.prompt(); pwaInstallPrompt.userChoice.then(() => { pwaInstallPrompt = null; }); };
2) Создаем компонент, который будет содержать минимальную инструкцию, побуждающую пользователя установить приложение.
/* src/components/ScreenHomeInstruction/ScreenHomeInstruction.tsx */ import * as React from 'react'; import { getHasPwaInstallPrompt, runPwaInstallPrompt } from 'utils'; export const ScreenHomeInstruction = () => { const showInstallButton = getHasPwaInstallPrompt(); return ( <div className='screenHomeInstruction'> <p>For the best experience</p> <br /> {showInstallButton ? ( <button type='button' onClick={runPwaInstallPrompt}> Add to Home screen </button> ) : ( <p>Tap "Share", then "Add to Home Screen"</p> )} </div> ); };
Прячем ее, если приложение открыто в standalone-режиме.
/* src/components/ScreenHomeInstruction/ScreenHomeInstruction.css */ .screenHomeInstruction { @media (display-mode: standalone) { display: none; } }
3) Вернемся в компонент ScreenLockOverlay и добавим туда отображение компонента ScreenHomeInstruction
/* src/components/ScreenLock/ScreenLockOverlay.tsx */ import * as React from 'react'; import { ScreenLockInstruction } from 'components/ScreenLockInstruction'; type Props = { onUnlock: () => void; }; export const ScreenLockOverlay = ({ onUnlock }: Props) => { /* Overlay logic */ return ( <div style={ { // Overlay styles } } > <ScreenLockInstruction /> <p>Slide to unlock</p> {/* Overlay input */} </div> ); };
Теперь пользователь будет осведомлен, сможет сделать свой опыт использования приложения куда лучше и больше не переживать, что случайные нажатия ухом или щекой приведут к браузерной навигации или еще чему-нибудь неожиданному.
Дополнительным плюсом является то, что теперь у пользователя в десктопном браузере также появится кнопка добавления вашего приложение в список приложений системы (если есть поддержка в браузере и операционной системе).
Несколько небольших доработок – запрет гашения экрана через Wake Lock, ручная блокировка интерфейса и запуск приложения в PWA standalone-режиме – позволяют значительно приблизить веб-звонки к нативному опыту.
Эти решения не делают браузер полноценной заменой нативному приложению, но они устраняют самые болезненные проблемы: обрыв аудио, случайные нажатия и непреднамеренную навигацию. В результате пользоваться звонками из веба становится предсказуемо и комфортно.
И во многих сценариях этого уже достаточно, чтобы нативное приложение оказалось просто не нужно.