Skip to content

Latest commit

 

History

History
644 lines (471 loc) · 19 KB

File metadata and controls

644 lines (471 loc) · 19 KB

Publishing React Native Apps

Table of Contents


Release Overview

Publishing a React Native app involves native binary releases (App Store / Play Store) and optionally over-the-air (OTA) JavaScript updates for non-native changes.

┌──────────────────────────────────────────────────────────┐
│                    Release Pipeline                       │
├──────────────┬─────────────────────┬─────────────────────┤
│   Build      │   Store Submission │   OTA (optional)    │
│   iOS .ipa   │   App Store Connect│   CodePush / EAS    │
│   Android AAB│   Google Play Console│  JS/asset updates  │
└──────────────┴─────────────────────┴─────────────────────┘
Release Type Can Update Review Required Use Case
Store release Native + JS Yes (1-3 days) New native modules, permissions
OTA update JS/assets only No Bug fixes, UI changes, logic

iOS App Store — Certificates and Provisioning

Apple Developer Program Requirements

  • Enroll in Apple Developer Program ($99/year)
  • Create an App ID (Bundle Identifier, e.g., com.company.app)
  • Configure capabilities (Push Notifications, Associated Domains, etc.)

Certificate Types

Certificate Purpose
Apple Development Debug builds on registered devices
Apple Distribution App Store and Ad Hoc releases
Push Notification APNs authentication

Provisioning Profiles

A provisioning profile links:

  • App ID
  • Distribution certificate
  • Device list (development/ad hoc) or App Store distribution
# Modern approach: Xcode Automatic Signing
# Xcode → Target → Signing & Capabilities → Automatically manage signing

# Manual / CI approach with Fastlane Match
# Stores certs and profiles in encrypted git repo

Fastlane iOS Release Example

# ios/fastlane/Fastfile
default_platform(:ios)

platform :ios do
  desc "Build and upload to App Store Connect"
  lane :release do
    setup_ci if ENV['CI']

    match(
      type: "appstore",
      app_identifier: "com.company.myapp",
      readonly: true
    )

    increment_build_number(xcodeproj: "MyApp.xcodeproj")

    build_app(
      scheme: "MyApp",
      export_method: "app-store",
      configuration: "Release"
    )

    upload_to_app_store(
      skip_metadata: false,
      submit_for_review: false,
      precheck_include_in_app_purchases: false
    )
  end
end

App Store Connect Steps

  1. Create app record in App Store Connect
  2. Upload build via Xcode, Transporter, or Fastlane
  3. Fill metadata: description, screenshots, privacy policy URL
  4. Complete App Privacy questionnaire
  5. Submit for review
  6. Release manually or automatically after approval

TestFlight

Use TestFlight for beta distribution before public release. Internal testers (up to 100) get builds immediately; external testers require brief Beta App Review.


Google Play Store — AAB and Signing

Android App Bundle (AAB)

Google Play requires AAB format (not APK) for new apps. Google generates optimized APKs per device configuration (screen density, CPU architecture, language).

# Generate release AAB
cd android
./gradlew bundleRelease

# Output: android/app/build/outputs/bundle/release/app-release.aab

Signing Configuration

// android/app/build.gradle
android {
    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
                storeFile file(MYAPP_UPLOAD_STORE_FILE)
                storePassword MYAPP_UPLOAD_STORE_PASSWORD
                keyAlias MYAPP_UPLOAD_KEY_ALIAS
                keyPassword MYAPP_UPLOAD_KEY_PASSWORD
            }
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
# android/gradle.properties (never commit passwords — use CI secrets)
MYAPP_UPLOAD_STORE_FILE=my-release-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=*****
MYAPP_UPLOAD_KEY_PASSWORD=*****

Generate Upload Keystore

keytool -genkeypair -v \
  -storetype PKCS12 \
  -keystore my-release-key.keystore \
  -alias my-key-alias \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

Critical: Back up your keystore. Google Play App Signing can recover upload key loss, but losing both is catastrophic.

Fastlane Android Release

# android/fastlane/Fastfile
default_platform(:android)

