Our React Native Upgrade Journey: Stability, Compliance & Performance Wins

Our mobile app is central to users experiencing Simpl — helping people make payments, track expenses, manage their pay-later cycles and discover merchants. To keep the user journey smooth, secure, and compliant with platform requirements, we periodically upgrade our core technology stack.

We recently moved the target and compile SDK versions from 34 to 35 in our Android app, which necessitated movement from React Native 0.75.2 to 0.79.2.

This upgrade was unexpectedly involved for us. This article describes why we needed to do the upgrade, the difficulties that we encountered and we solved those difficulties.

Why We Had to Upgrade

First, let's see the basic outline of Simpl mobile app.

80-85% of the mobile app code of Simpl is written in react native, and we use native android and iOS code for the core functionalities like SIM binding, camera and contact list related functionalities.

We recently leapt from React Native 0.75.2 → 0.79.2. On paper, it looks like a small version bump. In reality, it meant fixing critical crashes, staying ahead of Google Play Store's policy deadlines and future proofing our app for Android 15.

Crashes & Compliance

For most of the year, we were busy in implementing new features in the app, such as Select - which is the loyalty program for Simpl app, Quest - which is a rewards program, and OTM (one time mandate) using which users who don't have a khata with Simpl can block the money in their bank account and then get a khata with Simpl.

As a result, stability and compliance took a back seat. However, when the problems became sufficiently severe and some deadlines set by Google Play Store were on the horizon, we moved first to fix our app to resolve various stability and compliance related problems.

Firstly, there was a stability problem with the app. We used Digio SDK in our app to help us with eKYC of the users. Digio app used camera of the device to accomplish eKYC and that functionality required Android API level 35, so our app must be compileSdkVersion = 35. However, we we were at compileSdkVersion = 34 , and this was leading to 2-3% our users being hit by a crash as they tried to do their eKYC.

Secondly, Google Play Store mandated that all apps must target the API level 35 from August 31, 2025. Our app was still on API level 34, which meant were getting close to non compliance.

Thirdly, Play Store also mandated that starting November 1, 2025, all new apps and updates to existing apps submitted to Google Play and targeting Android 15+ devices must support 16 KB page sizes on 64-bit devices. This needed react native version upgrade to 0.77.

We kept minSdkVersion = 24 (Android 7.0). So yes, that trusty Moto G from 2016 still gets love ❤️.

What’s Changing Under the Hood

To better understand the changes, let's first get clarity on what is meant by minSdkVersion , targetSdkVersion and compileSdkVersion .

minSdkVersion is the minimum supported android framework version in the device. E.g. if minSdkVersion = 34 , then our app won't install on a device which has android framework version of 33.

compileSdkVersion is the version whose API has been used to compile the app. E.g. if compileSdkVersion = 35 , then our app can use APIs introduced in Android 15. However, using an API higher than the device version requires a runtime check.

targetSdkVersion is the highest API level with which the app has been designed, tested, and confirmed compatible. This is the version where the app is expected to behave "as intended."

In the above measures, following table summarizes the state of our app before and after the upgrade

Setting

Before

After

minSdkVersion

24

24 (unchanged)

targetSdkVersion

34

35

compileSdkVersion

34

35

buildToolsVersion

34.0.0

35.0.0

React Native

0.75.2

0.79.2

We could have taken the “safe” route and inched forward through 0.76.x and 0.77.x. Instead, we made a leap straight to 0.79.2.

Firstly, we decided the bigger jump would lead to fewer upgrade cycles ahead, saving time and effort in the near future. Secondly, moving to 0.79.2 gets us the bug fixes done in that version.

Our Upgrade Process

Step 1: The Checklist

We ran npm outdated to get the outdated project dependencies. Other than that we verified that all our development environments (node, ruby, JDK, Xcode) met the new target version's need.

Besides, we made a list of critical user flows using native code so that we make sure to test them post development to ensure that nothing is broken.

With the audit complete and our risks mapped, we safely isolated the work:

git checkout -b feature/rn-upgrade-0.79.2

Step 2: Core React Native Upgrade

For the Core package update, we relied heavily on the React Native Upgrade Helper. This tool provided a necessary side-by-side diff that eliminated hours of guesswork.

We systematically applied the changes it recommended, focusing on stabilizing the native build environment. Key configuration files that required careful manual merging included:

  • Android: android/build.gradle and android/app/build.gradle (for SDK and Gradle alignment).
  • iOS: ios/Podfile (for dependency).
  • Metro: metro.config.js (for bundler updates).

Step 3: Dependency and Environment Stabilization

This is where the real challenges begin. A React Native upgrade is never about updating the framework alone—it’s a massive dependency alignment project where every third-party library demanded attention.

dependcy fun images.png

The core challenge wasn't just version bumping; it was deep-diving into native module behavior. We faced two major bottlenecks:

The Firebase Failures

Updating the core framework broke critical components of RN Firebase (Push Notifications, Remote Config, etc.), severely impacting stability.

The Problem

Our Solution

Push Notifications (PN): FCM token registration and notification delivery became inconsistent due to race conditions and changes in the RN bridge timing.

We fixed the Initialization Timing, moving Firebase setup earlier in the app lifecycle, and updated iOS background task registration to restore reliable PN delivery.

Remote Config: The component stopped fetching or caching configurations, always returning default values due to new RN networking and file system changes.

We implemented a Cache Reset Strategy on app updates, ensuring the Remote Config initialization sequence ran after core Firebase setup to correctly read new values.

The Linear Gradient Layout Bugs

The react-native-linear-gradient library, a key aesthetic component, was plagued by two issues stemming from the new RN 0.79 layout engine.

