Codepush Mandatory Updates - React Native OTA Update Flow

Learn how to implement mandatory Codepush-style updates in React Native with Stallion. Handle non-dismissable update popups, download progress tracking, and automatic restarts for critical updates. Perfect for migrating from Codepush.

Codepush Mandatory Updates - React Native OTA Update Flow

Mandatory updates are critical releases that require immediate installation. Unlike optional updates, mandatory releases cannot be dismissed by users and must be applied before the app can continue functioning normally. This is especially important for security patches, critical bug fixes, or breaking changes that require immediate deployment.

Stallion provides a robust API to handle mandatory updates in React Native applications, giving you full control over the update experience similar to Codepush. This guide will show you how to implement a custom mandatory update flow with progress tracking and non-dismissable UI.

Important:

The code examples in this guide are dummy implementations provided for demonstration purposes. You must implement your own styling and design for the update modal based on your app's design language and brand guidelines. The examples focus on the functional implementation and API usage rather than production-ready UI components.

Prerequisite:

Before continuing, ensure that the Stallion SDK is installed and configured in your React Native app. If not, refer to our Installation Guide.

Detecting Mandatory Updates

The useStallionUpdate hook provides access to update metadata, including the isMandatory flag. This flag indicates whether a release requires immediate installation and cannot be skipped, similar to Codepush's mandatory update mechanism.

import { useStallionUpdate } from "react-native-stallion";

const UpdateHandler = () => {
  const { newReleaseBundle, isRestartRequired } = useStallionUpdate();

  // Check if the new release is mandatory
  if (newReleaseBundle?.isMandatory) {
    // Handle mandatory update flow
  }

  return null;
};

The newReleaseBundle object contains all metadata about the downloaded update, including:

  • isMandatory: Boolean indicating if the update is mandatory
  • releaseNote: Release notes for the update
  • version: Version number of the bundle
  • Other metadata fields

Creating a Non-Dismissable Update Popup

For mandatory updates, you should create a modal or popup that cannot be dismissed by users. This ensures that critical updates are applied before users can continue using the app, just like Codepush mandatory updates.

import React, { useState, useEffect } from "react";
import { Modal, View, Text, StyleSheet } from "react-native";
import { useStallionUpdate } from "react-native-stallion";

const MandatoryUpdateModal = () => {
  const { newReleaseBundle, isRestartRequired } = useStallionUpdate();
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    if (newReleaseBundle?.isMandatory) {
      setIsVisible(true);
    }
  }, [newReleaseBundle]);

  // Prevent dismissal for mandatory updates
  const handleBackdropPress = () => {
    // Do nothing - modal cannot be dismissed
  };

  return (
    <Modal
      visible={isVisible}
      transparent
      animationType="fade"
      onRequestClose={handleBackdropPress}
    >
      <View style={styles.overlay}>
        <View style={styles.modalContainer}>
          <Text style={styles.title}>Update Required</Text>
          <Text style={styles.message}>
            {newReleaseBundle?.releaseNote ||
              "A critical update is available. Please wait while we download and install it."}
          </Text>
          {/* Progress bar and restart button will be added here */}
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: "rgba(0, 0, 0, 0.7)",
    justifyContent: "center",
    alignItems: "center",
  },
  modalContainer: {
    backgroundColor: "#fff",
    borderRadius: 12,
    padding: 24,
    width: "80%",
    maxWidth: 400,
  },
  title: {
    fontSize: 20,
    fontWeight: "bold",
    marginBottom: 12,
  },
  message: {
    fontSize: 14,
    color: "#666",
    marginBottom: 16,
  },
});

Tracking Download Progress

Stallion emits DOWNLOAD_PROGRESS_PROD events during the download process. You can listen to these events using addEventListener to track download progress and update your UI accordingly.

The event payload contains a progress property that represents the download progress as a fraction between 0 and 1 (0 = 0%, 1 = 100%).

import React, { useState, useEffect } from "react";
import { Modal, View, Text, StyleSheet, TouchableOpacity } from "react-native";
import {
  useStallionUpdate,
  addEventListener,
  removeEventListener,
  restart,
} from "react-native-stallion";