platform :android do
  desc "Deploy to Google Play internal track"
  lane :internal do
    gradle(task: "clean bundleRelease")

    upload_to_play_store(
      track: "internal",
      aab: "app/build/outputs/bundle/release/app-release.aab",
      json_key: "play-store-credentials.json"
    )
  end
end

Play Console Tracks

Track Audience Purpose
Internal Up to 100 testers Fast CI validation
Closed Invited groups Beta testing
Open Public opt-in Large beta
Production All users Live release

CodePush and OTA Updates

Microsoft CodePush (via App Center) delivers JavaScript bundle updates without store review.

What OTA Can and Cannot Update

Allowed Not Allowed
JS bundle changes Native module additions
Image/asset updates Permission changes
Bug fixes in logic SDK version bumps
UI layout changes New native dependencies

CodePush Setup (React Native CLI)

npm install react-native-code-push
appcenter apps create -d MyApp-iOS -o iOS -p React-Native
appcenter apps create -d MyApp-Android -o Android -p React-Native
appcenter codepush deployment add -a MyOrg/MyApp-iOS Staging
appcenter codepush deployment add -a MyOrg/MyApp-iOS Production
// App.tsx
import codePush from 'react-native-code-push';

function App() {
  return <RootNavigator />;
}

const codePushOptions = {
  checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
  installMode: codePush.InstallMode.ON_NEXT_RESTART,
  mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
};

export default codePush(codePushOptions)(App);

Releasing an OTA Update

# Release to Staging
appcenter codepush release-react \
  -a MyOrg/MyApp-iOS \
  -d Staging \
  --description "Fix checkout bug"

# Promote Staging → Production after validation
appcenter codepush promotion -a MyOrg/MyApp-iOS -s Staging -d Production

CodePush Best Practices

  • Always test on Staging deployment first
  • Use mandatory flag for critical security fixes
  • Maintain compatibility between native binary and JS bundle versions
  • Log CodePush version in crash reports for debugging

Expo Updates (EAS Update)

Expo projects use EAS Update instead of CodePush for OTA delivery.

npm install -g eas-cli
eas login
eas update:configure
// app.json
{
  "expo": {
    "runtimeVersion": {
      "policy": "appVersion"
    },
    "updates": {
      "url": "https://u.expo.dev/your-project-id"
    }
  }
}
# Publish an update to a channel
eas update --branch production --message "Fix login redirect"

# Build native binary separately
eas build --platform all --profile production

Runtime Version

runtimeVersion ensures OTA updates only apply to compatible native binaries. Mismatch prevents crashes from missing native modules.

Policy Behavior
"appVersion" Tied to version in app.json
"nativeVersion" Tied to build number
Custom string Manual control

Versioning Strategy

Semantic Versioning

MAJOR.MINOR.PATCH
  2   .  4  .  1

MAJOR — Breaking changes
MINOR — New features (backward compatible)
PATCH — Bug fixes

Platform-Specific Version Numbers

Platform User-Facing Internal Build
iOS CFBundleShortVersionString (1.2.3) CFBundleVersion (42)
Android versionName (1.2.3) versionCode (42)
// package.json
{ "version": "1.2.3" }
# Fastlane — sync versions
increment_version_number(version_number: "1.2.3")
increment_build_number
// android/app/build.gradle
defaultConfig {
    versionCode 42
    versionName "1.2.3"
}

Versioning Rules

  1. Increment build number on every store upload (even same version)
  2. Never reuse version codes on Play Store
  3. Tag git commits with release versions
  4. Changelog per release for support and rollback decisions

Staged Rollout and Release Management

iOS Phased Release

App Store Connect offers phased release over 7 days:

  • Day 1: ~1% of users
  • Gradually increases to 100% by day 7
  • Can pause at any point

Android Staged Rollout

Play Console allows percentage rollout (1% → 5% → 20% → 50% → 100%):

Production release → Start at 10% → Monitor crash rate → Increase to 50% → Full rollout

Release Monitoring

