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 mandatoryreleaseNote: Release notes for the updateversion: 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
-
Non-Dismissable Modal: Set
onRequestCloseto an empty function to prevent users from dismissing mandatory update modals, similar to Codepush mandatory updates. -
Progress Tracking: Use
addEventListenerto listen forDOWNLOAD_PROGRESS_PRODevents and update your UI with theprogressvalue (0 to 1). -
Event Listener Cleanup: Always use
removeEventListenerand pass the same listener function that was used withaddEventListenerto properly unsubscribe from events. -
Conditional Restart Button: Only show the restart button when
isRestartRequiredistrue, indicating that the download is complete and the app is ready to restart. -
Restart Method: Import and call the
restartfunction fromreact-native-stallionto 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.