# OTA Updates with CodePush ## CodePush Setup ### Installation ```bash # Install CodePush CLI npm install -g code-push-cli # Install React Native CodePush yarn add react-native-code-push ``` ### App Center Configuration ```bash # Login to App Center code-push login # Create apps code-push app add MyApp-iOS ios react-native code-push app add MyApp-Android android react-native # Get deployment keys code-push deployment ls MyApp-iOS -k code-push deployment ls MyApp-Android -k ``` ### Environment Configuration **.env files:** ```bash # .env.staging CODEPUSH_DEPLOYMENT_KEY_IOS=staging-ios-key CODEPUSH_DEPLOYMENT_KEY_ANDROID=staging-android-key # .env.production CODEPUSH_DEPLOYMENT_KEY_IOS=production-ios-key CODEPUSH_DEPLOYMENT_KEY_ANDROID=production-android-key ``` ## CodePush Integration ### App Configuration **App.tsx:** ```typescript import React, { useEffect, useState } from "react"; import { Alert, Platform } from "react-native"; import codePush from "react-native-code-push"; import Config from "react-native-config"; import { AppNavigator } from "./src/navigation/AppNavigator"; import { LoadingScreen } from "./src/components/LoadingScreen"; const codePushOptions = { checkFrequency: codePush.CheckFrequency.ON_APP_RESUME, installMode: codePush.InstallMode.ON_NEXT_RESUME, mandatoryInstallMode: codePush.InstallMode.IMMEDIATE, deploymentKey: Platform.select({ ios: Config.CODEPUSH_DEPLOYMENT_KEY_IOS, android: Config.CODEPUSH_DEPLOYMENT_KEY_ANDROID, }), updateDialog: { appendReleaseDescription: true, descriptionPrefix: "\n\nWhat's New:\n", mandatoryContinueButtonLabel: "Install Now", mandatoryUpdateMessage: "A critical update is available and must be installed.", optionalIgnoreButtonLabel: "Later", optionalInstallButtonLabel: "Install", optionalUpdateMessage: "An update is available. Would you like to install it?", title: "Update Available", }, }; const App: React.FC = () => { const [syncInProgress, setSyncInProgress] = useState(false); const [syncMessage, setSyncMessage] = useState(""); useEffect(() => { codePush.notifyAppReady(); }, []); const codePushStatusDidChange = (syncStatus: codePush.SyncStatus) => { switch (syncStatus) { case codePush.SyncStatus.CHECKING_FOR_UPDATE: setSyncMessage("Checking for updates..."); break; case codePush.SyncStatus.DOWNLOADING_PACKAGE: setSyncMessage("Downloading update..."); setSyncInProgress(true); break; case codePush.SyncStatus.INSTALLING_UPDATE: setSyncMessage("Installing update..."); break; case codePush.SyncStatus.UP_TO_DATE: setSyncMessage("App is up to date"); setSyncInProgress(false); break; case codePush.SyncStatus.UPDATE_INSTALLED: setSyncMessage("Update installed successfully"); setSyncInProgress(false); break; default: setSyncInProgress(false); break; } }; const codePushDownloadDidProgress = (progress: codePush.DownloadProgress) => { const percentage = Math.round( (progress.receivedBytes / progress.totalBytes) * 100 ); setSyncMessage(`Downloading update... ${percentage}%`); }; if (syncInProgress) { return ; } return ; }; export default codePush(codePushOptions)(App); ``` ### Manual Update Check ```typescript // src/utils/updateManager.ts import codePush from "react-native-code-push"; import { Alert } from "react-native"; export class UpdateManager { static async checkForUpdate(): Promise { try { const update = await codePush.checkForUpdate(); if (update) { Alert.alert( "Update Available", `Version ${update.appVersion} is available. Would you like to install it now?`, [ { text: "Later", style: "cancel" }, { text: "Install", onPress: () => this.downloadAndInstall() }, ] ); } else { Alert.alert("No Updates", "Your app is up to date!"); } } catch (error) { console.error("Error checking for updates:", error); } } static async downloadAndInstall(): Promise { try { await codePush.sync({ installMode: codePush.InstallMode.IMMEDIATE, updateDialog: { appendReleaseDescription: true, descriptionPrefix: "\n\nChanges:\n", }, }); } catch (error) { console.error("Error downloading update:", error); Alert.alert( "Update Failed", "Failed to download update. Please try again later." ); } } static async getUpdateMetadata(): Promise { try { return await codePush.getUpdateMetadata(); } catch (error) { console.error("Error getting update metadata:", error); return null; } } static async rollback(): Promise { try { await codePush.restartApp(); } catch (error) { console.error("Error rolling back:", error); } } } ``` ## Deployment Strategies ### Staged Rollout ```bash # Deploy to staging first code-push release-react MyApp-iOS ios --deploymentName Staging # Test staging deployment # If successful, promote to production with rollout percentage # Deploy to 10% of production users code-push release-react MyApp-iOS ios --deploymentName Production --rollout 10% # Monitor metrics and gradually increase rollout code-push patch MyApp-iOS Production --rollout 25% code-push patch MyApp-iOS Production --rollout 50% code-push patch MyApp-iOS Production --rollout 100% ``` ### Automated Deployment **scripts/deploy-codepush.js:** ```javascript #!/usr/bin/env node const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); const platform = process.argv[2]; // ios or android const environment = process.argv[3] || "staging"; // staging or production const rollout = process.argv[4] || "100"; // rollout percentage if (!platform || !["ios", "android"].includes(platform)) { console.error( "Usage: node deploy-codepush.js [staging|production] [rollout%]" ); process.exit(1); } const appName = platform === "ios" ? "MyApp-iOS" : "MyApp-Android"; const deploymentName = environment === "production" ? "Production" : "Staging"; // Get version from package.json const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8")); const version = packageJson.version; // Generate release notes from git commits const releaseNotes = execSync("git log --oneline -10", { encoding: "utf8" }) .split("\n") .filter((line) => line.trim()) .map((line) => `• ${line.split(" ").slice(1).join(" ")}`) .join("\n"); console.log(`Deploying to ${appName} ${deploymentName}...`); console.log(`Version: ${version}`); console.log(`Rollout: ${rollout}%`); try { const command = [ "code-push release-react", appName, platform, `--deploymentName ${deploymentName}`, `--description "${releaseNotes}"`, `--rollout ${rollout}%`, "--mandatory false", ].join(" "); execSync(command, { stdio: "inherit" }); console.log(`✅ Successfully deployed to ${appName} ${deploymentName}`); // Send notification (Slack, email, etc.) notifyDeployment(appName, deploymentName, version, rollout); } catch (error) { console.error("❌ Deployment failed:", error.message); process.exit(1); } function notifyDeployment(appName, deployment, version, rollout) { // Implementation for sending notifications console.log( `📱 ${appName} ${deployment} v${version} deployed to ${rollout}% of users` ); } ``` ### Rollback Strategy ```bash # Check deployment history code-push deployment history MyApp-iOS Production # Rollback to previous version code-push rollback MyApp-iOS Production # Rollback to specific label code-push rollback MyApp-iOS Production --targetRelease v1.2.3 ``` ## Update Policies ### Update Configuration ```typescript // src/config/updatePolicy.ts import codePush from "react-native-code-push"; export const UpdatePolicy = { // Check for updates when app resumes from background checkFrequency: codePush.CheckFrequency.ON_APP_RESUME, // Install updates on next app restart (non-disruptive) installMode: codePush.InstallMode.ON_NEXT_RESTART, // For mandatory updates, install immediately mandatoryInstallMode: codePush.InstallMode.IMMEDIATE, // Minimum background duration before checking (in seconds) minimumBackgroundDuration: 60, // Update dialog configuration updateDialog: { appendReleaseDescription: true, descriptionPrefix: "\n\nWhat's New:\n", mandatoryContinueButtonLabel: "Install Now", mandatoryUpdateMessage: "A critical update is required to continue using the app.", optionalIgnoreButtonLabel: "Not Now", optionalInstallButtonLabel: "Install", optionalUpdateMessage: "An update is available with new features and improvements.", title: "Update Available", }, }; ``` ### Conditional Updates ```typescript // src/utils/conditionalUpdates.ts import codePush from "react-native-code-push"; import { Platform } from "react-native"; import DeviceInfo from "react-native-device-info"; export class ConditionalUpdates { static async shouldCheckForUpdate(): Promise { // Don't check for updates in development if (__DEV__) return false; // Check device conditions const batteryLevel = await DeviceInfo.getBatteryLevel(); const isCharging = await DeviceInfo.isBatteryCharging(); const connectionType = await DeviceInfo.getConnectionType(); // Only update if: // - Battery > 20% OR device is charging // - Connected to WiFi (for large updates) const batteryOk = batteryLevel > 0.2 || isCharging; const connectionOk = connectionType === "wifi"; return batteryOk && connectionOk; } static async checkForUpdateWithConditions(): Promise { const shouldCheck = await this.shouldCheckForUpdate(); if (!shouldCheck) { console.log("Skipping update check due to device conditions"); return; } try { const update = await codePush.checkForUpdate(); if (update) { // Check update size const updateSizeMB = (update.packageSize || 0) / (1024 * 1024); if (updateSizeMB > 10) { // Large update - require WiFi and user confirmation this.promptForLargeUpdate(update, updateSizeMB); } else { // Small update - download automatically this.downloadUpdate(update); } } } catch (error) { console.error("Error checking for conditional update:", error); } } private static promptForLargeUpdate( update: codePush.RemotePackage, sizeMB: number ) { // Show custom dialog for large updates // Implementation depends on your UI framework } private static async downloadUpdate(update: codePush.RemotePackage) { try { await update.download(); await update.install(codePush.InstallMode.ON_NEXT_RESTART); } catch (error) { console.error("Error downloading update:", error); } } } ``` ## Monitoring & Analytics ### Update Analytics ```typescript // src/utils/updateAnalytics.ts import AnalyticsService from "@config/analytics.config"; import codePush from "react-native-code-push"; export class UpdateAnalytics { static trackUpdateCheck() { AnalyticsService.logEvent("codepush_check_for_update"); } static trackUpdateAvailable(update: codePush.RemotePackage) { AnalyticsService.logEvent("codepush_update_available", { app_version: update.appVersion, package_size: update.packageSize, is_mandatory: update.isMandatory, deployment_key: update.deploymentKey, }); } static trackUpdateDownloadStart(update: codePush.RemotePackage) { AnalyticsService.logEvent("codepush_download_start", { app_version: update.appVersion, package_size: update.packageSize, }); } static trackUpdateDownloadComplete(update: codePush.LocalPackage) { AnalyticsService.logEvent("codepush_download_complete", { app_version: update.appVersion, package_size: update.packageSize, }); } static trackUpdateInstalled(update: codePush.LocalPackage) { AnalyticsService.logEvent("codepush_update_installed", { app_version: update.appVersion, install_mode: update.installMode, }); } static trackUpdateFailed(error: Error, context?: string) { AnalyticsService.logEvent("codepush_update_failed", { error_message: error.message, context: context || "unknown", }); } static trackRollback(reason: string) { AnalyticsService.logEvent("codepush_rollback", { reason, }); } } ``` ### Health Monitoring ```typescript // src/utils/updateHealthMonitor.ts import codePush from "react-native-code-push"; import CrashlyticsService from "@config/firebase.config"; export class UpdateHealthMonitor { private static readonly CRASH_THRESHOLD = 3; private static crashCount = 0; static async monitorUpdateHealth() { try { const updateMetadata = await codePush.getUpdateMetadata(); if (updateMetadata && updateMetadata.isFirstRun) { // This is the first run after an update this.trackFirstRunAfterUpdate(updateMetadata); // Start monitoring for crashes this.startCrashMonitoring(); } } catch (error) { console.error("Error monitoring update health:", error); } } private static trackFirstRunAfterUpdate(update: codePush.LocalPackage) { CrashlyticsService.log( `First run after CodePush update: ${update.appVersion}` ); // Set custom attributes for crash reporting CrashlyticsService.setUserAttributes({ codepush_version: update.appVersion || "unknown", codepush_label: update.label || "unknown", is_codepush_update: "true", }); } private static startCrashMonitoring() { // Monitor app crashes for the next 24 hours const monitoringDuration = 24 * 60 * 60 * 1000; // 24 hours setTimeout(() => { this.evaluateUpdateStability(); }, monitoringDuration); } private static async evaluateUpdateStability() { if (this.crashCount >= this.CRASH_THRESHOLD) { console.warn( `Update appears unstable (${this.crashCount} crashes). Consider rollback.` ); // Optionally trigger automatic rollback // await this.performAutomaticRollback(); } else { console.log("Update appears stable"); CrashlyticsService.log("CodePush update validated as stable"); } } private static async performAutomaticRollback() { try { await codePush.restartApp(); CrashlyticsService.log("Automatic rollback performed due to instability"); } catch (error) { console.error("Failed to perform automatic rollback:", error); } } static reportCrash() { this.crashCount++; CrashlyticsService.log(`Crash reported. Count: ${this.crashCount}`); } } ``` ## Best Practices ### Update Guidelines 1. **Test Thoroughly** - Test updates on staging environment - Verify on multiple devices and OS versions - Test offline scenarios 2. **Gradual Rollout** - Start with small percentage (5-10%) - Monitor crash rates and user feedback - Gradually increase rollout 3. **Rollback Strategy** - Have rollback plan ready - Monitor key metrics after deployment - Set up automated alerts for issues 4. **Update Frequency** - Don't update too frequently (user fatigue) - Bundle related changes together - Consider user timezone for deployments 5. **Communication** - Provide clear release notes - Notify users of important changes - Use appropriate update messaging