Тикетинг-платформа E7 решила веб-чекаут. Но у мобильного веба есть потолок. Pinch-to-zoom на SVG-карте зала в мобильном браузере — терпимо, не хорошо. А после покупки билета поток email → PDF → скриншот → показать на входе — это боль, которая копится от мероприятия к мероприятию.
E7 понадобилось нативное приложение. Я собрал его за 10 недель на React Native и Expo.
Почему натив, а не PWA
Это было реальное решение, не дефолт. Полный фреймворк сравнения натива с PWA — в отдельном посте — вот как факторы сыграли для E7:
Жесты карты мест убили путь PWA. Выбор мест требует pinch-to-zoom, двойной тап для центрирования и инерционного панорамирования — всё на 60fps для SVG с 3,000 узлами. Mobile Safari и Chrome обрабатывают это терпимо для фото, но не для интерактивных SVG, где каждый узел — это тап-цель. Мы прототипировали в браузере — 24fps на Pixel 4a. Натив с Reanimated 3 — 60fps на том же устройстве.
Офлайн-хранение билетов требует реальных API устройства. PWA может использовать Cache API, но не может надёжно хранить зашифрованные данные билетов между перезапусками на iOS Safari. React Native с AsyncStorage + SecureStore даёт зашифрованное офлайн-хранение, которое сохраняется.
Пуш-уведомления для напоминаний о мероприятиях. Технически возможно с PWA, но включение ~15% (пользователи не разрешают). Нативный пуш через Expo Notifications — 72% подписки. Для ивент-приложения напоминания — вторая по использованию функция после билетов.
Ограничения
10 недель. Веб-платформа уже работала; приложение должно было выйти до следующего концертного сезона.
Одна кодовая база, две платформы. Бюджета на отдельные iOS и Android команды нет. React Native + Expo — единственный путь, который даёт обе платформы из одной кодовой базы в заданные сроки.
Общий бэкенд. Приложение обращается к тому же API, что и веб-платформа. Без отдельного бэкенда, без дублирования данных, без проблем синхронизации. Одна Prisma-схема, одна база Postgres, один аккаунт Stripe.
Что отгрузили
Выбор мест — 60fps на бюджетном телефоне
Это инженерный центр проекта. SVG на 3,000 мест, который можно зумить, панорамировать и тапать на нативной скорости.
Архитектура:
-
Reanimated 3 для жестов. Pinch-to-zoom и пан работают на нативном UI-потоке, не на JS-потоке. Это значит, что обработка жестов не блокирует рендеринг React. На Pixel 4a разница — 24fps (жесты на JS-потоке) vs 60fps (нативный поток).
-
Виртуализированный рендеринг мест. Только места, видимые в текущей области просмотра, рендерятся как интерактивные
Pressable. Места за пределами — один статичный<Path>. При панорамировании места входят и выходят из интерактивной зоны. Это держит количество узлов ниже 200 вне зависимости от размера зала. -
Оптимистичная блокировка мест. При тапе на место UI показывает его как «ваше» немедленно (оптимистичное обновление), пока запрос блокировки летит в фоне. Если блокировка не удалась (кто-то другой успел первым) — место возвращается в доступные с хаптик-вибрацией. Воспринимаемая задержка: ноль. Реальный раунд-трип: ~150ms.
-
Цветовое кодирование состояний. Доступные — белые. Ваши — зелёные. Заблокированные другими — серые. Проданные — тёмные. Легенда не нужна — состояния очевидны по цвету.
Пост об офлайн-архитектуре погружается глубже в протокол синхронизации. Короткая версия: мы собрали 4-состоянную очередь (pending → syncing → confirmed → failed), которая обрабатывает обрывы сети посреди чекаута без потери выбора пользователя.
QR-кошелёк
После покупки билеты появляются во встроенном кошельке. Каждый билет показывает:
- Название мероприятия, дату, площадку
- Информацию о месте/секции
- QR-код, обновляющийся каждые 30 секунд (для предотвращения шаринга скриншотов)
- Маршрут до площадки (диплинк в Карты)
Кошелёк работает полностью офлайн. Данные билета + ключи для подписи QR зашифрованы и хранятся локально при покупке. Даже если пользователь в подземном клубе без сигнала — он может показать билет, и сканер персонала его верифицирует.
Ротация QR — позднее добавление. Первоначальный QR был статичным — одна и та же картинка каждый раз. Через две недели после запуска мы увидели скриншоты QR-кодов в соцсетях. Добавление ротации на основе времени (QR кодирует ID билета + текущее 30-секундное окно, подписанное HMAC) убило шаринг скриншотов без влияния на легитимных пользователей.
Пуш-уведомления
Три типа уведомлений, настроенных по данным вовлечённости:
- Напоминание о мероприятии: за 24 часа. 68% переходов.
- Открытие входа: когда начинается чекин на площадке. Отправляется только владельцам билетов.
- Новые мероприятия: еженедельный дайджест для подписавшихся. 22% открытий — прилично для неспамового приложения.
Expo Notifications обрабатывает кросс-платформенную доставку. Firebase Cloud Messaging на Android, APNs на iOS, один API для обоих. Настройки уведомлений по категориям — можно отключить новые мероприятия, не теряя напоминания.
Диплинки
Письмо подтверждения покупки включает диплинк: e7shop://ticket/abc123. Если приложение установлено — открывается прямо на билете. Если нет — фоллбэк на веб-страницу билета с баннером установки.
Одна эта фича увеличила установки на 40% в первый месяц. Люди покупают в вебе → получают письмо → тапают ссылку → устанавливают приложение → видят свой билет. Безтрастный онбординг.
Цифры
Шесть месяцев продакшн-данных:
| Метрика | Значение |
|---|---|
| Всего установок | 8,200+ |
| Рейтинг в сторе | 4.8★ (iOS) / 4.7★ (Android) |
| Безаварийных сессий | 99.7% |
| Завершение чекаута (мобилка) | 82% (vs 78% веб) |
| Среднее время выбора мест | 12s |
| Офлайн-билетов обслужено | ~1,400 |
| Подписка на пуш | 72% |
82% завершений чекаута в приложении vs 78% в мобильном вебе — скромно, но разрыв в удовлетворённости шире, чем цифры. Пользователи приложения оценивают выбор мест на 4.9/5 в пост-покупочных опросах; пользователи мобильного веба — на 3.4/5. Опыт фундаментально другой, даже если дельта конверсии небольшая.
1,400 обслуженных офлайн-билетов — это 1,400 человек, которые застряли бы на входе с «нет сигнала», если бы мы собрали PWA. На подземной площадке в Братиславе это примерно половина аудитории.
Таймлайн ревью в App Store
Реальность для всех, кто планирует мобильный запуск:
| Этап | День |
|---|---|
| Первая сборка в TestFlight | Неделя 6 |
| Внутреннее тестирование завершено | Неделя 8 |
| Отправка в App Store | Неделя 9, понедельник |
| Ревью Apple — первый отказ | Неделя 9, четверг |
| Исправление: добавлена кнопка «Restore Purchases» | Неделя 9, пятница |
| Повторная отправка | Неделя 9, пятница |
| Одобрено | Неделя 10, вторник |
| Отправка в Google Play | Неделя 9, понедельник |
| Одобрено | Неделя 9, среда |
Apple отклонил нас за отсутствие кнопки «Restore Purchases», хотя мы не продаём покупки в приложении — билеты покупаются через Stripe в вебе, а приложение только показывает их. Ревьюер Apple интерпретировал показ билетов как поток покупки. Мы добавили no-op кнопку «Восстановить покупки», которая показывает «Все билеты синхронизированы из вашего аккаунта», и прошли при повторной отправке.
Урок: Закладывайте полную неделю на ревью App Store, даже для v1. Google Play одобрил за 2 дня. Apple — 8 дней с учётом цикла отказа.
Что удивило
Обработка кнопки «Назад» на Android сломала выбор мест. Дефолтное поведение React Navigation закрывало весь экран при нажатии аппаратной кнопки «Назад» во время выбора мест. Пользователи ожидали «снять последнее место», а не «уйти со страницы». Кастомная логика BackHandler исправила, но мы поймали это только потому, что бета-тестер сообщил о потере выбора.
Расход батареи от обновления QR. 30-секундная ротация QR изначально держала экран активным и перерисовывала компонент по таймеру. На некоторых Android-устройствах это съедало 8% батареи в час на экране билета. Переключились на обновления через requestAnimationFrame, которые работают только когда экран билета в фокусе — расход упал до менее 1%.
Пользователи не читали онбординг. Мы собрали 3-экранный онбординг с объяснением функций. Аналитика показала, что 78% пользователей свайпали все три экрана менее чем за 2 секунды — они не читали. Заменили на контекстные тултипы, появляющиеся при первом использовании каждой функции. Взаимодействие с справочным контентом выросло с 4% до 31%.
Что бы сделал иначе
Подключил бы Expo Updates с первого дня. Мы использовали EAS Build для бинарных релизов, но не включили OTA-обновления до 4-й недели после запуска. Это значит, что два критических фикса потребовали полной повторной отправки в App Store (3-дневный цикл каждый). Expo Updates запушил бы эти фиксы за минуты.
Использовал бы Expo Router вместо React Navigation. На момент разработки Expo Router был пре-v1, и мы выбрали стабильный вариант. Сейчас он зрелый, файл-базированный и автоматически обрабатывает диплинки. Сэкономил бы ~2 дня ручной настройки навигации и конфигурации диплинков.
A/B-тестировал бы начальный зум карты мест. Мы по умолчанию показываем весь зал. Большинство пользователей сразу зумят в предпочтительную секцию. Старт с зумом в самую популярную секцию (по историческим данным продаж) сэкономил бы жест и, вероятно, улучшил бы время выбора.
Бизнес-результат
У E7 теперь есть нативное приложение, которое установили 8,200+ человек, с рейтингом 4.8★ и 72% подпиской на пуш-уведомления. Это прямой маркетинговый канал к аудитории — без рекламных расходов, без реселлера, без посредника.
Завершение чекаута на мобилке выше в приложении, чем в вебе. Офлайн-билеты работают на подземных площадках. Ротация QR убила шаринг скриншотов. А поток диплинков из email в приложение стимулирует установки без единого попапа «скачайте наше приложение».
Приложение стоило E7 примерно 30% от того, что нативное агентство процитировало за iOS-only. Они получили обе платформы, общий бэкенд и команду из одного человека, отгрузившую за 10 недель.
Если вы собираете мобильный продукт и сомневаетесь, даст ли React Native нативное ощущение производительности — даст, для правильных юзкейсов. Выбор мест, карты, UI с тяжёлыми жестами — всё реально на 60fps с правильной архитектурой. Давайте обсудим ваш случай.