На главную
Solo FrontendReact NativeMobile SDKOTA Updates

Onay — KwaakaQR SDK

2025 — н.в. · соло фронтенд-разработчик

Встроил купонный модуль в Onay — суперприложение с 800 тыс. ежедневных пользователей по всему Казахстану. Пользователи находят скидки ближайших ресторанов прямо внутри привычного приложения, без отдельной установки.

Работал соло на стороне фронтенда: полная ответственность за архитектуру, производительность и деплой. В тесном взаимодействии с backend-командой Kwaaka и iOS/Android разработчиками Onay, которые встраивали SDK в действующее приложение.

800 тыс.

ежедневных пользователей Onay

Алматы

первый запуск, партнёрские точки на карте

~5 мин

доставка критического обновления без App Store

Контекст и архитектура

SDK внутри нативного приложения — не то же самое, что обычное RN-приложение

React NativeTypeScriptiOS SDKAndroid SDKNativeModules
  • Kwaaka — платформа, которая помогает ресторанам запускать купонные акции. Onay — суперприложение с 800 тыс. ежедневных пользователей по всему Казахстану. Задача: дать пользователям Onay доступ к купонам прямо внутри привычного приложения, без установки отдельного.
  • Большинство React Native разработчиков делают standalone-приложения. SDK внутри нативного хоста — принципиально другая задача: нет контроля над lifecycle, нет своего root navigation, нет своего splash screen. SDK не должен крашить хост-приложение — краш означает падение самого Onay.
  • Выбор React Native обусловлен тем, что вся команда Kwaaka работает на стеке React (веб-продукты). Это позволило разделять компонентный подход, переиспользовать логику и не нанимать отдельных iOS/Android специалистов для фронтенда.
  • Effector выбран как стейт-менеджер, потому что другие продукты Kwaaka уже использовали его на вебе — единая ментальная модель для команды. Плюс Effector популярен в СНГ-сообществе: хорошая документация, активная поддержка, готовые паттерны для сложных зависимостей.

Почему эти технологии

React Native

Вся команда Kwaaka на React — единый стек

Effector

Используется в веб-проектах Kwaaka, популярен в СНГ

Farfetched

Кеш, ретраи, stale-while-revalidate из коробки

SDK vs Standalone App

✗ нет своего lifecycle✗ нет root navigation✗ нельзя крашить хост✓ встраивается в любой нативный app
OTA-обновления

Критические фиксы — без релиза в App Store

OTACloudflare R2CDNSHA-256CI/CD
  • Реализовал собственную систему over-the-air обновлений: JS-бандл собирается, загружается на Cloudflare R2 и применяется при следующем запуске SDK — без ревью в App Store и без обновления нативного приложения Onay.
  • Двухуровневая схема: latest.json — регулярные обновления раз в 30 минут, critical.json — сигнальный файл, который будит SDK в течение 5 минут если нужно срочно доставить фикс. Критическое обновление не значит «качай critical», оно говорит «иди немедленно проверь latest».
  • Верификация бандла через SHA-256 hash перед применением. При смене нативной версии SDK — кеш автоматически инвалидируется, чтобы старый JS-бандл не обращался к несуществующим нативным API.
  • Настроил CI/CD скрипт: сборка бандла → загрузка на R2 → обновление manifest-файлов → инвалидация CDN-кэша. Один запуск скрипта — и обновление у всех пользователей без единого релиза в сторах.

OTA схема

bundle.js → R2 CDN
latest.json (30 мин)
critical.json (~5 мин)
SHA-256 verify → apply

Без ревью в App Store.
Без обновления нативного Onay.

Интеграция с Onay

Нативный бридж без логина пользователя

BearerTokenJWTDeep LinksiOS SwiftAndroid Kotlin
  • Пользователь Onay уже авторизован в нативном приложении. SDK получает JWT, координаты и язык из нативной части через NativeModules — без всплывающих экранов входа, без повторной авторизации.
  • Тесно взаимодействовал с iOS и Android командами Onay: согласовывал API нативного бриджа, lifecycle SDK, обработку deep-link навигации и сценарии закрытия модального окна. Ни одна из сторон не меняла свою архитектуру — SDK встроился в существующий flow.
  • Совместно отладили граничные кейсы: холодный старт, фоновое обновление бандла, восстановление после краша SDK без падения хоста, корректный dismiss без утечек памяти.
  • Подготовил документацию для нативной команды: какие модули регистрировать, как передавать токен, как обрабатывать навигационные события со стороны SDK.

Инициализация SDK

NativeModules→ JWT токен→ координаты→ язык (RU/KZ/EN)SDK ready, без логина ✓

Платформы

iOSAndroid
iOS Swift + Android Kotlin

Публичный SDK API — одинаковый контракт на двух платформах

