npm package: @rn-flix/codepush
Description: Flix Codepush
Author: Flix
Latest version: 1.0.1

# @rn-flix/codepush

**A robust CodePush solution for React Native, enabling you to instantly deploy mobile app updates directly to your users' devices.**

This SDK empowers developers to manage and push over-the-air (OTA) updates for their React Native applications. It provides a client-side interface to a CodePush-compatible server, handling everything from checking for updates to downloading and installing them, with built-in support for automatic rollbacks on fatal errors.

---

## Features

-   **Cross-Platform:** Full support for both Android and iOS.
-   **Flexible Update Strategies:** Install updates immediately, on next app restart, or on next resume.
-   **Automatic Rollbacks:** If an update causes a fatal crash, the SDK will automatically roll back to the previous stable version on the next app launch.
-   **Download Progress:** Emit events to the JavaScript layer for tracking download progress, perfect for creating custom loading indicators.
-   **Simple API:** A clean and intuitive API for both manual and automated update management.
-   **Higher-Order Component:** Includes a `withCodePush` HOC for easily wrapping your root component and enabling automatic updates.
-   **New & Old Architecture:** Compatible with both React Native's old and new architectures.

---

## Installation

First, add the package to your project using `npm` or `yarn`:

```bash
npm install @rn-flix/codepush --save
```

or

```bash
yarn add @rn-flix/codepush
```

### iOS Setup

Navigate to your `ios` directory and install the pods.

```bash
cd ios && pod install
```

#### `AppDelegate.m`

You need to override the `sourceURLForBridge` method to allow the CodePush SDK to determine the location of your JavaScript bundle.

```swift
// AppDelegate.swift

import FlixCodepush // 1. Import the header

// ...

  override func bundleURL() -> URL? {
#if DEBUG
    RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
    FlixCodepush.bundleURL() // 2. Use the CodePush bundleURL method
#endif
}
```

### Android Setup

For Android, autolinking should handle most of the setup. However, you need to tell your application where to get the JS bundle from.

#### `MainApplication.java` (for older React Native versions)

```java
// MainApplication.java

import com.rnflix.codepush.FlixCodepush; // 1. Import the package

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
      new DefaultReactNativeHost(this) {
        // ...
      
        @Override
        protected String getJSBundleFile() {
          return FlixCodepush.getJSBundleFile(getApplicationContext());
        }
      };

  // ...
}
```

#### `MainApplication.kt` (for newer React Native versions)

```kotlin
// MainApplication.kt

import com.rnflix.codepush.FlixCodepush // 1. Import the package

class MainApplication : Application(), ReactApplication {

  override val reactNativeHost: ReactNativeHost =
      object : DefaultReactNativeHost(this) {

        // ...

        // 2. Add this override
        override fun getJSBundleFile(): String {
            return FlixCodepush.getJSBundleFile(applicationContext)
        }

        override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
        override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
      }

  // ...
}
```

---

## Complete Usage example

