Senior Platform Engineer · Technical Leadership · Pickles Asia (Feb-Apr 2025)

React Native Platform Modernization
A 4-year-old legacy app with zero package upgrades, modernized in 3 months with zero regressions

Pickles Asia's app was locked on React Native 0.63.3 (2020) - every dependency untouched for 5+ years. React 16, Agora v4.1.2, Metro 0.67, Node 16, Ruby 2.7. Couldn't ship to iOS 18 or Android 14. Two engineers proposed Expo migration or Flutter rewrite. Both were wrong for a huge production app. Instead, I designed a 3-stage cross-platform upgrade (0.63→0.72), modernized the entire toolchain, built Fastlane CI/CD, fixed three silent blockers, shipped with zero regressions. 60% faster builds. Delivered in 3 months.

rn.platform.0.63→0.72Shipping
3
Stages
0
Regressions
12+
Packages
2 OS
iOS + Android
Cross-platform unblocked · Team velocity +60% · iOS 18 + Android 14 ready
The Challenge

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.

rn.blockers.audit3 Blockers
0.63.3
RN version (2020)
iOS 18
Target OS
4yr+
Tech gap
yarn iosCRASH (Dynatrace)
TestFlight upload409 (Bitcode)
IPA imagesMissing
Xcode runOK (masked issues)
RN 0.63.3 + iOS 18 · incompatibility matrix
The Solution

Three-stage phased migration with automated testing and CI/CD

1
Plan: Three-stage strategy with checkpoints - Phased upgrade with explicit go/no-go gates:
  • 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
2
Upgrade toolchain: Node, Ruby, Java - Modernize all dependencies into support windows:
  • 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
3
Stage 1: RN 0.63.3 → 0.64.0 - Minimal version bump, establish baseline:
  • Created feature/rn-stage-1 branch
  • Tested login, auctions, bidding, payments
  • Full regression testing passed
  • Branch independently deployable
4
Stage 2: RN 0.64.0 → 0.67.0 - SDK ecosystem stabilizes at 0.67:
  • Updated Gradle and CocoaPods versions
  • Created feature/rn-stage-2 branch
  • Full regression tests passed
  • Stable intermediate checkpoint
5
Stage 3: RN 0.67.0 → 0.72.0 - First RN with full iOS 18 & Xcode 16 support:
  • Created feature/rn-stage-3 branch
  • Metro bundling reconfigured for new assets
  • Full iOS 18 + Xcode 16 compatibility
  • Ready for latest production environment
6
Build Fastlane CI/CD pipeline - Automated build, test, and deployment lanes:
  • Build lane: tests + IPA generation
  • Test lane: staging validation
  • Deploy lane: TestFlight upload + bitcode strip
  • Zero manual deployments via GitHub Actions
7
Developer environment: Docker + Script Files - Standardized onboarding & local development:
  • 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
React Native 3-Stage Upgrade Pipeline
BASELINE · RN 0.63.3 (2020) · 3 BLOCKERSRN 0.63.3 · Node 16 · Ruby 2.7 · Java 11 · Gradle 7.5Dynatrace iOS crashAgora bitcode :409Metro asset failure3-STAGE SEQUENTIAL UPGRADE PIPELINEStage 1 · RN 0.63 → 0.64 · Fix Dynatrace iOS crashBlockerDynatrace iOS agentcrashes on startupRoot: RN arch mismatchFixPin Dynatrace 8.255RN arch alignedGo/No-GoiOS crash resolved ✓0 regressionsProceed ✓staging validated ✓Stage 2 · RN 0.64 → 0.67 · Fix Agora bitcode (TestFlight :409)BlockerAgora SDK bitcodeTestFlight HTTP 409Apple rejected uploadFixFastlane: strip bitcodeENABLE_BITCODE=NOXcode build flagGo/No-GoTestFlight upload ✓Agora streaming liveProceed ✓staging validated ✓Stage 3 · RN 0.67 → 0.72 · Fix Metro + iOS 18 compatBlockerMetro asset bundlersymlink resolution failGradle 7.5 incompatibleFixMetro config overrideGradle 7.5 → 8.2Java 11 → 17Go/No-GoiOS 18 + Android 14 ✓60% faster buildsReleased ✓TOOLCHAIN UPGRADES + FASTLANE CI/CDNode.jsRubyJavaGradleBefore16Before2.7Before11Before7.5After18After3.1After17After8.2Fastlane CI/CD - 3 Lanesbuild_lane · test_lane · deploy_lane (TestFlight + Android Play Internal)iOS 18 + Android 14 · 0 regressions · 60% faster builds · Fastlane automated3 independent blockers isolated and fixed per stage - no big-bang upgrade risk
Fastlane CI/CD: iOS + Android Automation
CI/CD TRIGGER CHAINDevelopergit push mainGitHubpush · PR mergeGitHub Actionsworkflow triggerFastlanelane runnerFASTLANE LANESlane :testJest (RN unit tests)RN integration testsstaging validationlane :buildXcode 16.2 · .IPAGradle 8.2 · .AABENABLE_BITCODE=NOlane :deployTestFlight uploadPlay Console submit0 manual stepsPLATFORM BUILD STACKSiOS Build StackRuby 3.1 · CocoaPods 1.16Xcode 16.2 · iOS 18 SDKDynatrace patch appliedBitcode disabled (Agora fix)Android Build StackNode 18 · Metro bundler 0.73Java 17 · Gradle 8.2React Native 0.72.0Metro bundling restoredDEPLOYMENT TARGETSApp StoreiOS 18 · TestFlight → productionGoogle Play StoreAndroid 14 · internal → productionFastlane · 60% faster builds · 0 regressions · manual deploys eliminated
The Results