Monitor during rollout:

  • Crash-free rate (Firebase Crashlytics, Sentry)
  • ANR rate (Android)
  • Key business metrics (conversion, retention)
  • Store reviews and support tickets

Rollback Strategy

Scenario Action
JS bug Push OTA fix or rollback CodePush/EAS deployment
Native crash Halt rollout; submit hotfix binary
Critical issue Pull app from sale (last resort)

Pre-Release Checklist

## Pre-Release Checklist

### Code Quality
- [ ] All tests pass in CI
- [ ] No debug logs or dev API URLs
- [ ] Feature flags configured for production
- [ ] Error reporting (Sentry) configured for release

### iOS
- [ ] Version and build number incremented
- [ ] Provisioning profile valid
- [ ] Privacy manifest (PrivacyInfo.xcprivacy) complete
- [ ] App Store screenshots and metadata updated
- [ ] TestFlight beta tested

### Android
- [ ] versionCode incremented
- [ ] Release keystore configured in CI
- [ ] ProGuard rules tested (no runtime crashes)
- [ ] Play Store listing updated
- [ ] Internal track tested

### OTA (if applicable)
- [ ] Runtime version matches native binary
- [ ] Staging deployment validated
- [ ] Rollback plan documented

Interview Questions & Answers

Q1: Walk through the iOS App Store release process for a React Native app.

Answer:

  1. Apple Developer account — Enroll and create App ID matching bundle identifier.
  2. Certificates & profiles — Create Distribution certificate and App Store provisioning profile (or use Xcode automatic signing / Fastlane Match).
  3. Configure release build — Set version (CFBundleShortVersionString) and build number (CFBundleVersion) in Xcode or via Fastlane.
  4. Archive & upload — Build release scheme, archive in Xcode, upload to App Store Connect via Transporter or Fastlane upload_to_app_store.
  5. App Store Connect — Select build, complete metadata, screenshots, privacy questionnaire, and export compliance.
  6. TestFlight — Optional beta testing with internal/external testers.
  7. Submit for review — Apple review takes 24-48 hours typically.
  8. Release — Manual release or automatic after approval; optionally enable phased release.

React Native-specific: ensure Hermes bytecode, native modules, and privacy manifests are correctly configured before archiving.


Q2: What is an Android App Bundle (AAB) and how do you sign a release build?

Answer:

AAB is Google's publishing format that contains compiled app resources and native libraries. Google Play's servers generate optimized APKs for each device configuration — resulting in smaller downloads than universal APKs.

Signing steps:

  1. Generate upload keystore with keytool
  2. Configure signingConfigs.release in android/app/build.gradle
  3. Store credentials in gradle.properties or CI environment variables (never in git)
  4. Run ./gradlew bundleRelease to produce signed AAB
  5. Upload to Play Console

Play App Signing: Google manages the app signing key; you upload with an upload key. This enables key recovery if upload key is lost.

Enable ProGuard/R8 for release builds to shrink and obfuscate native Java/Kotlin code.


Q3: Explain CodePush / OTA updates. What are the limitations?

Answer:

OTA (Over-The-Air) updates deliver new JavaScript bundles and assets to users without App Store / Play Store review.

CodePush (App Center) and EAS Update (Expo) are popular solutions.

Can update:

  • JavaScript logic and React components
  • Images and static assets bundled with JS
  • Bug fixes and feature toggles

Cannot update:

  • Native code changes (new native modules, SDK upgrades)
  • Permission changes in Info.plist / AndroidManifest
  • App icon or splash screen (native assets)

Apple's guidelines require OTA updates not to significantly change app purpose. Major feature changes should go through store review.

Always use deployment channels (Staging → Production) and set runtime version compatibility to prevent mismatched native/JS bundles.


Q4: How does Expo Updates (EAS Update) differ from CodePush?

Answer:

Aspect CodePush EAS Update
Ecosystem App Center / Microsoft Expo
Project type RN CLI and Expo Primarily Expo (config plugins for bare)
Configuration Manual native setup eas update:configure
Channels Deployments (Staging/Prod) Branches and channels
Runtime compat Manual target binary version runtimeVersion policy in app.json
Integration Separate from build pipeline Unified with EAS Build