```javascript
import { useState } from 'react';
import {
  Alert,
  Button,
  Image,
  Platform,
  SafeAreaView,
  ScrollView,
  StyleSheet,
  Text,
  View
} from 'react-native';
import codePush, {
  type DownloadProgress,
  type LocalPackage,
  type RemotePackage,
} from '@rn-flix/codepush';

// --- Configuration ---
const SERVER_URL = "https://your-codepush-server.com"
const DEPLOYMENT_KEY = Platform.OS === 'ios' ? 'YOUR_IOS_DEPLOYMENT_KEY' : 'YOUR_ANDROID_DEPLOYMENT_KEY';
// ---------------------

export default function App() {
  const [status, setStatus] = useState<string>('App Loaded. SDK not configured.');
  const [downloadProgress, setDownloadProgress] = useState<DownloadProgress | null>(
    null
  );
  const [remotePackage, setRemotePackage] = useState<RemotePackage | null>(null);
  const [localPackage, setLocalPackage] = useState<LocalPackage | null>(null);
  const [isFirstRun, setIsFirstRun] = useState<boolean | null>(null);
  const [isRolledBack, setIsRolledBack] = useState<boolean | null>(null);
  const [installMode, setInstallMode] = useState(codePush.InstallMode.IMMEDIATE);

  // --- Manual Handler Functions ---

  const handleConfigure = async () => {
    try {
      setStatus('Configuring...');
      await codePush.configure({ serverUrl: SERVER_URL });
      setStatus('SDK Configured. Ready to notify or check status.');
      console.log('CodePush configured successfully');
    } catch (e: any) {
      setStatus(`Configure error: ${e.message}`);
      console.error('Failed to configure CodePush', e);
    }
  };

  const handleNotifyAppReady = async () => {
    try {
      setStatus('Notifying App Ready...');
      await codePush.notifyAppReady();
      setStatus('App Ready notification sent. Rollback watchdog disarmed.');
      console.log('Successfully notified app ready.');
    } catch (e: any) {
      setStatus(`Notify error: ${e.message}`);
      console.error('Failed to notify app ready', e);
    }
  };

  const handleCheckInitialStatus = async () => {
    try {
      setStatus('Checking initial status...');
      const rolledBack = await codePush.isRolledBack();
      if (rolledBack) {
        Alert.alert(
          'Rollback Detected',
          'The app was rolled back to a previous version.'
        );
      }
      setIsRolledBack(rolledBack);

      const firstRun = await codePush.isFirstRun();
      setIsFirstRun(firstRun);

      const metadata = await codePush.getUpdateMetadata();
      setLocalPackage(metadata);
      setStatus('Initial status checked and displayed below.');
    } catch (e: any) {
      setStatus(`Status check error: ${e.message}`);
      console.error('Failed during initial status check', e);
    }
  };

  const handleCheckForUpdate = async () => {
    setStatus('Checking for update...');
    setRemotePackage(null);
    try {
      const update = await codePush.checkForUpdate(DEPLOYMENT_KEY);
      if (update) {
        setStatus('Update available!');
        setRemotePackage(update);
      } else {
        setStatus('App is up to date.');
      }
    } catch (e: any) {
      setStatus(`Check error: ${e.message}`);
      console.error(e);
    }
  };

  const handleDownload = async () => {
    if (!remotePackage) {
      setStatus('No remote package to download. Check for update first.');
      return;
    }
    setStatus('Downloading...');
    setDownloadProgress(null);
    let progressListener: any = null;
    try {
      progressListener = codePush.codePushEventEmitter.addListener(
        'onDownloadProgress',
        (...args: any[]) => {
          const progressData = args[0] as DownloadProgress;
          console.log(
            `Downloading: ${progressData.receivedBytes} / ${progressData.totalBytes}`
          );
          setDownloadProgress(progressData);
        }
      );

      const downloadedPackage = await codePush.downloadUpdate(remotePackage);
      // Since download is complete, we can update the localPackage state
      setLocalPackage(downloadedPackage);
      setStatus('Download complete.');
    } catch (e: any) {
      setStatus(`Download error: ${e.message}`);
      console.error(e);
    } finally {
      progressListener?.remove();
    }
  };

  const handleInstall = async () => {
    if (!localPackage) {
      setStatus('No package downloaded. Download first.');
      return;
    }
    setStatus('Installing...');
    try {
      await codePush.installUpdate(localPackage, installMode, 0);
      setStatus('Install called. App will update based on selected mode.');
    } catch (e: any) {
      setStatus(`Install error: ${e.message}`);
      console.error(e);
    }
  };

  const handleRestart = () => {
    setStatus('Requesting restart...');
    codePush.restartApp();
  };

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView contentContainerStyle={styles.scrollContent}>
        <Text style={styles.title}>flix CodePush Manual Control</Text>
        <View style={styles.statusContainer}>
          <Text style={styles.statusTitle}>Current Status:</Text>
          <Text style={styles.statusText}>{status}</Text>
        </View>

        <View style={styles.buttonContainer}>
          <Text style={styles.stepHeader}>Step 1: Initial Setup</Text>
          <Button title="Configure SDK" onPress={handleConfigure} />
          <Button
            title="Notify App Ready (Disarm Rollback)"
            onPress={handleNotifyAppReady}
            color="#34A853"
          />
          <Button
            title="Check Initial Status (isFirstRun, etc.)"
            onPress={handleCheckInitialStatus}
          />
        </View>

        <View style={styles.buttonContainer}>
          <Text style={styles.stepHeader}>Step 2: Update Process</Text>
          <Button title="Check for Update" onPress={handleCheckForUpdate} />
          <Button
            title="Download Update"
            onPress={handleDownload}
            disabled={!remotePackage}
          />
          <View style={styles.installModeSelector}>
            <Text style={styles.infoTitle}>Select Install Mode:</Text>
            <View style={styles.buttonRow}>
              <Button
                title="IMMEDIATE"
                onPress={() => setInstallMode(codePush.InstallMode.IMMEDIATE)}
                color={
                  installMode === codePush.InstallMode.IMMEDIATE
                    ? '#4285F4'
                    : undefined
                }
              />
              <Button
                title="ON_RESUME"
                onPress={() =>
                  setInstallMode(codePush.InstallMode.ON_NEXT_RESUME)
                }
                color={
                  installMode === codePush.InstallMode.ON_NEXT_RESUME
                    ? '#4285F4'
                    : undefined
                }
              />
              <Button
                title="ON_RESTART"
                onPress={() =>
                  setInstallMode(codePush.InstallMode.ON_NEXT_RESTART)
                }
                color={
                  installMode === codePush.InstallMode.ON_NEXT_RESTART
                    ? '#4285F4'
                    : undefined
                }
              />
            </View>
          </View>
          <Button
            title="Install Update"
            onPress={handleInstall}
            disabled={!localPackage}
            color="#FBBC05"
          />
        </View>

        <View style={styles.buttonContainer}>
          <Text style={styles.stepHeader}>Step 3: App Control</Text>
          <Button title="Force Restart" onPress={handleRestart} color="#EA4335" />
        </View>

        <View style={styles.infoBox}>
          <Text style={styles.infoTitle}>Download Progress</Text>
          {downloadProgress ? (
            <Text>
              {`${Math.round(
                (downloadProgress.receivedBytes / downloadProgress.totalBytes) *
                  100
              )}% `}
              ({downloadProgress.receivedBytes} / {downloadProgress.totalBytes}{' '}
              bytes)
            </Text>
          ) : (
            <Text>N/A</Text>
          )}
        </View>

        <View style={styles.infoBox}>
          <Text style={styles.infoTitle}>Flags (from status check)</Text>
          <Text>
            Is First Run After Update:{' '}
            {isFirstRun === null ? 'N/A' : String(isFirstRun)}
          </Text>
          <Text>
            Has Rolled Back:{' '}
            {isRolledBack === null ? 'N/A' : String(isRolledBack)}
          </Text>
        </View>

        <View style={styles.infoBox}>
          <Text style={styles.infoTitle}>Available Remote Package</Text>
          <Text style={styles.infoJson}>
            {remotePackage ? JSON.stringify(remotePackage, null, 2) : 'N/A'}
          </Text>
        </View>

        <View style={styles.infoBox}>
          <Text style={styles.infoTitle}>Current/Downloaded Package</Text>
          <Text style={styles.infoJson}>
            {localPackage
              ? JSON.stringify(localPackage, null, 2)
              : 'No update installed or downloaded.'}
          </Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  scrollContent: { padding: 20 },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 20,
  },
  statusContainer: {
    backgroundColor: '#f0f0f0',
    padding: 15,
    borderRadius: 8,
    marginBottom: 20,
    alignItems: 'center',
  },
  statusTitle: { fontSize: 16, fontWeight: '600' },
  statusText: {
    fontSize: 16,
    color: '#333',
    marginTop: 5,
    textAlign: 'center',
  },
  stepHeader: {
    fontSize: 18,
    fontWeight: 'bold',
    marginTop: 10,
    marginBottom: 10,
    borderTopWidth: 1,
    borderTopColor: '#ccc',
    paddingTop: 10,
  },
  installModeSelector: { marginVertical: 10 },
  buttonRow: { flexDirection: 'row', justifyContent: 'space-around' },
  buttonContainer: { gap: 10, marginBottom: 20 },
  infoBox: {
    backgroundColor: '#e8f4f8',
    padding: 15,
    borderRadius: 8,
    marginBottom: 15,
  },
  infoTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 10 },
  infoJson: {
    fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
    fontSize: 12,
  },
});
```

---

## API Reference

### `codePush.configure(options)`

Configures the SDK. Must be called before any other method.

-   `options`: An object with:
    -   `serverUrl` (string, required): The URL of your CodePush server.
    -   `deploymentKey` (string, optional): The default deployment key to use for updates.

### `codePush.checkForUpdate(deploymentKey)`

Checks the server for an update. Returns a `RemotePackage` object if an update is available, otherwise `null`.

### `codePush.downloadUpdate(remotePackage)`

Downloads the update package. Returns a `LocalPackage` object.

### `codeush.installUpdate(localPackage, installMode, minimumBackgroundDuration)`

Installs a previously downloaded update.

### `codePush.getUpdateMetadata()`

Retrieves metadata for the currently installed update (e.g., `label`, `packageHash`). Returns `null` if the app is running the binary version.

### `codePush.notifyAppReady()`

Notifies the SDK that an installed update should be considered successful.

### `codePush.restartApp()`

Immediately restarts the app.

### `codePush.isFirstRun()`

Checks if this is the first run of the app after an update has been installed.

### `codePush.isRolledBack()`

Checks if the app has been rolled back to a previous version after a crash.

---

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.