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.
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 tagged status: 'offline' | 'online'.
A shared UploadModal component 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