Supreme Seed Power+
An offline-first React Native field-operations app for agricultural field officers — GPS-tagged crop programs, dealer registration, and a sync layer resilient enough to survive a major native SDK upgrade without breaking production.
Overview
Supreme Seed Power+ is a field-operations app for Surovi Group's seed business (HeeraBEEJ), used by field officers (FPFs) to run GPS-tagged crop programs and register new dealer/customer accounts from rural areas where connectivity is a maybe, not a given. The app is fully Bengali for its field-worker user base and syncs to a Laravel Sanctum backend once a connection is available.
The engineering problem isn't the form-filling — it's making every write survive an unknown length of offline time without losing data or double-submitting, while the surrounding native toolchain (Expo, React Native, MMKV, React Navigation) keeps shifting underneath a production app that can't afford downtime.
I own the mobile application end-to-end: architecture, offline-sync design, native build pipeline, and ongoing feature delivery.
The Problem
Field officers work in areas with unreliable or no mobile signal, but still need to capture GPS-tagged crop program data and register new dealers/customers as part of their daily routine. A web dashboard or an online-only app isn't viable — an officer can't be expected to lose a form because a village had no coverage, and the backend can't accept duplicate or partial submissions once connectivity returns.
On top of the offline constraint, the app has to keep shipping through a fast-moving native ecosystem: Expo and React Native both ship breaking changes multiple times a year, and Android's targetSdk requirements force periodic upgrades that aren't optional. A production app used daily by field staff has very little tolerance for an upgrade that quietly breaks auth, native modules, or the build pipeline.
My Role
- Designed and implemented the offline-first sync architecture (queue, dual-phase upload, status lifecycle) from scratch
- Own the native build pipeline: EAS builds, Android signing, and the SDK/React Native upgrade path
- Led a full field-by-field reconciliation of the customer-registration API contract against a backend specification, shipping the required type and UI changes
- Diagnosed and resolved a multi-layered native dependency upgrade (Expo SDK, React Native, MMKV, React Navigation) required for Android API 36 (targetSdk 36) Play Store compliance
- Investigated and characterized a platform-level Android IME input bug through multiple engineering iterations, documenting root cause analysis for future reference
Technical Stack
| Layer | Choice |
|---|---|
| Framework | React Native 0.81 (New Architecture) via Expo SDK 54 |
| Routing | Expo Router (file-based navigation) |
| UI | React Native Paper (Material Design 3) |
| State | Zustand |
| Offline storage | MMKV (Nitro Modules), dual-instance design for auth vs. domain data |
| Networking | Axios, with a custom token-refresh interceptor chain |
| Backend | Laravel + Sanctum token auth (external, consumed via REST) |
| Build / Distribution | EAS Build (Android APK/AAB), Gradle |
| Language | TypeScript (mixed with legacy JS files being migrated incrementally) |
Technical Decisions
Offline-First Data Flow
Both the program-creation and customer-registration flows are designed to work with zero connectivity:
- The field officer captures data — GPS location, photos, form fields — entirely offline.
- Records queue locally in a Zustand store persisted to MMKV (
programStore.ts,customerStore.ts), each item taggedstatus: 'offline' | 'online'. - A shared
UploadModalcomponent detects connectivity (hooks/useOnlineStatus.ts) and drives a two-phase sync: images upload first via a dedicated endpoint, then the full record posts with the server-returned image paths substituted in. - Successfully synced records flip to
status: 'online'and stay visible/editable (read-only) in-app — nothing is discarded after submission, giving officers a durable local audit trail.
Multi-Step Form State Management
Customer registration is a 4-step wizard — personal info, address, business details, attachments — backed by a single Zustand store holding per-step partial state, merged into a complete payload at submission time. Each screen validates and persists independently, which supports mid-flow exit/resume and post-submission edit/read-only review without pulling in a heavier form library.
Dual MMKV Instance Design
Two separate MMKV configurations are deliberately kept apart: one for auth/session data (token, hydration state), one for domain/business data (offline queues). This isolates blast radius — clearing or migrating one never risks corrupting the other — and lets the auth layer hydrate independently of business data during cold start.
Auth Hydration Gate
A dedicated gate in the protected route layout blocks child screen rendering until MMKV → Zustand hydration completes. This closes a real race condition where tab screens' effects fired before the token loaded, producing intermittent 401s — a subtle bug class common to persisted-state React Native apps that's easy to miss without an explicit gate.
Resilient API Layer
The Axios client layers two independent recovery paths: a request interceptor that falls back to reading the token directly from MMKV if the Zustand store hasn't hydrated yet, and a response interceptor that retries once on 401 by re-reading the token from storage — bypassing the store entirely — guarded by a retry flag to prevent loops. This keeps the app resilient to store/storage timing skew without scattering manual retry logic through call sites.
Field Conditions & UX
- Full Bengali UI for a field-worker user base, including Bengali-safe date formatting (avoiding locale-dependent numeral rendering the backend can't ingest) and font-rendering fixes for Bengali glyph metrics inside constrained UI containers.
- GPS capture optimized for field conditions: attempts last-known-location first for instant feedback, then falls back to a fresh low-accuracy fix, balancing speed against rural GPS signal variability.
- In-app camera capture instead of the OS camera intent, to sidestep Android 14/15 FileProvider/intent inconsistencies that caused unreliable photo capture on officers' actual devices.
- Client-side image compression before upload, since field officers regularly operate on constrained mobile data in low-signal areas.
Challenges & Solutions
Challenge: A major native SDK upgrade (Expo 53 → 54 / RN 0.79 → 0.81) was required for Android API 36 compliance, and it surfaced far more than a routine dependency bump.
Solution: Resolved each break on its own terms rather than papering over it: migrated off react-native-mmkv v3 after its C++ codegen stopped compiling against RN 0.81's C++20 bridging headers, adopting v4's Nitro Modules rewrite and its createMMKV()/.remove() API; routed around expo-file-system's API-shape change via its /legacy compatibility import rather than rewriting working file-handling logic; diagnosed a duplicate-module resolution bug across @react-navigation/* packages (root node_modules vs. Expo Router's bundled copies) that was crashing NavigationContent at runtime, fixed by realigning dependency ranges; and identified which hand-maintained Android customizations — release keystore, signing credentials, a manifest permission suppression — don't survive expo prebuild --clean, restoring them systematically instead of losing release-signing configuration.
Challenge: A genuinely tricky Android input bug — forcing upper-case transformation on a controlled TextInput while typing caused characters to duplicate, most visible right after switching input methods.
Solution: Traced it to composing-region conflicts between the JS-driven value rewrite and the native keyboard's predictive-text engine. Iterated through several mitigation strategies — native autoCapitalize, blur-time normalization, disabling autocorrect/spellcheck, forcing a non-predictive keyboard type — to characterize the actual root cause before the feature was descoped, rather than shipping a fix that only masked the symptom.
Challenge: An authoritative backend field specification arrived after a feature had already shipped, with no clear list of what had drifted.
Solution: Ran a systematic field-by-field diff against the live implementation — types, store, and three UI screens — surfacing a missing field, six missing localized companion fields, and several TypeScript nullability mismatches, and closed the gaps without re-deriving the feature from scratch.
Outcome
- Offline queue and dual-phase sync mean a field officer's work survives any length of connectivity gap, with zero duplicate submissions or silent data loss
- The Expo 54 / RN 0.81 upgrade shipped clean despite four independent breaking changes across the native dependency graph, keeping the app compliant with Android's targetSdk 36 requirement
- A documented root-cause investigation of the IME duplicate-character bug left a clear record for the next engineer who hits a similar composing-region issue
- Field-by-field API reconciliation closed a real contract drift without a feature rewrite
- Live on Google Play, v2.1.4, in active development