Back to home
Solo FrontendReact NativeMobile SDKOTA Updates

Onay — KwaakaQR SDK

2025 — present · solo frontend developer

Built a coupon module embedded inside Onay — a super-app with 800K daily active users across Kazakhstan. Users discover discounts at nearby restaurants directly inside the app they already use, with no separate install.

Worked solo on the frontend side: full ownership of architecture, performance, and deployment. In close collaboration with the Kwaaka backend team and the Onay iOS/Android developers who embedded the SDK into their live app.

800K

daily active users on Onay

Almaty

pilot launch, partner venues live on the map

~5 min

critical update delivery without App Store

Context & Architecture

An SDK inside a native app is fundamentally different from a standalone RN app

React NativeTypeScriptiOS SDKAndroid SDKNativeModules
  • Kwaaka is a platform that helps restaurants run coupon campaigns. Onay is a super-app with 800K daily active users across Kazakhstan. The goal: give Onay users access to restaurant coupons directly inside the app they already have — no separate install required.
  • Most React Native developers build standalone apps. An SDK running inside a native host is a fundamentally different problem: no control over the lifecycle, no own root navigation, no own splash screen. The SDK must never crash the host — a crash means bringing down Onay itself.
  • React Native was chosen because the entire Kwaaka team works on a React stack (web products). This allowed sharing the component approach, reusing business logic, and avoiding separate iOS/Android frontend hires.
  • Effector was chosen as the state manager because other Kwaaka products already used it on the web — one mental model for the whole team. It's also popular in the CIS developer community: good documentation, active support, and proven patterns for complex dependencies.

Why these technologies

React Native

Whole Kwaaka team is on React — one unified stack

Effector

Used in Kwaaka web products, popular in CIS community

Farfetched

Cache, retries, stale-while-revalidate out of the box

SDK vs Standalone App

✗ no own lifecycle✗ no root navigation✗ must never crash the host✓ embeds into any native app
OTA Updates

Critical fixes — without an App Store release

OTACloudflare R2CDNSHA-256CI/CD
  • Built a custom over-the-air update system: the JS bundle is compiled, uploaded to Cloudflare R2, and applied on the next SDK launch — no App Store review, no update to the native Onay app.
  • Two-level scheme: latest.json — regular checks every 30 minutes; critical.json — a signal file that wakes the SDK within ~5 minutes when an urgent fix needs to be delivered. A critical update doesn't mean 'download critical' — it means 'go check latest immediately'.
  • Bundle integrity verified via SHA-256 hash before applying. When the native SDK version changes, the cache is automatically invalidated so an old JS bundle doesn't call non-existent native APIs.
  • Set up a CI/CD script: build bundle → upload to R2 → update manifest files → invalidate CDN cache. One script run — and the update reaches all users with zero store releases.

OTA scheme

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

No App Store review.
No update to the native Onay app.

Onay Integration

Native bridge with no user login

BearerTokenJWTDeep LinksiOS SwiftAndroid Kotlin
  • The Onay user is already authenticated in the native app. The SDK receives the JWT, coordinates, and language from the native layer via NativeModules — no login screens, no re-authentication.
  • Worked closely with Onay's iOS and Android teams: agreed on the native bridge API, SDK lifecycle, deep-link navigation handling, and modal dismiss flows. Neither side changed their architecture — the SDK slotted into the existing flow.
  • Jointly debugged edge cases: cold start, background bundle updates, SDK crash recovery without taking down the host, and clean dismiss without memory leaks.
  • Prepared documentation for the native team: which modules to register, how to pass the token, how to handle navigation events from the SDK side.

SDK initialisation

NativeModules→ JWT token→ coordinates→ language (RU/KZ/EN)SDK ready, no login ✓

Platforms

iOSAndroid
iOS Swift + Android Kotlin

Public SDK API — same contract on both platforms

