Build Custom OTA Update UI in React Native with SwiftPatch
How to build custom OTA update UI in React Native — modals, banners, progress bars, and mandatory update screens. Complete guide with code examples using SwiftPatch hooks.
Why Custom Update UI Matters
Default OTA update behavior is silent — the update downloads in the background and applies on next restart. That works for minor fixes, but sometimes you need to control the user experience:
- Mandatory updates: Force users to update before using the app (security patches, breaking API changes)
- Feature announcements: Show users what's new with a custom modal
- Download progress: Show a progress bar for larger updates on slow connections
- Update banners: Non-intrusive notification that an update is available
SwiftPatch gives you full control over the update UI through its React hooks API. Here's how to build each pattern.
Basic Setup
First, make sure SwiftPatch is initialized in your app:
// App.tsx
import { SwiftPatch } from 'swiftpatch';
SwiftPatch.init({
deploymentKey: 'YOUR_KEY',
checkFrequency: 'ON_APP_RESUME',
installMode: 'ON_NEXT_RESTART',
autoRollback: true,
});
Pattern 1: Optional Update Modal
A polite modal that lets users choose when to update:
import React, { useState, useEffect } from 'react';
import { View, Text, Modal, TouchableOpacity, StyleSheet } from 'react-native';
import { SwiftPatch } from 'swiftpatch';
function UpdateModal() {
const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateInfo, setUpdateInfo] = useState<any>(null);
useEffect(() => {
checkForUpdates();
}, []);
async function checkForUpdates() {
const update = await SwiftPatch.checkForUpdate();
if (update && !update.isMandatory) {
setUpdateInfo(update);
setUpdateAvailable(true);
}
}
async function handleUpdate() {
setUpdateAvailable(false);
await SwiftPatch.downloadUpdate();
SwiftPatch.applyUpdate({ installMode: 'ON_NEXT_RESTART' });
}
return (
<Modal visible={updateAvailable} transparent animationType="fade">
<View style={styles.overlay}>
<View style={styles.modal}>
<Text style={styles.title}>Update Available</Text>
<Text style={styles.description}>
A new version is available with bug fixes and improvements.
</Text>
<View style={styles.buttons}>
<TouchableOpacity
style={styles.laterButton}
onPress={() => setUpdateAvailable(false)}
>
<Text style={styles.laterText}>Later</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.updateButton}
onPress={handleUpdate}
>
<Text style={styles.updateText}>Update Now</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
}
Pattern 2: Mandatory Update Screen
A full-screen blocker for critical updates — users can't dismiss this:
import React, { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { SwiftPatch } from 'swiftpatch';
function MandatoryUpdateScreen() {
const [isMandatory, setIsMandatory] = useState(false);
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState('Checking for updates...');
useEffect(() => {
handleMandatoryUpdate();
}, []);
async function handleMandatoryUpdate() {
const update = await SwiftPatch.checkForUpdate();
if (update && update.isMandatory) {
setIsMandatory(true);
setStatus('Downloading update...');
await SwiftPatch.downloadUpdate({
onProgress: (downloaded, total) => {
setProgress(downloaded / total);
setStatus(`Downloading... ${Math.round((downloaded / total) * 100)}%`);
},
});
setStatus('Installing update...');
SwiftPatch.applyUpdate({ installMode: 'IMMEDIATE' });
}
}
if (!isMandatory) return null;
return (
<View style={styles.fullScreen}>
<View style={styles.content}>
<Text style={styles.title}>Required Update</Text>
<Text style={styles.subtitle}>
Please wait while we install an important update.
</Text>
{/* Progress bar */}
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${progress * 100}%` }]} />
</View>
<Text style={styles.statusText}>{status}</Text>
<ActivityIndicator size="large" color="#007AFF" />
</View>
</View>
);
}
Pattern 3: Non-Intrusive Banner
A small banner at the top or bottom of the screen:
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, Animated, StyleSheet } from 'react-native';
import { SwiftPatch } from 'swiftpatch';
function UpdateBanner() {
const [visible, setVisible] = useState(false);
const slideAnim = useState(new Animated.Value(-60))[0];
useEffect(() => {
checkUpdate();
}, []);
async function checkUpdate() {
const update = await SwiftPatch.checkForUpdate();
if (update && !update.isMandatory) {
setVisible(true);
Animated.spring(slideAnim, {
toValue: 0,
useNativeDriver: true,
}).start();
}
}
async function handleUpdate() {
await SwiftPatch.downloadUpdate();
SwiftPatch.applyUpdate({ installMode: 'IMMEDIATE' });
}
function dismiss() {
Animated.timing(slideAnim, {
toValue: -60,
duration: 200,
useNativeDriver: true,
}).start(() => setVisible(false));
}
if (!visible) return null;
return (
<Animated.View
style={[
styles.banner,
{ transform: [{ translateY: slideAnim }] },
]}
>
<Text style={styles.bannerText}>New update available</Text>
<View style={styles.bannerActions}>
<TouchableOpacity onPress={handleUpdate}>
<Text style={styles.updateLink}>Update</Text>
</TouchableOpacity>
<TouchableOpacity onPress={dismiss}>
<Text style={styles.dismissLink}>Dismiss</Text>
</TouchableOpacity>
</View>
</Animated.View>
);
}
Pattern 4: Download Progress with Cancel
For larger updates, give users control:
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { SwiftPatch } from 'swiftpatch';
function DownloadProgress() {
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const [downloadSize, setDownloadSize] = useState('');
async function startDownload() {
setDownloading(true);
await SwiftPatch.downloadUpdate({
onProgress: (downloaded, total) => {
setProgress(downloaded / total);
setDownloadSize(`${(downloaded / 1024).toFixed(0)}KB / ${(total / 1024).toFixed(0)}KB`);
},
});
setDownloading(false);
// Apply on next restart
SwiftPatch.applyUpdate({ installMode: 'ON_NEXT_RESTART' });
}
return (
<View style={styles.container}>
{downloading ? (
<>
<View style={styles.progressTrack}>
<View
style={[styles.progressFill, { width: `${progress * 100}%` }]}
/>
</View>
<Text style={styles.sizeText}>{downloadSize}</Text>
</>
) : (
<TouchableOpacity style={styles.downloadButton} onPress={startDownload}>
<Text style={styles.buttonText}>Download Update</Text>
</TouchableOpacity>
)}
</View>
);
}
Pattern 5: "What's New" Changelog
Show users what changed in the update:
import React, { useState, useEffect } from 'react';
import { View, Text, Modal, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { SwiftPatch } from 'swiftpatch';
function WhatsNewModal() {
const [showModal, setShowModal] = useState(false);
const [releaseNotes, setReleaseNotes] = useState('');
useEffect(() => {
checkForUpdates();
}, []);
async function checkForUpdates() {
const update = await SwiftPatch.checkForUpdate();
if (update) {
setReleaseNotes(update.description || 'Bug fixes and improvements.');
setShowModal(true);
}
}
async function handleUpdate() {
setShowModal(false);
await SwiftPatch.downloadUpdate();
SwiftPatch.applyUpdate({ installMode: 'IMMEDIATE' });
}
return (
<Modal visible={showModal} transparent animationType="slide">
<View style={styles.overlay}>
<View style={styles.whatsNewModal}>
<Text style={styles.whatsNewTitle}>What's New</Text>
<ScrollView style={styles.notesContainer}>
<Text style={styles.releaseNotes}>{releaseNotes}</Text>
</ScrollView>
<TouchableOpacity style={styles.updateButton} onPress={handleUpdate}>
<Text style={styles.updateButtonText}>Update & Restart</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
Best Practices
1. Don't Block the User Unnecessarily
Only use mandatory updates for critical security patches or breaking API changes. For everything else, use optional modals or non-intrusive banners.
2. Show Progress for Updates Over 500KB
Even though SwiftPatch patches are typically tiny (~200KB), show a progress indicator for any download that might take more than a second on slow connections.
3. Test All UI States
- No update available (UI should be hidden)
- Optional update available
- Mandatory update available
- Download in progress
- Download failed (network error)
- Update applied, awaiting restart
4. Handle Network Errors Gracefully
try {
await SwiftPatch.downloadUpdate();
} catch (error) {
// Show retry button, don't crash the app
setError('Download failed. Check your connection.');
}
5. Respect User Choice
If a user dismisses an optional update, don't show it again immediately. Use a cooldown period:
const COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours
async function checkWithCooldown() {
const lastDismissed = await AsyncStorage.getItem('update_dismissed_at');
if (lastDismissed && Date.now() - parseInt(lastDismissed) < COOLDOWN_MS) {
return; // Don't show again yet
}
// ... check for updates
}
SwiftPatch vs Stallion: Custom UI Comparison
Both SwiftPatch and Stallion support custom update UIs, but the APIs differ:
| Feature | SwiftPatch | Stallion |
|---|---|---|
| Update check | SwiftPatch.checkForUpdate() | useStallion().checkUpdate() |
| Download with progress | downloadUpdate({ onProgress }) | Limited callback support |
| Mandatory flag | update.isMandatory | update.isMandatory |
| Apply modes | IMMEDIATE, ON_NEXT_RESTART, ON_NEXT_RESUME | IMMEDIATE, ON_RESTART |
| Release notes | update.description | Limited |
| Cancel download | Supported | Not documented |
SwiftPatch provides more granular control over the update flow, including download progress callbacks and multiple apply modes.
Conclusion
Custom OTA update UI is essential for providing a great user experience. With SwiftPatch's React hooks API, you can build:
- Optional update modals that respect user choice
- Mandatory update screens with progress bars
- Non-intrusive banners with smooth animations
- Download progress with cancel support
- "What's New" changelogs that engage users
The key is matching the UI to the update urgency — silent for minor fixes, visible for features, and blocking only for critical security patches.
Ready to ship updates faster?
Get started with SwiftPatch for free. No credit card required.
Join WaitlistRelated Articles
Expo EAS Update Pricing: Cost, Bandwidth & What It Really Costs at Scale
Expo EAS Update feels cheap when you're starting out. Then your app grows. Here's how Expo actually bills for EAS Update, why costs grow faster than you expect, and what happens when you're shipping frequent releases to real users.
ComparisonExpo EAS Update Alternative — Best Expo Updates Replacement with Patch Updates
Expo EAS Update works well inside the Expo ecosystem. But as apps scale, teams need patch updates, rollback, internal testing, bare React Native support, and on-premise hosting. Here's how SwiftPatch compares.
GuidePatch Updates: The Modern CodePush Alternative
Microsoft CodePush is deprecated. Learn how to migrate to SwiftPatch, the modern OTA update platform for React Native with 98% smaller patches, automatic rollback, and enterprise security.