const MandatoryUpdateModal = () => {
  const { newReleaseBundle, isRestartRequired } = useStallionUpdate();
  const [isVisible, setIsVisible] = useState(false);
  const [downloadProgress, setDownloadProgress] = useState(0);

  useEffect(() => {
    if (newReleaseBundle?.isMandatory) {
      setIsVisible(true);
    }
  }, [newReleaseBundle]);

  useEffect(() => {
    // Define the event listener function
    const eventListener = (event) => {
      if (event.type === "DOWNLOAD_PROGRESS_PROD") {
        // Progress is a fraction between 0 and 1
        const progress = event?.progress || 0;
        setDownloadProgress(progress);
      }
    };

    // Add the event listener
    addEventListener(eventListener);

    // Cleanup: remove the event listener
    return () => {
      removeEventListener(eventListener);
    };
  }, []);

  const handleRestart = () => {
    restart();
  };

  return (
    <Modal
      visible={isVisible}
      transparent
      animationType="fade"
      onRequestClose={() => {}} // Non-dismissable
    >
      <View style={styles.overlay}>
        <View style={styles.modalContainer}>
          <Text style={styles.title}>Update Required</Text>
          <Text style={styles.message}>
            {newReleaseBundle?.releaseNote ||
              "A critical update is available. Please wait while we download and install it."}
          </Text>

          {/* Progress Bar */}
          <View style={styles.progressContainer}>
            <View style={styles.progressBar}>
              <View
                style={[
                  styles.progressFill,
                  { width: `${downloadProgress * 100}%` },
                ]}
              />
            </View>
            <Text style={styles.progressText}>
              {Math.round(downloadProgress * 100)}%
            </Text>
          </View>

          {/* Restart Button - Only show when restart is required */}
          {isRestartRequired && (
            <TouchableOpacity
              style={styles.restartButton}
              onPress={handleRestart}
            >
              <Text style={styles.restartButtonText}>Restart App</Text>
            </TouchableOpacity>
          )}
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: "rgba(0, 0, 0, 0.7)",
    justifyContent: "center",
    alignItems: "center",
  },
  modalContainer: {
    backgroundColor: "#fff",
    borderRadius: 12,
    padding: 24,
    width: "80%",
    maxWidth: 400,
  },
  title: {
    fontSize: 20,
    fontWeight: "bold",
    marginBottom: 12,
  },
  message: {
    fontSize: 14,
    color: "#666",
    marginBottom: 16,
  },
  progressContainer: {
    marginVertical: 16,
  },
  progressBar: {
    height: 8,
    backgroundColor: "#e0e0e0",
    borderRadius: 4,
    overflow: "hidden",
    marginBottom: 8,
  },
  progressFill: {
    height: "100%",
    backgroundColor: "#007AFF",
    borderRadius: 4,
  },
  progressText: {
    fontSize: 12,
    color: "#666",
    textAlign: "center",
  },
  restartButton: {
    backgroundColor: "#007AFF",
    padding: 12,
    borderRadius: 8,
    marginTop: 16,
  },
  restartButtonText: {
    color: "#fff",
    textAlign: "center",
    fontSize: 16,
    fontWeight: "600",
  },
});

Complete Implementation Example

Here's a complete example that combines all the concepts:

import React, { useState, useEffect } from "react";
import { Modal, View, Text, StyleSheet, TouchableOpacity } from "react-native";
import {
  useStallionUpdate,
  addEventListener,
  removeEventListener,
  restart,
} from "react-native-stallion";

