@ddedic/expo-fancy-ota-updates
A highly customizable OTA (Over-The-Air) update UI package for Expo apps. Includes animated banners, info screens, and full theming support.

📚 Full Documentation | 📦 npm Package | 💻 GitHub
🏆 Proudly Used In
See exactly what's eating your storage — and fix it in seconds
Photo Trim gives you complete visibility into your photo library clutter. Discover large files, old media, and forgotten videos buried in both your device and iCloud. Smart grouping and powerful filters make it effortless to identify and delete unnecessary photos and videos — reclaiming gigabytes of storage with just a few taps.
📊 Storage Insights See what's cluttering device & iCloud | 🔍 Sort by Size Find large photos & videos instantly | ☁️ iCloud vs Device Clear visibility of where files live | 📅 Filter by Date Target old, forgotten media | 🗑️ Batch Delete Clean up hundreds in one tap | 🔒 Privacy First All analysis stays on your device |
🌐 Visit photo-trim.com
·
📱 Download on App Store
|
✨ Features
UI Components
- 🎨 Fully Customizable — Theme colors, gradients, border radius, and animations
- 🌍 i18n Ready — Pass your own translations or use English defaults
- ✨ Animated Banner — Beautiful gradient banner with pulse animation
- 🧱 Gradient Fallback — Uses solid background automatically when
expo-linear-gradient is not installed
- 📱 Info Screen — Full debug/info screen with changelog display
- 🔌 Drop-in Ready — Works out of the box with sensible defaults
- 🎯 Render Props — Override any component with your own implementation
- 🎨 Icon Support — Uses lucide-react-native with text fallbacks
- 📡 Channel Surfing — Switch update channels at runtime (Expo SDK 54+)
CLI Publishing Tool
- 🚀 Easy Publishing — Simple
ota-publish command to publish OTA updates
- ⚙️ Configurable — Customize via
ota-updates.config.js
- 📊 Multiple Version Strategies — Semver, build number, or date-based
- 📝 Smart Changelog — Auto-generate from git, manual input, file, or custom hook
- 🧩 Per-Channel Templates — Channel-specific version/message formats
- ✂️ Short Channel Aliases — Use
{channelAlias} for compact versions like 1.1.6-p42
- 🎯 Interactive Mode — Guided prompts for easy publishing
- 🔍 Dry Run — Preview changes before publishing
- 🪝 Hooks System — Run custom logic and override changelog/message/version
- 📦 Multi-Channel — Support for dev, preview, production channels
- ↩️ Revert Command — Safely republish a previous update to roll back a channel
- ⇄ Promote Command — Copy an update group from one channel to another
📸 Screenshots
📦 Installation
pnpm add @ddedic/expo-fancy-ota-updates
npm install @ddedic/expo-fancy-ota-updates
yarn add @ddedic/expo-fancy-ota-updates
Peer Dependencies
The following dependencies are required:
pnpm add expo expo-updates expo-device react react-native react-native-reanimated react-native-safe-area-context
Optional dependencies for enhanced visuals:
pnpm add expo-linear-gradient
pnpm add lucide-react-native react-native-svg
🚀 Quick Start
1. Create Version File
Create an ota-version.json file in your project root (this gets updated by your OTA publishing workflow):
{
"version": "1.0.0",
"buildNumber": 1,
"releaseDate": "2026-01-01T00:00:00.000Z",
"changelog": ["Initial release"]
}
2. Wrap Your App
import {
OTAUpdatesProvider,
UpdateBanner
} from '@ddedic/expo-fancy-ota-updates';
import versionData from './ota-version.json';
export default function App() {
return (
<OTAUpdatesProvider config={{ versionData }}>
{/* Banner auto-shows when update is available */}
<UpdateBanner />
{/* Your app content */}
<YourApp />
</OTAUpdatesProvider>
);
}
That's it! The banner will automatically appear when an OTA update is detected.
📚 Documentation
📖 View Full Documentation →
Comprehensive documentation with guides, examples, and API reference:
🧪 Expo Showcase Demo
Want to feel the package before integrating it?
Run the full demo app from this repo:
pnpm install
pnpm --dir examples/demo start
It demonstrates:
- dynamic provider controls (
minCheckIntervalMs, recordSkippedChecks, foreground checks)
- runtime channel switching ("channel surfing") with interactive UI
- default and custom banner renderers
OTAInfoScreen in modal mode
OTAInfoScreen in tab mode with native-header coordination
- iOS native-tabs compatibility fallback strategy
- live telemetry via
useOTAUpdates()
Live Demo
Update banner, status, and lifecycle controls
|
Info Screen
Version details, changelog, and update actions
|
Settings
Customization, mode toggle, and channel surfing
|
📖 Demo Docs: Expo Showcase Demo
📡 Channel Surfing
Switch the update channel your app pulls from at runtime — perfect for letting QA/stakeholders preview updates from staging or preview channels without rebuilding.
Requires Expo SDK 54+ (Updates.setUpdateRequestHeadersOverride()).
import { useOTAUpdates } from '@ddedic/expo-fancy-ota-updates';
function ChannelPicker() {
const { channel, isSwitchingChannel, switchChannel } = useOTAUpdates();
const handleSwitch = async (name: string) => {
const result = await switchChannel(name);
if (result.isSkipped) {
console.log('Skipped:', result.reason);
} else if (result.success) {
console.log(`Switched from ${result.previousChannel} to ${result.newChannel}`);
}
};
return (
<View>
<Text>Current channel: {channel ?? 'N/A'}</Text>
{isSwitchingChannel && <ActivityIndicator />}
<Button title="Production" onPress={() => handleSwitch('production')} />
<Button title="Staging" onPress={() => handleSwitch('staging')} />
<Button title="Preview" onPress={() => handleSwitch('preview')} />
</View>
);
}
The method follows the same guard pattern as checkForUpdate — it skips gracefully in DEV mode, simulators, and when updates are disabled, returning a SwitchChannelResult with the skip reason.
🚀 CLI Publishing Tool
This package includes a powerful CLI for publishing OTA updates with version tracking.
Quick Start
In your project (recommended):
npx ota-publish --channel production
Global usage:
npx -p @ddedic/expo-fancy-ota-updates ota-publish --channel production
pnpm add -g @ddedic/expo-fancy-ota-updates
ota-publish --channel production
Using in Your App
After publishing to npm:
npx ota-publish --channel development
For local development (before npm publish):
Add to your package.json:
{
"scripts": {
"ota:dev": "node node_modules/@ddedic/expo-fancy-ota-updates/bin/ota-publish.js --channel development",
"ota:preview": "node node_modules/@ddedic/expo-fancy-ota-updates/bin/ota-publish.js --channel preview",
"ota:prod": "node node_modules/@ddedic/expo-fancy-ota-updates/bin/ota-publish.js --channel production"
}
}
Then run: npm run ota:dev, npm run ota:preview, or npm run ota:prod
Release Management Commands
npx ota-publish revert --channel production
npx ota-publish promote --from preview --to production
npx ota-publish revert --channel production --dry-run
npx ota-publish promote --from preview --to production --dry-run
Shorter Production Version Example
If 1.1.6-production.1 is too long, use channel aliasing:
export default {
channelAliases: { production: 'p' },
versionFormatByChannel: {
production: '{major}.{minor}.{patch}-p{build}',
},
};
Output: 1.1.6-p1
📖 Full CLI Documentation →
📖 API Reference
<OTAUpdatesProvider>
The main provider that enables OTA update functionality throughout your app.
Props
children | ReactNode | Your app content |
theme | Partial<OTATheme> | Custom theme (merged with defaults) |
translations | Partial<OTATranslations> | Custom translations (merged with defaults) |
config | OTAConfig | Provider behavior configuration |
Config Options
interface OTAConfig {
checkOnMount?: boolean;
checkOnForeground?: boolean;
minCheckIntervalMs?: number;
recordSkippedChecks?: boolean;
autoDownload?: boolean;
autoReload?: boolean;
versionData?: OTAVersionData;
debug?: boolean;
}
Full Example
<OTAUpdatesProvider
theme={{
colors: {
primary: '#6366F1',
primaryLight: '#818CF8',
background: '#0B0B0F',
text: '#FFFFFF',
},
bannerGradient: ['#6366F1', '#818CF8'],
borderRadius: 16,
}}
translations={{
banner: {
updateAvailable: 'New Update Available!',
updateButton: 'Update Now',
},
}}
config={{
checkOnMount: true,
checkOnForeground: true,
minCheckIntervalMs: 30000,
recordSkippedChecks: true,
autoDownload: false,
versionData: require('./ota-version.json'),
}}
>
{children}
</OTAUpdatesProvider>
useOTAUpdates() Hook
Access OTA update state and actions from any component within the provider.
import { useOTAUpdates } from '@ddedic/expo-fancy-ota-updates';
function MyComponent() {
const {
isUpdateAvailable,
isDownloading,
isDownloaded,
status,
checkError,
downloadError,
lastCheck,
lastSkippedReason,
currentUpdateId,
channel,
runtimeVersion,
isEmbeddedUpdate,
isSwitchingChannel,
switchChannel,
otaVersion,
otaBuildNumber,
otaReleaseDate,
otaChangelog,
checkForUpdate,
downloadUpdate,
reloadApp,
simulateUpdate,
theme,
translations,
} = useOTAUpdates();
}
<UpdateBanner>
Animated banner that appears when updates are available.
Props
style | object | Custom container style |
visible | boolean | Controlled visibility mode |
onDismiss | () => void | Called when banner is dismissed |
renderBanner | (props) => ReactNode | Custom render function |
Basic Usage
<UpdateBanner />
Controlled Mode
const [showBanner, setShowBanner] = useState(false);
<UpdateBanner
visible={showBanner}
onDismiss={() => setShowBanner(false)}
/>
Custom Render
<UpdateBanner
renderBanner={({
isDownloaded,
isDownloading,
otaVersion,
onUpdate,
onRestart,
onDismiss,
theme
}) => (
<View style={{ backgroundColor: theme.colors.primary }}>
<Text>{isDownloaded ? 'Ready!' : `v${otaVersion} available`}</Text>
<Button
title={isDownloaded ? 'Restart' : 'Update'}
onPress={isDownloaded ? onRestart : onUpdate}
/>
<Button title="Later" onPress={onDismiss} />
</View>
)}
/>
<OTAInfoScreen>
Full debug/info screen for OTA updates. Great for settings or debug menus.
Props
mode | 'developer' | 'user' | Screen mode (default: 'developer') |
onBack | () => void | Back navigation callback |
renderHeader | (props) => ReactNode | Custom header render |
renderInfo | (props) => ReactNode | Custom render for Info section |
renderActions | (props) => ReactNode | Custom render for Actions section |
renderChangelog | (props) => ReactNode | Custom render for Changelog section |
showRuntimeVersion | boolean | Toggle runtime version visibility |
showOtaVersion | boolean | Toggle OTA version visibility |
showReleaseDate | boolean | Toggle release date visibility |
showUpdateId | boolean | Toggle update ID visibility |
showCheckButton | boolean | Toggle "Check for Updates" button |
showDownloadButton | boolean | Toggle "Download" button |
showReloadButton | boolean | Toggle "Reload" button |
showDebugSection | boolean | Toggle debug actions section |
style | object | Custom container style |
Usage
import { OTAInfoScreen } from '@ddedic/expo-fancy-ota-updates';
<OTAInfoScreen
mode="user"
onBack={() => navigation.goBack()}
/>
<OTAInfoScreen
renderInfo={({ theme }) => (
<View style={{ padding: 20 }}>
<Text style={{ color: theme.colors.text }}>My Custom Header</Text>
</View>
)}
/>
Sub-Components
You can also import sub-components directly if you want to compose your own screen:
import { OTAUpdateInfo, OTAUpdateActions, OTAUpdateChangelog } from '@ddedic/expo-fancy-ota-updates';
🎨 Theming
Theme Structure
interface OTATheme {
colors: {
primary: string;
primaryLight: string;
background: string;
backgroundSecondary: string;
backgroundTertiary: string;
text: string;
textSecondary: string;
textTertiary: string;
border: string;
error: string;
success: string;
warning: string;
};
bannerGradient?: [string, string];
borderRadius?: number;
buttonBorderRadius?: number;
animation?: {
duration?: number;
pulseDuration?: number;
};
}
Using Built-in Themes
import {
OTAUpdatesProvider,
defaultTheme,
lightTheme,
} from '@ddedic/expo-fancy-ota-updates';
<OTAUpdatesProvider theme={lightTheme}>
// Or customize from defaults
<OTAUpdatesProvider theme={{
...defaultTheme,
colors: {
...defaultTheme.colors,
primary: '#10B981',
},
}}>
🌍 Translations
Translation Structure
interface OTATranslations {
banner: {
updateAvailable: string;
updateReady: string;
downloading: string;
versionAvailable: string;
restartToApply: string;
updateButton: string;
restartButton: string;
};
infoScreen: {
title: string;
statusTitle: string;
embeddedBuild: string;
otaUpdate: string;
runtimeVersion: string;
otaVersion: string;
releaseDate: string;
updateId: string;
channel: string;
whatsNew: string;
checkForUpdates: string;
downloadUpdate: string;
reloadApp: string;
debugTitle: string;
simulateUpdate: string;
devMode: string;
notAvailable: string;
none: string;
};
}
German Example
<OTAUpdatesProvider
translations={{
banner: {
updateAvailable: 'Neue Version verfügbar',
updateReady: 'Update bereit',
downloading: 'Download läuft...',
versionAvailable: 'Eine neue Version ist verfügbar',
restartToApply: 'Neustart zum Anwenden',
updateButton: 'Aktualisieren',
restartButton: 'Neustart',
},
infoScreen: {
title: 'OTA Updates',
checkForUpdates: 'Nach Updates suchen',
},
}}
>
🔧 Advanced Usage
With App Theme Integration
import { useTheme } from './your-theme-context';
function AppProviders({ children }) {
const { colors, isDark } = useTheme();
return (
<OTAUpdatesProvider
theme={{
colors: {
primary: colors.primary,
background: colors.background,
text: colors.text,
// Map your theme colors
},
}}
>
{children}
</OTAUpdatesProvider>
);
}
With i18n Library
import { useTranslation } from 'react-i18next';
function AppProviders({ children }) {
const { t } = useTranslation('ota');
return (
<OTAUpdatesProvider
translations={{
banner: {
updateAvailable: t('banner.updateAvailable'),
updateButton: t('banner.updateButton'),
// ...
},
}}
>
{children}
</OTAUpdatesProvider>
);
}
📄 License
MIT License © 2025-2026 Danijel Dedic, Technabit e.U.
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.