Как PWA помогло клиентам ЮMoney продолжать получать пуши — даже без приложения
Всю вторую половину 2023 года мы превращали наш сайт в PWA-приложение, чтобы им было удобно пользоваться на устройствах iOS. Рассказываем, что из этого получилось и какие у нас планы на будущее. 🙌
Меня зовут Оля, я ведущий программист в отделе разработки интерфейсов ЮMoney. Работаю в команде «Портал» и занимаюсь главной страницей, страницами настроек, онлайн-оплаты и аналитики расходов.
В 2022 году мы, как и многие компании, столкнулись с удалением своих приложений из сторов и искали альтернативные решения. Одним из вариантов было сделать своё прогрессивное веб-приложение (Progressive Web App, PWA).
Что такое PWA
Википедия определяет PWA как технологию в веб-разработке, которая визуально и функционально трансформирует сайт в приложение. А так её видит ChatGPT:
Я же определяю PWA как набор технологий, которые позволяют превратить приложение в нечто нативное. Вот какие это могут быть технологии:
- Push API,
- Web App Manifest,
- Service Worker,
- Notifications API,
- Shape Detection API,
- JavaScript, CSS, HTML и другие веб-технологии.
Мы обратились к PWA в первую очередь для того, чтобы восстановить пуш-уведомления у наших пользователей на iOS. Поэтому опыт, которым я сегодня поделюсь, будет больше актуален для этой операционной системы. Но технологии, которые рассмотрю, — кросс-браузерные, они работают и на десктопе, и на iOS, и на Android.
Как работают пуш-уведомления в PWA ЮMoney
Когда мы хотим подписать пользователя на пуш-сообщения, он заходит на сайт и видит предложение подписаться на уведомления от нас.
Если пользователь даёт разрешение на подписку, мы идём на наш сервер и запрашиваем генерацию специального ключа. Сервер отдаёт нам публичную часть этого ключа.
Дальше идём с этим ключом в службу пуш-сообщений, где создаётся подписка. Подписка возвращается обратно в браузер, и мы сохраняем этот объект у себя в бэкенде для дальнейшего использования.
После того как пользователь добавляет приложение, нам нужно сделать шесть шагов, чтобы пуши заработали:
1. Подключить манифест.
2. Зарегистрировать Service Worker.
3. Запросить разрешение на получение уведомлений.
4. Получить VAPID-ключ от бэкенда.
5. Создать подписку.
6. Сохранить её данные на бэкенде.
Рассмотрим технологии, используемые в этих шести шагах
1) WebAppManifest. Это JSON-файл с информацией о нашем приложении. В нём можно настроить различные параметры того, как будет выглядеть веб-приложение: цвет фона (background_color), иконка (icons), название приложения (name), стартовый URL, который будет открываться при запуске. Можно также добавить, например, UTM-метки для статистики.
И самое главное — это свойство display, определяющее предпочитаемый браузером вид сайта. Свойство может принимать такие значения, как browser (когда есть элементы управления браузером) и fullscreen (когда элементов управления браузером нет, как и иконок операционной системы). Мы в ЮMoney используем standalone, когда нет элементов управления браузером, но есть иконки операционной системы.
Вот так может выглядеть файл манифеста:
Подключение манифеста:
Подключить манифест нужно в общем месте для страниц сайта, чтобы он распространял своё действие на все страницы.
Тут мы столкнулись с проблемой: статические ресурсы, такие как манифест, хранятся у нас на поддомене. Нам пришлось прибегнуть к магии nginx и использовать redirect.
2) Service Worker. Это событийно-управляемый воркер, который представляет собой JavaScript-файл, запускаемый браузером в фоновом режиме. Он обеспечивает кэширование ресурсов для работы в офлайн-режиме, фоновую синхронизацию данных, а также (самое главное для нас) — получение и отправку пуш-уведомлений.
Для работы с Service Worker характерны некоторые особенности:
- Нет прямого доступа к DOM — взаимодействие с ним происходит через postMessage API.
- Он запускается в воркер-контексте, в отдельном потоке, не блокируя основной.
- Он полностью асинхронный — там нельзя использовать синхронный API.
- Он может перехватывать сетевые запросы, поэтому работает только по HTTPS или локально в целях разработки.
Для начала работы необходимо зарегистрировать Service Worker. Проверяем его доступность в нашем окружении. Если он доступен, то регистрируем его по адресу, передаваемому в метод регистрации. Важно обращать внимание на расположение файла сервис-воркера, потому что этим будет обусловлена его область видимости.
Регистрация Service worker:
В этом примере мы подключаем файл, расположенный в корне домена. Он будет распространять своё действие на весь домен.
Здесь мы столкнулись с проблемой, что его нужно хранить на поддомене и нет возможности использовать redirect nginx. Но вместо redirect можно применить rewrite.
В сервис-воркере можно подписаться на множество событий — нас интересуют два: получение пуша и клик на нотификацию.
Пример подписки на события. Так может выглядеть код сервис-воркера, где мы обрабатываем эти два события:
- При наступлении пуша мы показываем нотификацию с помощью метода showNotification.
- Во время клика на нотификацию мы открываем URL, который пришёл в данных пуша.
Здесь следует обратить внимание на конструкцию waitUntil. У разработчика мало влияния на то, когда сервис-воркер запускается и останавливается, браузер решает это сам. Этой конструкцией мы говорим, что сервис-воркер занимается важной обработкой и что, пока промис (специальный объект в JavaScript, который используется для написания и обработки асинхронного кода) не завершится, сервис-воркер останавливать не нужно.
3) Notifications API. С помощью этой технологии можно контролировать отображение системных уведомлений, даже если приложение неактивно, пользователь его свернул или переключился на другую вкладку. На этом этапе наша главная задача — инициировать браузером запрос, чтобы пользователь разрешил показывать ему уведомления. Это мы делаем с помощью метода requestPermission.
Safari неверно обрабатывает ответ запроса на получение уведомлений, если этот запрос инициирован не каким-то конкретным действием пользователя. Поэтому нам пришлось добавить всплывающее окно с одной кнопкой, нажав на которую, пользователь может разрешить или запретить отправку уведомлений.
Состояние разрешения может быть в трёх статусах:
- Default — когда мы ничего не запрашивали и можем запросить, а пользователь разрешит или запретит.
- Denied — когда мы запрашивали разрешение, а пользователь нам отказал. В этом случае мы не можем ничего сделать. Но у пользователя будет возможность самостоятельно поставить разрешение в настройках своего устройства.
- Granted — когда разрешение на отправку уведомлений получено, мы можем дальше создавать подписку. Для этого нам понадобится VAPID-ключ.
4) VAPID-ключ. VAPID — это добровольная идентификация сервера приложений (Voluntary Application Server Identification). Ключ передаётся службе пуш-сообщений и используется для проверки, что отправляющий пуши сервер — это тот же сервер, на пуши которого подписался пользователь. Бэкенд отдаёт нам публичный ключ, с которым мы создаём подписку на устройстве пользователя.
VAPID используется в момент, когда нам нужно отправить сообщения. Наш бэкенд подписывает свои сообщения приватным ключом, потом в POST-заголовке отправляет эту информацию в службу пуш-сообщений, а она с помощью публичного ключа подтверждает, что это сообщение подписано приватным ключом из той же пары, и отвечает нашему сервису, что сообщения приняты к доставке. Затем служба отправляет пуш-сообщение на устройство пользователя.
Чтобы получить VAPID-ключ, мы используем fetch API, потому что любой браузер, который поддерживает сервис-воркеры, поддерживает и fetch API.
5) Push API. Даёт возможность получать сообщения с сервера независимо от того, запущено приложение или нет. Среди методов Push API нас интересуют три:
- Создание подписки. Вызываем метод subscribe у инстанса pushManager регистрации сервис-воркера и передаём туда два параметра. Первый — userVisibleOnly, он должен быть всегда true и говорить о том, что сообщение, которое мы получаем, будет видно пользователю. False сейчас не поддерживает ни один браузер. Второй параметр — applicationServerKey, тот самый публичный ключ, полученный от нашего бэкенда.
- Получение подписки. Метод может понадобиться, если нужно проверить, есть ли уже подписка на устройстве пользователя и нужно ли создавать новую. Или если мы хотим получить какие-то данные из подписки, например endpoint или ключи.
- Отмена подписки. Понадобится, если нужно отписать пользователя — например, когда он разлогинится. У сервис-воркера нет знания о сессиях пользователя, и этим нужно управлять вручную.
6) Сохранение подписки на бэкенд. Финальный шаг из нашего списка. Используем fetch API и передаём на бэкенд данные подписки.
В итоге мы выполнили все шесть шагов:
- Подключили манифест.
- Подключили сервис-воркер.
- Запросили у пользователя разрешение на отправку уведомлений.
- Запросили и получили у бэкенда VAPID-ключ.
- Создали подписку.
- Cохранили её на бэкенде.
Теперь сервер может отправлять, а устройство — получать пуш-уведомления.
После запуска пуш-сообщений мы проанализировали, как пользователи применяют наш PWA. Выяснилось, что 75% посетителей сайта ЮMoney заходят на него с мобильных устройств и что более 35% пользователей iOS делают это через PWA. Эта цифра постоянно растёт. Команда нашего B2B-направления (ЮKassa) тоже подключила в свой сервис пуш-сообщения.
В итоге за первые 30 дней после запуска PWA мы отправили пользователям ЮMoney и ЮKassa около миллиона пуш-уведомлений.
Также мы добавили в наше приложение сканирование QR-кодов для быстрой оплаты на кассе. К сожалению, Safari пока не поддерживает Shape Detection API и Barcode Detection API, поэтому мы используем альтернативные библиотеки.
Считаем запуск PWA для iOS удачным и планируем cделать то же самое для Android-устройств.
А я так впечатлилась разработкой нашего PWA, что сделала собственный сервис уведомлений для любителей настолок.
Совместила, так сказать, два своих хобби! =)
Мой сервис отправляет пуш-уведомления о том, когда будет следующая игра. Попросила подписаться всех своих друзей, с которыми мы играем, чтобы никто не пропускал встречи. 😉
Если у вас остались вопросы о PWA в ЮMoney (или о моей разработке для любителей настольных игр), с удовольствием отвечу на них в комментариях. 🙌