const MandatoryUpdateHandler = () => {
  const { newReleaseBundle, isRestartRequired } = useStallionUpdate();
  const [isVisible, setIsVisible] = useState(false);
  const [downloadProgress, setDownloadProgress] = useState(0);

  // Show modal when mandatory update is detected
  useEffect(() => {
    if (newReleaseBundle?.isMandatory) {
      setIsVisible(true);
    }
  }, [newReleaseBundle]);

  // Listen to download progress events
  useEffect(() => {
    // Define the event listener function
    const eventListener = (event) => {
      if (event.type === "DOWNLOAD_PROGRESS_PROD") {
        const progress = event?.progress || 0;
        setDownloadProgress(progress);
      }
    };

    // Add the event listener
    addEventListener(eventListener);

    // Cleanup: remove the event listener by passing the same function
    return () => {
      removeEventListener(eventListener);
    };
  }, []);

  const handleRestart = () => {
    restart();
  };

  if (!newReleaseBundle?.isMandatory) {
    return null;
  }

  return (
    <Modal
      visible={isVisible}
      transparent
      animationType="fade"
      onRequestClose={() => {}} // Non-dismissable for mandatory updates
    >
      <View style={styles.overlay}>
        <View style={styles.modalContainer}>
          <Text style={styles.title}>Update Required</Text>

          <Text style={styles.message}>
            {newReleaseBundle.releaseNote ||
              "A critical update is available and must be installed to continue using the app."}
          </Text>

          {/* Download Progress Section */}
          <View style={styles.progressSection}>
            <View style={styles.progressBar}>
              <View
                style={[
                  styles.progressFill,
                  { width: `${downloadProgress * 100}%` },
                ]}
              />
            </View>
            <Text style={styles.progressText}>
              Downloading... {Math.round(downloadProgress * 100)}%
            </Text>
          </View>

          {/* Restart Button - Only visible when download is complete */}
          {isRestartRequired && (
            <TouchableOpacity
              style={styles.restartButton}
              onPress={handleRestart}
            >
              <Text style={styles.restartButtonText}>Restart App</Text>
            </TouchableOpacity>
          )}
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: "rgba(0, 0, 0, 0.75)",
    justifyContent: "center",
    alignItems: "center",
  },
  modalContainer: {
    backgroundColor: "#fff",
    borderRadius: 16,
    padding: 24,
    width: "85%",
    maxWidth: 400,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 8,
  },
  title: {
    fontSize: 22,
    fontWeight: "bold",
    marginBottom: 12,
    color: "#000",
  },
  message: {
    fontSize: 14,
    color: "#666",
    marginBottom: 20,
    lineHeight: 20,
  },
  progressSection: {
    marginVertical: 16,
  },
  progressBar: {
    height: 8,
    backgroundColor: "#e0e0e0",
    borderRadius: 4,
    overflow: "hidden",
    marginBottom: 8,
  },
  progressFill: {
    height: "100%",
    backgroundColor: "#007AFF",
    borderRadius: 4,
    transition: "width 0.3s ease",
  },
  progressText: {
    fontSize: 12,
    color: "#666",
    textAlign: "center",
  },
  restartButton: {
    backgroundColor: "#007AFF",
    paddingVertical: 14,
    paddingHorizontal: 24,
    borderRadius: 8,
    marginTop: 8,
  },
  restartButtonText: {
    color: "#fff",
    fontSize: 16,
    fontWeight: "600",
    textAlign: "center",
  },
});

export default MandatoryUpdateHandler;

Key Implementation Points

  1. Non-Dismissable Modal: Set onRequestClose to an empty function to prevent users from dismissing mandatory update modals, similar to Codepush mandatory updates.

  2. Progress Tracking: Use addEventListener to listen for DOWNLOAD_PROGRESS_PROD events and update your UI with the progress value (0 to 1).

  3. Event Listener Cleanup: Always use removeEventListener and pass the same listener function that was used with addEventListener to properly unsubscribe from events.

  4. Conditional Restart Button: Only show the restart button when isRestartRequired is true, indicating that the download is complete and the app is ready to restart.

  5. Restart Method: Import and call the restart function from react-native-stallion to trigger the app restart.

Best Practices for Mandatory Updates

  • Use Sparingly: Reserve mandatory updates for critical security patches or breaking changes that require immediate deployment.

  • Clear Communication: Provide clear release notes explaining why the update is mandatory and what changes users can expect.

  • Progress Feedback: Always show download progress to keep users informed during the update process.

  • Error Handling: Consider adding error handling for failed downloads or network issues.

  • Testing: Thoroughly test mandatory update flows in staging environments before deploying to production.

Tip:

For more information on handling optional updates and custom UI flows, check out our guide on Custom Update UX with Stallion.

Migration from Codepush:

If you're migrating from Codepush, Stallion's mandatory update flow provides similar functionality with enhanced progress tracking and better React Native integration. The isMandatory flag works similarly to Codepush's mandatory update mechanism, making the migration process straightforward.