The Problem

Our Solution

Padding Issue: Padding areas outside the gradient rendered as ugly white gaps.

We employed the Wrapper View Approach, applying padding to a parent view and letting the gradient fill the interior boundary.

Architecture Conflict & Aesthetic Failures: The library attempted to initialize New Architecture (JSI) components in our Old Architecture (Bridge) project, which, combined with stricter color parsing in RN 0.79, caused gradients on iOS to suddenly render as solid black or transparent.

We enforced Explicit Architecture Configuration in our build files to force the library into Old Architecture mode and standardized all color inputs to the simple 6-digit hex format to bypass new native parsing errors.

By fixing these critical issues and aligning all our core Build Tools (Gradle to 8.x, CocoaPods to 1.15.2), we finally achieved a stable, working build on the new React Native foundation.

Dev challenges that we faced

During the implementation of above mentioned upgrades, we faced the following problems.

1.Build errors due to cached dependencies

Old caches caused build errors and the inevitable "works on my machine" arguments. A thorough cache flush was a mandatory step to eliminate residual artifacts and ensure a clean, stable environment for the new React Native version.

At the build time as well as at the run time we did the following -

  • Native Builds: We deleted local build artifacts (rm -rf android/.gradle android/build).
  • Dependencies: We removed JavaScript dependencies (rm -rf node_modules).
  • iOS Environment: We cleaned the CocoaPods setup (cd ios && pod cache clean --all && rm -rf Pods && pod install).
  • Bundler: Finally, we reset the Metro bundler (npm start -- --reset-cache).

2. Backward Compatibility

Even while targeting new SDKs (like Android 15), we were committed to supporting our older user base. We achieved this stability by implementing conditional logic throughout our app, ensuring that new native features and API calls always had a stable fallback path:


import { Platform } from 'react-native';

if (Platform.OS === 'android' && Platform.Version >= 35) {
  // Android 15 features
} else {
  // Older versions fallback
}
  1. Common Challenges & Quick Fixes

Here are some of more challenges that faced and how to got over them.

Firstly, for build failures, we ensured that we update gradle and AGP (android gradle plugin). A good fraction of build failures we solved by using updated gradle and AGP.

Secondly, for problems related to third party libraries, we read the changelogs of third party libraries and read the github issues of those libraries. For instance, for the padding problem that we described above, on searching in the github for the problem, we found the patch that had not yet been merged. So, we merged the patch in our codebase to solve the problem.

From Planning to Rollout

Team Workflow & Execution

This entire upgrade was managed primarily by a single dedicated engineer, ensuring clear ownership and efficient decision-making. The core goal was to minimize disruption, and we succeeded: the rest of the team continued shipping new features without major interruptions. When a dependency required deeper, specialized knowledge, we brought in module experts. When stability required verification, the QA team executed focused regression testing. By keeping ownership clear and collaboration focused, the upgrade progressed efficiently in the background.

Post-Upgrade Validation

After the upgrade was complete, the app was thoroughly validated across three layers: stability, performance, and compatibility.

  1. Automated Testing: Our Jest and snapshot test suites ran successfully, confirming the integrity of the JavaScript logic and component rendering.
  2. Manual QA: The QA team meticulously verified all critical user flows—including payments, login, and checkout—across a wide range of devices, from older Moto G models to the latest Pixel 8, ensuring broad OS and device compatibility.
  3. Performance & Monitoring: Performance benchmarks confirmed a noticeable improvement in startup times. Real-time monitoring through Firebase Crashlytics and Sentry provided the final proof, confirming that no regressions or new stability issues were present in the upgraded build.

This multi-layered validation strategy provided the confidence needed to proceed with the final rollout.

Rollout plan

To minimize user impact and risk, we adopted a phased rollout strategy. Each stage was carefully monitored, allowing any issues to be caught and fixed rapidly before impacting the wider user base. This active monitoring was crucial for ensuring a stable, controlled deployment.

The rollout plan looked like this:

rollout-graph.png

(Crash monitoring + performance tracking at each stage ensured stability before moving forward.)

Upgrade Results

The upgrade went smoothly and proved to be stable. Users experienced fewer crashes, and the app maintained reliability across both Android and iOS. It fully met Google Play’s latest compliance requirements, and new Android 15 protections strengthened user security.

Number of daily crashes. We deployed the changes mid august.

Performance improved noticeably — the app started faster, felt more responsive, and the bundle became leaner. At the same time, backward compatibility was preserved, so even users on older devices continued to enjoy a smooth and stable experience. Overall, the upgrade not only enhanced performance and reliability but also positioned the app to better handle future platform updates.

Lessons Learned

A few key practices made the upgrade successful. Having a single owner for the process created clear accountability, while documenting each step improved transparency. Validating changes in small increments helped reduce risk, and continuous monitoring ensured that rollout decisions were backed by real data.

The process also highlighted areas for improvement. Starting dependency upgrades earlier would prevent last-minute blockers, running smoke tests on older devices sooner would catch regressions faster, involving QA earlier would spread testing more evenly, and documenting native changes in real time would make handoffs smoother.

The Way Forward

With the upgrade complete, the app is in a much stronger position. Build times are faster, long-standing technical blockers have been resolved, and improved startup time. This foundation enables the team to adopt upcoming React Native features more easily and stay aligned with future platform requirements.

The journey was complex but worthwhile. Our careful planning, rigorous dependency management, and thorough device validation helped us improve stability, performance, and compliance for every user. Beyond the immediate gains, the process has strengthened the entire engineering team, equipping us with better practices for a smoother, faster, and more predictable next iteration.