SwiftKotlinRCTBridgeReactActivityNativeModulesXCFrameworkAAR
  • On iOS, wrote a public Swift API: CouponsSDK.present(from:token:onTokenRefresh:onDismiss:) — one line in the host app. Internally — KwaakaCouponsViewController with RCTRootView and KwaakaCouponsBridgeManager (singleton) that caches the RCT bridge by token. Same token = reuse bridge, new token = new bridge. SDK delivered as XCFramework via Swift Package Manager.
  • RCTFatalHandler interception: on any unhandled JS exception inside the SDK, the global fatal handler is temporarily replaced — instead of crashing the host app, an error screen with a 'Close' button is shown. After dismiss the ViewController restores the original handler. Without this, any JS error would take down all of Onay.
  • On Android: KwaakaCouponsSDK.warmUp(application, token) — warms up the bridge on a daemon thread (~60–80 MB RAM, non-blocking). CouponsActivity reads the token from Intent extras before super.onCreate() and registers it in BearerTokenModule — so the RN bridge gets the token before the first render. On token change via onNewIntent, recreate() is called for a clean restart.
  • On 401 from the server with a partner JWT: the SDK calls the native onTokenRefresh callback (iOS — closure, Android — lambda), waits for a new token with a 30s timeout, then retries the request. On a second 401 — the SDK closes itself. The host app knows nothing about this logic; it just passes the token refresh callback when opening the SDK.
  • The bridge survives between sessions — reopening the SDK shows the screen instantly with no RN cold start. The bridge resets only in two cases: token change and a critical OTA update (consumeCriticalPending). OTA logic is identical on both platforms: no cache → blocking sync request on startup, cache present → async background check.

iOS public API

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

Android public API

KwaakaCouponsSDK.warmUp(app, token)CouponsActivity.createIntent(ctx, token)
Technical Decisions

Solo: you are the architect, reviewer, and engineer simultaneously

PerformanceEffectorFarfetchedRe-renderStale Cache
  • Only frontend developer on the project. No code review above you, no second opinion before merge — architectural decisions are irreversible and made alone. This forces you to think two steps ahead.
  • Non-trivial bug example: when swiping between coupons, the entire restaurant page header (hero image, logo, info block) re-rendered from scratch. Root cause — a feedback loop: coupon change → localActiveCouponId → renderListHeader recreated → CouponSliderPager remounted → initialScrollIndex reset to first slide. Fix: memoize static header parts with useMemo (stable JSX object), move couponDetailSections out of ListHeaderComponent into SectionList sections — renderListHeader stopped depending on localActiveCouponId entirely.
  • Another example: after switching geolocation away from Almaty and back — the home screen stayed empty. Farfetched was serving cached empty data (<30 sec) without a network request. Fix: refresh() instead of start() on coordinate change — forces a cache bypass.
  • Request architecture on Farfetched: global queries with caching, stale-while-revalidate, automatic retries on timeouts (522/0). The app remains stable on unstable mobile connections.
  • The coupon cart flow is one of the most complex pieces of client-side logic. 6 coupon types (percentage, fixed amount, gift, paid gift, 2+1, X+Y), each with its own combination of unlock conditions: minimum order amount, minimum item count, presence of specific products, number of unique items. All validated on the client in real time.
  • CouponThresholdHint — a live progress widget: while the coupon is locked, shows how much more needs to be added (in amount or item count), with a progress bar and precise text per type. Once the condition is met — the widget switches to success mode with a gradient card.
  • Before payment: double validation. Client-side — blocks the button and explains the issue before sending any request. Server-side — validateCouponCart before order creation; if the backend rejects it, errors are shown in a red card inside the payment sheet. For gift/bundle coupons, the frontend builds the payload itself: resolves the gift item from coupon conditions, calculates quantity via getCouponEffectGetQuantity, and appends it to items before submission.
  • Additional complexity: the API returns fields in two formats — camelCase and snake_case (buyQty / buy_qty, minOrderValue / min_order_value). The entire normalization layer is written manually with WeakMap caching for hotpath calculations of validItemIds.

Coupon types

% / fixed amountgift / paid_gift2+1 / X+Y bundle
min order amountmin item countspecific required itemsunique item count

Payment flow

1. client-side validation2. syncCouponCartDraft3. validateCouponCart4. createCheckout→ Kaspi / Halyk / Ioka
Launch

Pilot launch in Almaty — the foundation for scaling

Geo-feedMultilangAlmatyPartner venues
  • Launched as a pilot in Almaty: first partner venues connected, geo-feed of coupons based on user coordinates with a configurable search radius. The architecture was designed for growth from day one — adding a new city requires no changes to the client.
  • Implemented multilingual support (RU/KZ/EN) with auto-detection of language from the JWT and on-the-fly switching. All three languages fully supported — important for a Kazakhstan-wide audience.
  • SDK embedded in Onay with 800K daily active users. The coupon module is available to a portion of that audience right now, with no separate install.
  • Distribution through Onay solves the core problem of a new product — no need to build an audience from scratch: the users are already there, the task is to give them value in a familiar context.

Current status

Pilot launch in Almaty

first venues live → architecture built to scale

Team

Me — solo frontend (RN)
Kwaaka backend team
Onay iOS/Android team

Need help with your project?

Experience in mobile development, SDK integrations, and cross-team collaboration. Happy to discuss your challenge.