@swiftpatch/react-native
v3.0.0
Published
React Native SDK for SwiftPatch OTA updates with differential patching, automatic rollback, and bundle signing
Maintainers
Readme
SwiftPatch React Native SDK
Over-the-Air (OTA) update SDK for React Native with differential patching, automatic rollback, and cryptographic verification. Optimized for the React Native New Architecture.
✨ Features
- 🚀 Differential Patching - Download only what changed (bsdiff/bspatch)
- 🔄 Automatic Rollback - Crash detection with instant recovery
- 🔒 Bundle Signing - RSA signature verification for security
- ⚡ New Architecture - TurboModules for 40% faster performance
- 🎯 Dual-Slot System - Safe production/staging environment switching
- 📦 Small Updates - Patches typically 10-50x smaller than full bundles
- 🛡️ Type-Safe - Full TypeScript support
- 🎨 React Hooks - Modern API with Provider pattern
- 📱 Cross-Platform - iOS and Android support
📦 Installation
npm install @swiftpatch/react-native
# or
yarn add @swiftpatch/react-nativeiOS Setup
Step 1: Install CocoaPods Dependencies
cd ios && pod install && cd ..Step 2: Update AppDelegate
In ios/AppDelegate.mm (or .swift), update the bundle URL method:
Objective-C:
// ...other imports
#import "SwiftPatchModule.h"
@implementation AppDelegate
// ...other implementations
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [SwiftPatchModule getBundleURL];
#endif
}Swift (RN 0.76+):
import react_native_swiftpatch
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
SwiftPatchModule.getBundleURL()
#endif
}Step 3: Add Credentials to Info.plist
Add your SwiftPatch credentials to ios/YourApp/Info.plist:
<plist version="1.0">
<dict>
<!-- ...other configs... -->
<key>SwiftPatchProjectId</key>
<string>YOUR_PROJECT_ID</string>
<!-- App token starts with spb_... and is 46 characters long -->
<key>SwiftPatchAppToken</key>
<string>spb_YOUR_APP_TOKEN_HERE</string>
<!-- Optional: Public key for bundle signing -->
<key>SwiftPatchPublicKey</key>
<string>YOUR_PUBLIC_KEY</string>
<!-- ...other configs... -->
</dict>
</plist>Android Setup
Step 1: Update MainApplication
In android/app/src/main/java/.../MainApplication.kt:
Kotlin (RN 0.76+):
// ...other imports
import com.swiftpatch.SwiftPatchModule
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
// ...other methods
override fun getJSBundleFile(): String? {
return SwiftPatchModule.getJSBundleFile(applicationContext)
}
}For React Native 0.82+:
import com.swiftpatch.SwiftPatchModule
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages,
jsBundleFilePath = SwiftPatchModule.getJSBundleFile(applicationContext)
)
}
}Step 2: Add Credentials to strings.xml
Add your credentials to android/app/src/main/res/values/strings.xml:
<resources>
<string name="app_name">YourApp</string>
<!-- App token starts with spb_... and is 46 characters long -->
<string name="SwiftPatchProjectId">YOUR_PROJECT_ID</string>
<string name="SwiftPatchAppToken">spb_YOUR_APP_TOKEN_HERE</string>
<!-- Optional: Public key for bundle signing -->
<string name="SwiftPatchPublicKey">YOUR_PUBLIC_KEY</string>
</resources>🚀 Quick Start
1. Wrap your app with SwiftPatchProvider
Using Native Config (Recommended):
If you've added credentials to Info.plist and strings.xml, you don't need to pass them in JS:
import { withSwiftPatch } from '@swiftpatch/react-native';
function App() {
return <YourApp />;
}
// Credentials are read from native config files
export default withSwiftPatch(App);Or with Provider pattern:
import { SwiftPatchProvider } from '@swiftpatch/react-native';
export default function App() {
return (
<SwiftPatchProvider config={{ debug: __DEV__ }}>
<YourApp />
</SwiftPatchProvider>
);
}Using JS Config (Alternative):
import { SwiftPatchProvider } from '@swiftpatch/react-native';
export default function App() {
return (
<SwiftPatchProvider
config={{
deploymentKey: 'YOUR_DEPLOYMENT_KEY',
serverUrl: 'https://your-server.com/api/v1', // optional
debug: __DEV__,
}}
>
<YourApp />
</SwiftPatchProvider>
);
}2. Use the hook in your components
import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native';
function UpdateButton() {
const {
status,
availableUpdate,
downloadProgress,
checkForUpdate,
downloadUpdate,
installUpdate,
restart,
} = useSwiftPatch();
const handleUpdate = async () => {
// Check for updates
const update = await checkForUpdate();
if (update) {
// Download the update
await downloadUpdate();
// Install and restart
await installUpdate();
restart();
}
};
if (status === UpdateStatus.DOWNLOADING) {
return <Text>Downloading: {downloadProgress?.percentage}%</Text>;
}
return (
<Button
title={availableUpdate ? 'Update Available' : 'Check for Updates'}
onPress={handleUpdate}
/>
);
}3. Or use the built-in modal
import { useSwiftPatchModal, SwiftPatchModal } from '@swiftpatch/react-native';
function App() {
const { showModal } = useSwiftPatchModal();
return (
<>
<YourApp />
<SwiftPatchModal />
<Button title="Check for Updates" onPress={showModal} />
</>
);
}📚 API Reference
SwiftPatchProvider
interface SwiftPatchConfig {
/**
* Deployment key from SwiftPatch dashboard.
* Optional if configured in native files (Info.plist / strings.xml)
*/
deploymentKey?: string;
serverUrl?: string;
checkOnResume?: boolean;
checkInterval?: number;
installMode?: InstallMode;
mandatoryInstallMode?: InstallMode;
debug?: boolean;
customHeaders?: Record<string, string>;
publicKey?: string;
autoStabilizeAfterLaunches?: number;
crashDetectionWindowMs?: number; // Default: 10000
maxCrashesBeforeRollback?: number; // Default: 2
autoStagingInDev?: boolean; // Default: false
}Native Config Keys
| Key | Platform | Description |
|-----|----------|-------------|
| SwiftPatchProjectId | iOS/Android | Your project ID |
| SwiftPatchAppToken | iOS/Android | App token (starts with spb_...) |
| SwiftPatchDeploymentKey | iOS/Android | Alternative to AppToken |
| SwiftPatchServerUrl | iOS/Android | Custom server URL (optional) |
| SwiftPatchPublicKey | iOS/Android | Public key for signing (optional) |
Verify Native Config
import { getNativeConfig } from '@swiftpatch/react-native';
const config = await getNativeConfig();
console.log('Project ID:', config.projectId);
console.log('App Token:', config.appToken);useSwiftPatch Hook
const {
// State
status, // Current update status
downloadProgress, // Download progress (0-100%)
currentBundle, // Currently installed bundle info
availableUpdate, // Available update info
isRestartRequired, // Whether restart is needed
error, // Last error
lastCheckedAt, // Last check timestamp
slotMetadata, // Dual-slot system metadata
environment, // Current environment (PROD/STAGE)
// Actions
checkForUpdate, // Check for available updates
downloadUpdate, // Download available update
installUpdate, // Install downloaded update
restart, // Restart app to apply update
rollback, // Rollback to previous version
clearPendingUpdate, // Clear pending update
getCurrentBundle, // Get current bundle info
stabilize, // Stabilize current bundle
switchEnvironment, // Switch PROD/STAGE environment
getSlotMetadata, // Get slot metadata
markMounted, // Mark app as mounted (crash detection)
downloadStageBundle, // Download staging bundle
} = useSwiftPatch();Imperative API (Non-React)
For use outside React components:
import { SwiftPatch } from '@swiftpatch/react-native';
const swiftPatch = new SwiftPatch({
deploymentKey: 'YOUR_KEY',
});
await swiftPatch.init();
const update = await swiftPatch.checkForUpdate();
if (update) {
await swiftPatch.downloadAndInstall(update);
swiftPatch.restart();
}🎯 Install Modes
enum InstallMode {
IMMEDIATE = 'immediate', // Install and restart immediately
ON_NEXT_RESTART = 'onNextRestart', // Install on next app restart
ON_NEXT_RESUME = 'onNextResume', // Install when app resumes
}🔧 Advanced Features
Differential Patching
SwiftPatch automatically uses differential patching when available:
// Server determines if patch is available
// SDK handles full bundle vs patch automatically
await downloadUpdate();
// Patch files are typically 10-50x smaller
// e.g., 50MB bundle → 2MB patchAutomatic Rollback
The crash detection window and crash threshold are configurable:
<SwiftPatchProvider
config={{
crashDetectionWindowMs: 15_000, // 15 second window (default: 10s)
maxCrashesBeforeRollback: 3, // Allow 3 crashes before rollback (default: 2)
}}
>
<App />
</SwiftPatchProvider>const { rollback } = useSwiftPatch();
// Manual rollback
await rollback();Bundle Signing (Optional)
<SwiftPatchProvider
config={{
deploymentKey: 'YOUR_KEY',
publicKey: 'YOUR_RSA_PUBLIC_KEY',
}}
>
<App />
</SwiftPatchProvider>
// SDK automatically verifies signaturesEnvironment Switching (PROD/STAGE)
const { switchEnvironment, environment } = useSwiftPatch();
// Switch to staging
await switchEnvironment(EnvironmentMode.STAGING);
// Download and test staging bundle
await downloadStageBundle(url, hash);
// Switch back to production
await switchEnvironment(EnvironmentMode.PRODUCTION);📋 Common Patterns
Silent Background Update
Download and install updates silently. The update applies on the next app restart — no UI code needed.
import { SwiftPatchProvider, InstallMode } from '@swiftpatch/react-native';
export default function App() {
return (
<SwiftPatchProvider
config={{
installMode: InstallMode.ON_NEXT_RESTART,
checkOnResume: true,
checkInterval: 300_000, // Check every 5 minutes
debug: __DEV__,
}}
>
<YourApp />
</SwiftPatchProvider>
);
}Mandatory Update with Blocking UI
Force users to update before using the app. Shows a full-screen blocking overlay during download.
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
import {
SwiftPatchProvider,
useSwiftPatch,
UpdateStatus,
InstallMode,
} from '@swiftpatch/react-native';
function MandatoryUpdateGate({ children }: { children: React.ReactNode }) {
const {
status,
availableUpdate,
downloadProgress,
checkForUpdate,
downloadUpdate,
installUpdate,
restart,
} = useSwiftPatch();
const [checking, setChecking] = useState(true);
useEffect(() => {
(async () => {
const update = await checkForUpdate();
if (update?.isMandatory) {
await downloadUpdate();
await installUpdate();
restart();
}
setChecking(false);
})();
}, []);
if (
checking ||
(availableUpdate?.isMandatory && status !== UpdateStatus.UP_TO_DATE)
) {
return (
<View style={styles.overlay}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.text}>
{status === UpdateStatus.DOWNLOADING
? `Updating... ${downloadProgress?.percentage ?? 0}%`
: 'Checking for updates...'}
</Text>
</View>
);
}
return <>{children}</>;
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
text: { marginTop: 16, fontSize: 16, color: '#333' },
});
export default function App() {
return (
<SwiftPatchProvider
config={{ mandatoryInstallMode: InstallMode.IMMEDIATE }}
>
<MandatoryUpdateGate>
<YourApp />
</MandatoryUpdateGate>
</SwiftPatchProvider>
);
}Custom Update Banner
Show a dismissible banner when an update is available. Let users choose when to update.
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native';
export function UpdateBanner() {
const {
status,
availableUpdate,
downloadProgress,
checkForUpdate,
downloadUpdate,
installUpdate,
restart,
} = useSwiftPatch();
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
checkForUpdate();
}, []);
if (dismissed || !availableUpdate || status === UpdateStatus.UP_TO_DATE) {
return null;
}
const handleUpdate = async () => {
await downloadUpdate();
await installUpdate();
restart();
};
return (
<View style={styles.banner}>
<Text style={styles.title}>
Update Available v{availableUpdate.version}
</Text>
{status === UpdateStatus.DOWNLOADING ? (
<Text style={styles.progress}>
Downloading... {downloadProgress?.percentage ?? 0}%
</Text>
) : (
<View style={styles.actions}>
<TouchableOpacity onPress={handleUpdate} style={styles.updateBtn}>
<Text style={styles.updateText}>Update Now</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setDismissed(true)}
style={styles.laterBtn}
>
<Text style={styles.laterText}>Later</Text>
</TouchableOpacity>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
banner: {
backgroundColor: '#007AFF',
padding: 12,
marginHorizontal: 16,
borderRadius: 8,
marginTop: 8,
alignItems: 'center',
},
title: { color: '#fff', fontWeight: '600', fontSize: 14 },
progress: { color: '#fff', fontSize: 12, marginTop: 4 },
actions: { flexDirection: 'row', marginTop: 8, gap: 12 },
updateBtn: {
backgroundColor: '#fff',
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 6,
},
updateText: { color: '#007AFF', fontWeight: '600' },
laterBtn: { paddingHorizontal: 16, paddingVertical: 6 },
laterText: { color: 'rgba(255,255,255,0.8)' },
});Error Handling with Descriptions
Use getErrorDescription() to show helpful error messages to developers:
import {
useSwiftPatch,
getErrorDescription,
} from '@swiftpatch/react-native';
function MyComponent() {
const { error } = useSwiftPatch();
if (error) {
const info = getErrorDescription(error.code);
console.warn(info.description);
console.warn('Fix:', info.troubleshooting);
}
return null;
}🆕 New Architecture Support
SwiftPatch v2.0+ fully supports the React Native New Architecture:
Enable New Architecture
iOS (ios/Podfile):
ENV['RCT_NEW_ARCH_ENABLED'] = '1'Android (android/gradle.properties):
newArchEnabled=truePerformance Benefits
| Operation | Legacy | TurboModule | Improvement | |-----------|--------|-------------|-------------| | Check Update | 15ms | 2ms | 87% faster | | Get Bundle Info | 8ms | 1ms | 87% faster | | Native Calls | 3-5ms | 0.5-1ms | 80% faster |
Detection
import { IS_TURBO_MODULE_ENABLED } from '@swiftpatch/react-native';
console.log('Using TurboModules:', IS_TURBO_MODULE_ENABLED);🧪 Testing
npm testAll core functionality is tested:
- Update checking
- Download & installation
- Rollback mechanisms
- Cryptographic verification
- Event handling
📱 Platform Requirements
| Platform | Minimum | Recommended | |----------|---------|-------------| | iOS | 13.4+ | 15.0+ | | Android | API 24+ (7.0) | API 31+ (12) | | React Native | 0.76.0+ | 0.76.5+ | | React | 18.2.0+ | 18.3.0+ |
🔒 Security
- Bundle Signing: Optional RSA signature verification
- HTTPS Only: All downloads over secure connections
- Hash Verification: SHA-256 hash checking for all bundles
- Integrity Checks: Automatic corruption detection
📊 Bundle Size
| File | Size | Compressed | |------|------|------------| | Core JS | ~45KB | ~12KB | | Native (iOS) | ~150KB | - | | Native (Android) | ~200KB | - |
🛠️ Development
# Clone the repository
git clone https://github.com/codewprincee/react-native-swiftpatch.git
# Install dependencies
npm install
# Run tests
npm test
# Build
npm run prepare
# Run example app
npm run example start🐛 Troubleshooting
iOS Build Errors
cd ios
rm -rf Pods Podfile.lock
pod install
cd ..Android Build Errors
cd android
./gradlew clean
cd ..Type Errors
npm run typescript📄 License
MIT License - see LICENSE for details
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide first.
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Docs: docs.swiftpatch.io
🎉 Acknowledgments
Built with:
- React Native
- bsdiff/bspatch for differential patching
- TypeScript
Made with ❤️ by the SwiftPatch Team