SwiftKotlinRCTBridgeReactActivityNativeModulesXCFrameworkAAR
  • На iOS написал публичный Swift API: CouponsSDK.present(from:token:onTokenRefresh:onDismiss:) — одна строка в хост-приложении. Внутри — KwaakaCouponsViewController с RCTRootView и KwaakaCouponsBridgeManager (singleton), который кеширует RCT bridge по токену. Один и тот же токен = переиспользование bridge, новый токен = новый bridge. SDK поставляется как XCFramework через Swift Package Manager.
  • Перехват RCTFatalHandler: при любом необработанном JS-исключении внутри SDK глобальный fatal handler временно заменяется на собственный — вместо краша хост-приложения показывается экран с ошибкой и кнопкой «Закрыть». После dismiss ViewController восстанавливает оригинальный handler. Без этого любая JS-ошибка роняла бы весь Onay.
  • На Android: KwaakaCouponsSDK.warmUp(application, token) — прогрев bridge на daemon-потоке (~60-80 MB RAM, не блокирует UI). CouponsActivity читает токен из Intent extra до super.onCreate() и прописывает его в BearerTokenModule — чтобы RN bridge получил токен ещё до первого рендера. При смене токена через onNewIntent вызывается recreate() для чистого старта.
  • При 401 от сервера с партнёрским JWT: SDK вызывает нативный callback onTokenRefresh (iOS — closure, Android — lambda), ждёт новый токен с таймаутом 30с, делает retry запроса. При повторном 401 — SDK закрывается сам. Хост-приложение не знает об этой логике, он просто передаёт callback обновления токена при открытии SDK.
  • Bridge живёт между сессиями — при повторном открытии SDK экран появляется мгновенно без холодного старта RN. Bridge сбрасывается только в двух случаях: смена токена и критический OTA-апдейт (consumeCriticalPending). OTA логика одинакова на обеих платформах: нет кеша → блокирующий sync-запрос при старте, есть кеш → async проверка в фоне.

iOS публичный API

CouponsSDK.present(from: viewController,token: jwt,onTokenRefresh: cb,onDismiss: cb)

Android публичный API

KwaakaCouponsSDK.warmUp(app, token)CouponsActivity.createIntent(ctx, token)
Технические решения

Соло: ты архитектор, ревьюер и инженер одновременно

PerformanceEffectorFarfetchedRe-renderStale Cache
  • Единственный фронтенд-разработчик проекта. Нет code review выше тебя, нет второго мнения перед merge — архитектурные решения необратимы и принимаются в одиночку. Это заставляет думать на два шага вперёд.
  • Пример нетривиальной проблемы: при свайпе между купонами весь хедер страницы ресторана (hero-картинка, логотип, инфоблок) перезагружался заново. Нашёл причину — feedback loop: смена купона → localActiveCouponId → renderListHeader пересоздаётся → CouponSliderPager ремаунтится → initialScrollIndex возвращает на первый слайд. Решение: мемоизировать статичные части хедера через useMemo (стабильный JSX-объект), вынести couponDetailSections из ListHeaderComponent в секции SectionList — renderListHeader перестал зависеть от localActiveCouponId вообще.
  • Другой пример: после смены геопозиции на не-Алматы и обратно — главная страница оставалась пустой. Farfetched отдавал кешированные пустые данные (< 30 сек) без сетевого запроса. Фикс: refresh() вместо start() при смене координат — принудительно обходит кеш.
  • Архитектура запросов на Farfetched: глобальные квери с кэшированием, stale-while-revalidate, автоматические ретраи на таймауты (522/0). Приложение работает устойчиво при нестабильном мобильном интернете.
  • Флоу корзины с купонами — один из самых сложных кусков клиентской логики. 6 типов купонов (процент, фиксированная сумма, подарок, платный подарок, 2+1, X+Y), каждый с собственной комбинацией условий разблокировки: минимальная сумма заказа, минимальное количество позиций, наличие конкретных товаров в корзине, количество уникальных позиций. Всё это проверяется на клиенте в реальном времени.
  • CouponThresholdHint — живой виджет прогресса: пока купон не разблокирован, показывает сколько ещё нужно добавить (в сумме или в штуках товаров), с прогресс-баром и точным текстом под каждый тип. Как только условие выполнено — виджет переключается в режим success с градиентной карточкой. Тексты динамически склоняются по правилам русской грамматики (1 товар / 2 товара / 5 товаров).
  • Перед оплатой: двойная валидация. Клиентская — блокирует кнопку и объясняет проблему до отправки запроса. Серверная — validateCouponCart перед созданием заказа, и если бэкенд отклоняет, ошибки показываются в красной карточке прямо в платёжном шите. При gift/bundle купонах фронтенд сам строит payload: resolves подарочный товар из условий купона, считает количество через getCouponEffectGetQuantity и добавляет его в items перед отправкой.
  • Дополнительная сложность: API возвращает поля в двух форматах — camelCase и snake_case (buyQty / buy_qty, minOrderValue / min_order_value). Весь слой нормализации написан вручную с WeakMap-кешированием для hotpath расчётов validItemIds.

Типы купонов

% / фиксированная суммаgift / paid_gift2+1 / X+Y bundle
min сумма заказаmin кол-во позицийконкретные товарыуникальные позиции

Флоу оплаты

1. клиентская валидация2. syncCouponCartDraft3. validateCouponCart4. createCheckout→ Kaspi / Halyk / Ioka
Запуск

Тестовый запуск в Алматы — основа для масштабирования

Geo-feedMultilangAlmatyПартнёрские точки
  • Запустились тестово в Алматы: первые партнёрские точки подключены, геолента купонов по координатам пользователя с радиусом поиска. Архитектура изначально спроектирована под рост — добавление нового города не требует изменений в клиентской части.
  • Реализовал мультиязычность (RU/KZ/EN) с автоопределением языка из JWT и переключением на лету. Все три языка поддерживаются полностью — важно для аудитории по всему Казахстану.
  • SDK встроен в Onay с аудиторией 800 тыс. ежедневных пользователей. Купонный модуль доступен части этой аудитории прямо сейчас, без отдельной установки.
  • Дистрибуция через Onay решает ключевую проблему нового продукта — не нужно строить свою аудиторию с нуля: пользователи уже есть, задача — дать им ценность в привычном контексте.

Текущий статус

Тестовый запуск в Алматы

первые точки → архитектура под рост

Команда

Я — соло фронтенд (RN)
Backend-команда Kwaaka
iOS/Android команда Onay

Нужна помощь с вашим проектом?

Есть опыт в мобильной разработке, SDK-интеграциях и кросс-командной работе. Готов обсудить вашу задачу.