Unblocked iOS 18 + Android 14 shipping. Zero regressions. Team velocity +60%.

3
Stages with explicit checkpoints
Planned three-stage migration with clear go/no-go criteria at each checkpoint.
Stage 1 (baseline and toolchain), Stage 2 (SDK ecosystem stabilisation), Stage 3 (final iOS 18 push).
Each stage was independently testable, independently deployable, and independently rollback-capable.
This approach reduced blast radius and provided decision points if issues arose.
0
Production regressions

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.

8
Tools modernized
Node.js 16 → 18 (LTS)
Ruby 2.7 → 3.1.2 (EOL management)
CocoaPods 1.12 → 1.16.1 (stability)
Java 11 → 17 (alignment)
Gradle 7.5 → 8.2.2 (Android tooling)
plus infrastructure updates. Every single tool in the stack was brought into support windows.
100%
Deployment automation
Fastlane CI/CD fixed eliminated manual Xcode deployments.
GitHub Actions on every push.
Automated bitcode stripping.
Automated TestFlight uploads.
Zero manual intervention required.
Engineering Decisions

Architectural choices, tradeoffs, and what nearly broke production

Why not Expo or Flutter rewrite?

Why upgrade instead of migrate/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 faster?

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?

Can we skip toolchain upgrades for now?

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?

If Stage 2 broke the app, could you quickly revert?

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?

When do you stop and rollback?

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.

THE TRADEOFFS

Cost · Risk · Tradeoffs

Estimated values based on production metrics and monitoring data

Cost Impact
12 weeks
Timeline investment
Three-stage approach took longer than a direct jump, but provided clear decision points and reduced blast radius. The real cost was coordination overhead, not engineering time.
Risk Reduction
minimized
Production risk
Full regression testing at each stage. Go/no-go checkpoints with explicit criteria. Staged rollout meant we caught blockers in staging, not production. Zero incidents post-launch.
Technical Tradeoff
+60%
Team velocity improvement
Modern toolchain (Node 18, Ruby 3.1, Gradle 8.2.2) reduced build times from 30 min to 18 min. Fastlane automation eliminated manual deployments. Onboarding friction dropped significantly.
unblocked
Platform longevity
iOS 18 support is now current. App can ship updates immediately. Future iOS releases won't break the platform. Toolchain is in support windows and maintainable for 2-3 more years without major upgrades.
Before vs After

Transformative Results: Before vs After

✕Before
  • 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
→
✓After
  • React Native 0.72iOS 18 compatible
  • Dynatrace fully patchedApp launches stable
  • Agora SDK rebuiltTestFlight shipping
  • Assets loading correctlyClean IPA builds
  • Modern toolchainFast dev cycle
⇪Business Impact
60+ version jump completed
3 critical blockers resolved
0 regressions
Tech Stack

What was used

FrameworkReact Native 0.63.3 → 0.72.0
iOSiOS 18 · Xcode 16.2
AndroidGradle 7.5 → 8.2.2
CI/CDFastlane · TestFlight
Native SDKsAgora SDK · Dynatrace
Node.js16.20.1 → 18.17.0
Ruby2.7.5 → 3.1.2
CocoaPods1.12.1 → 1.16.1

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.