React Native Platform Modernization
A 4-year-old legacy app with zero package upgrades, modernized in 3 months with zero regressions
Platform locked: 5 years of legacy packages, no iOS 18 / Android 14 support
React Native 0.63.3, released in October 2020, was never designed for iOS 18 or Xcode 16 or latest Android versions. The 4+ year gap between the framework and the OS it was running on created major blocking failures. Nothing upgraded. App couldn't ship to App Store or Play Store.
What made this project difficult was that the Simulator masked two of the three blockers - everything looked fine when launching from Xcode directly, but broke in the raw yarn ios environment and in actual TestFlight builds.
Some engineers proposed different paths: Expo (managed services) or Flutter rewrite. Both wrong for production scale.
Instead: systematic 3-stage upgrade of existing codebase, preserving 5 years of battle-tested business logic.
- Nothing upgraded: RN 0.63.3, React 16, Agora v4.1.2, Metro 0.67, Node 16, Ruby 2.7, Gradle 7.5, Java 11
- Can't ship: iOS 18 incompatible, Android 14 incompatible, app uninstallable
- Silent blockers unknown until staging: Dynatrace crash, Agora bitcode error, Metro asset failures
- Manual deployments: Xcode build (iOS) + Gradle build (Android), no CI/CD, no rollback
- Build times: 30 minutes. Dev onboarding painful. Tech debt everywhere.
| yarn ios | CRASH (Dynatrace) |
| TestFlight upload | 409 (Bitcode) |
| IPA images | Missing |
| Xcode run | OK (masked issues) |
Three-stage phased migration with automated testing and CI/CD
- Stage 1: RN 0.63→0.64, Stage 2: 0.64→0.67, Stage 3: 0.67→0.72
- Full regression testing in staging per stage
- Each stage independently deployable
- Safer than direct jump or expensive rewrite
- Node 16→18 (LTS), Ruby 2.7→3.1.2 (EOL)
- Java 11→17, Gradle 7.5→8.2.2
- CocoaPods 1.12→1.16.1
- Reduces future maintenance debt
- Created feature/rn-stage-1 branch
- Tested login, auctions, bidding, payments
- Full regression testing passed
- Branch independently deployable
- Updated Gradle and CocoaPods versions
- Created feature/rn-stage-2 branch
- Full regression tests passed
- Stable intermediate checkpoint
- Created feature/rn-stage-3 branch
- Metro bundling reconfigured for new assets
- Full iOS 18 + Xcode 16 compatibility
- Ready for latest production environment
- Build lane: tests + IPA generation
- Test lane: staging validation
- Deploy lane: TestFlight upload + bitcode strip
- Zero manual deployments via GitHub Actions
- Environment with Node 18, Ruby 3.1, CocoaPods, Gradle 8.2.2
- Script files for common development tasks
- New developers up & running in 1 command
- Eliminates environment setup friction
Unblocked iOS 18 + Android 14 shipping. Zero regressions. Team velocity +60%.
All three core flows fully validated at each stage: login, live auction, bidding, payment processing, vehicle listings, and profile management. Full regression test suite ran before each stage approval. Zero production incidents post-launch. This was achieved through rigorous staging validation and methodical progression.
Architectural choices, tradeoffs, and what nearly broke production
Why not Expo or Flutter rewrite?
Expo locks you into managed infrastructure. Flutter = rewrite 5 years of auction logic, payments, WebRTC. For production apps, both risk losing feature parity. Upgrade path preserves tested business logic. We're fixing infrastructure, not rebuilding the application.
Why 3 stages? Why not jump directly to 0.72?
Direct jump = 60+ version boundaries at once. If something breaks, no idea which version caused it. 3 stages isolate changes. Dynatrace crash: 0.63→0.64. Agora bitcode: 0.64→0.67. Metro bundling: 0.67→0.72. Each blocker traceable to specific stage. Prevents shipping broken app.
Upgrade toolchain together or later?
Separating them creates bottlenecks. Old CocoaPods can't use new Node. Old Ruby can't use new Xcode. Old Java can't use new Gradle. Upgrade everything together = one intense project now, but years of trouble-free builds for both platforms.
What if one stage had failed the checkpoint? What was the rollback plan?
Yes. Each stage is on its own branch (feature/rn-stage-1, feature/rn-stage-2, feature/rn-stage-3). If a stage fails its checkpoint (regression test failure, production blocker discovered, critical bug), that branch doesn't merge to main. We revert to the previous stage and investigate. The app continues shipping from the last good stage. In practice, we encountered blockers at each stage, but because they were caught during staging validation (not in production), we had time to diagnose and fix them before approving the next stage.
Go/no-go decisions: who decides?
Upfront: critical blocker >2 weeks = no-go. Major regression = no-go. Product + engineering leadership aligned on criteria. All 3 stages passed, but explicit framework meant team understood risks from day one.
Cost · Risk · Tradeoffs
Estimated values based on production metrics and monitoring data
Transformative Results: Before vs After
- React Native 0.63 (2020)iOS 18 incompatible
- Dynatrace crash on launchApp unusable
- Bitcode errors (Agora SDK)TestFlight blocked
- Missing static assetsBroken IPA builds
- Stale dev toolchainDev friction
- React Native 0.72iOS 18 compatible
- Dynatrace fully patchedApp launches stable
- Agora SDK rebuiltTestFlight shipping
- Assets loading correctlyClean IPA builds
- Modern toolchainFast dev cycle
What was used
Facing a legacy platform choice: rewrite, migrate, or upgrade?
I evaluate all three paths. I design cross-platform modernization with clear go/no-go checkpoints, build automation, and zero-regression deployments. I focus on preserving tested business logic and building for the long term. Let's talk about which path fits your situation.