EAS Update integrates with Expo's build system — same project ID, unified dashboard. CodePush works with any React Native project but requires App Center setup.

Both serve the same purpose: fast JS-only updates between store releases.


Q5: Describe your versioning strategy for iOS and Android.

Answer:

Use semantic versioning for user-facing version (1.4.2) synchronized across platforms via package.json and release automation.

Build numbers (iOS CFBundleVersion, Android versionCode):

  • Increment on every store upload
  • Never decrement or reuse (Play Store enforces this)
  • Can automate in CI with Fastlane increment_build_number

Git tagging: Tag releases as v1.4.2 for traceability.

OTA compatibility: Match CodePush target binary version or EAS runtimeVersion to the native build that JS update requires.

Branch strategy: release/1.4.x branches for hotfixes; merge back to main after release.


Q6: What is staged rollout and why is it important?

Answer:

Staged rollout releases a new version to a percentage of users before full deployment.

iOS: Phased release over 7 days (automatic 1% → 100%). Android: Manual percentage control (e.g., 5% → 20% → 100%).

Why it matters:

  • Catches crashes affecting only specific devices/OS versions
  • Limits blast radius of undiscovered bugs
  • Allows monitoring crash-free rate before full release
  • Can halt rollout if metrics degrade

Best practice: Combine with crash monitoring (Crashlytics/Sentry), key funnel metrics, and a documented rollback plan. For JS-only issues, OTA fix can reach 100% of users on the current binary without waiting for staged native rollout to complete.


Q7: What are provisioning profiles and why are they needed on iOS?

Answer:

A provisioning profile is Apple's mechanism to authorize an app to run on devices or be distributed through the App Store. It binds together:

  1. App ID — Bundle identifier and enabled capabilities
  2. Certificate — Proves developer identity (Development or Distribution)
  3. Devices — Registered UDIDs (development/ad hoc only; not needed for App Store)

Without a valid profile, iOS refuses to install or launch the app.

Types:

  • Development — Debug on registered devices
  • Ad Hoc — Internal distribution to registered devices
  • App Store — Distribution via App Store / TestFlight

Modern workflows use Xcode Automatic Signing or Fastlane Match (stores profiles in encrypted git repo for CI). Profiles expire annually — CI pipelines must handle renewal.


Q8: How do you automate React Native releases with Fastlane?

Answer:

Fastlane automates repetitive release tasks:

iOS lane:

  • match — Sync certificates and profiles
  • increment_build_number — Bump build
  • build_app — Archive and export IPA
  • upload_to_app_store — Upload to App Store Connect
  • deliver — Upload metadata and screenshots

Android lane:

  • gradle(task: "bundleRelease") — Build AAB
  • upload_to_play_store — Upload to Play Console track

CI integration (GitHub Actions example):

- name: iOS Release
  run: cd ios && fastlane release
  env:
    MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
    APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}

Combine with:

  • Automated version bumping from conventional commits
  • TestFlight / internal track on every main branch merge
  • Production release triggered by git tag

Q9: What should be on your pre-release checklist for a production React Native app?

Answer:

Code & config:

  • Remove debug code, dev URLs, and console.logs
  • Verify environment variables point to production
  • Confirm crash reporting and analytics use production keys
  • All CI tests pass including E2E on release build

iOS:

  • Valid distribution certificate and provisioning profile
  • Privacy manifest and App Tracking Transparency if applicable
  • Version/build incremented; release notes prepared
  • TestFlight validation completed

Android:

  • versionCode incremented; release signing configured
  • ProGuard tested — no reflection-related crashes
  • 64-bit libraries included
  • Internal track smoke test passed

Store listing:

  • Screenshots for required device sizes
  • Privacy policy URL current
  • Release notes written

Post-release:

  • Monitor crash-free rate for 24-48 hours
  • Staged rollout before 100%
  • OTA rollback plan documented

Navigation

Previous: Architecture Patterns

Next: Security