# 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 SaayamApp-iOS ios react-native
code-push app add SaayamApp-Android android react-native
# Get deployment keys
code-push deployment ls SaayamApp-iOS -k
code-push deployment ls SaayamApp-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 SaayamApp-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 SaayamApp-iOS ios --deploymentName Production --rollout 10%
# Monitor metrics and gradually increase rollout
code-push patch SaayamApp-iOS Production --rollout 25%
code-push patch SaayamApp-iOS Production --rollout 50%
code-push patch SaayamApp-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" ? "SaayamApp-iOS" : "SaayamApp-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 SaayamApp-iOS Production
# Rollback to previous version
code-push rollback SaayamApp-iOS Production
# Rollback to specific label
code-push rollback SaayamApp-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