|
| 1 | +--- |
| 2 | +name: expo-ota-branding-i18n |
| 3 | +description: Use when customizing expo-fancy-ota-updates with brand themes, localized copy, custom UpdateBanner rendering, or custom OTAInfoScreen sections. |
| 4 | +--- |
| 5 | + |
| 6 | +# Expo OTA Branding and i18n |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Use this skill when the default OTA UI needs to match an app's product design, language, or information architecture. `@ddedic/expo-fancy-ota-updates` supports three levels of customization: |
| 11 | + |
| 12 | +1. **Theme tokens** through `OTAUpdatesProvider theme`. |
| 13 | +2. **Translations** through `OTAUpdatesProvider translations`. |
| 14 | +3. **Render props** for `UpdateBanner` and `OTAInfoScreen` sections. |
| 15 | + |
| 16 | +Start with theme and translations. Use render props only when the layout or component structure must change. |
| 17 | + |
| 18 | +## When to Use |
| 19 | + |
| 20 | +Use this skill when the user asks to: |
| 21 | + |
| 22 | +- Make the update banner match app branding. |
| 23 | +- Localize OTA UI text into German, Portuguese, Spanish, French, or another language. |
| 24 | +- Replace the default banner with custom UI. |
| 25 | +- Hide debug-heavy fields for user-facing settings. |
| 26 | +- Customize `OTAInfoScreen` sections with `renderInfo`, `renderActions`, or `renderChangelog`. |
| 27 | + |
| 28 | +Use `expo-ota-ui-integration` first if the provider/banner/info screen are not integrated at all. |
| 29 | + |
| 30 | +## Theme Customization |
| 31 | + |
| 32 | +Define brand tokens and pass them to the provider. Theme values are merged with defaults, but nested `colors` should include every color you rely on for contrast. |
| 33 | + |
| 34 | +```tsx |
| 35 | +import { OTAUpdatesProvider } from '@ddedic/expo-fancy-ota-updates'; |
| 36 | +import versionData from './ota-version.json'; |
| 37 | + |
| 38 | +const otaTheme = { |
| 39 | + colors: { |
| 40 | + primary: '#7C3AED', |
| 41 | + primaryLight: '#A78BFA', |
| 42 | + background: '#0B0B0F', |
| 43 | + backgroundSecondary: '#15151C', |
| 44 | + backgroundTertiary: '#222230', |
| 45 | + text: '#FFFFFF', |
| 46 | + textSecondary: '#D1D5DB', |
| 47 | + textTertiary: '#9CA3AF', |
| 48 | + border: '#2D2D3A', |
| 49 | + error: '#EF4444', |
| 50 | + success: '#10B981', |
| 51 | + warning: '#F59E0B', |
| 52 | + }, |
| 53 | + bannerGradient: ['#7C3AED', '#2563EB'] as [string, string], |
| 54 | + borderRadius: 18, |
| 55 | + buttonBorderRadius: 14, |
| 56 | + animation: { |
| 57 | + duration: 250, |
| 58 | + pulseDuration: 1800, |
| 59 | + }, |
| 60 | +}; |
| 61 | + |
| 62 | +export function AppShell({ children }) { |
| 63 | + return ( |
| 64 | + <OTAUpdatesProvider theme={otaTheme} config={{ versionData }}> |
| 65 | + {children} |
| 66 | + </OTAUpdatesProvider> |
| 67 | + ); |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +## Translations |
| 72 | + |
| 73 | +Pass only the strings you need to override, or pass a complete locale object for consistency. |
| 74 | + |
| 75 | +```tsx |
| 76 | +const portugueseTranslations = { |
| 77 | + banner: { |
| 78 | + updateAvailable: 'Nova atualização disponível', |
| 79 | + updateReady: 'Atualização pronta', |
| 80 | + downloading: 'Baixando atualização...', |
| 81 | + versionAvailable: 'Uma nova versão está disponível', |
| 82 | + restartToApply: 'Reinicie para aplicar as mudanças', |
| 83 | + updateButton: 'Atualizar', |
| 84 | + restartButton: 'Reiniciar', |
| 85 | + }, |
| 86 | + infoScreen: { |
| 87 | + title: 'Atualizações OTA', |
| 88 | + statusTitle: 'Status da atualização', |
| 89 | + embeddedBuild: 'Build embutido', |
| 90 | + otaUpdate: 'Atualização OTA', |
| 91 | + runtimeVersion: 'Versão runtime', |
| 92 | + otaVersion: 'Versão OTA', |
| 93 | + releaseDate: 'Data de lançamento', |
| 94 | + updateId: 'ID da atualização', |
| 95 | + channel: 'Canal', |
| 96 | + whatsNew: 'Novidades', |
| 97 | + checkForUpdates: 'Verificar atualizações', |
| 98 | + downloadUpdate: 'Baixar atualização', |
| 99 | + reloadApp: 'Reiniciar app', |
| 100 | + debugTitle: 'Depuração', |
| 101 | + simulateUpdate: 'Simular atualização', |
| 102 | + hideSimulation: 'Ocultar simulação', |
| 103 | + devMode: 'Modo desenvolvimento', |
| 104 | + notAvailable: 'Indisponível', |
| 105 | + none: 'Nenhum', |
| 106 | + }, |
| 107 | +}; |
| 108 | + |
| 109 | +<OTAUpdatesProvider translations={portugueseTranslations}> |
| 110 | + <YourApp /> |
| 111 | +</OTAUpdatesProvider> |
| 112 | +``` |
| 113 | + |
| 114 | +For dynamic locale selection: |
| 115 | + |
| 116 | +```tsx |
| 117 | +import * as Localization from 'expo-localization'; |
| 118 | + |
| 119 | +const translationsByLocale = { |
| 120 | + en: englishTranslations, |
| 121 | + pt: portugueseTranslations, |
| 122 | + de: germanTranslations, |
| 123 | +}; |
| 124 | + |
| 125 | +const locale = Localization.locale.split('-')[0]; |
| 126 | +const translations = translationsByLocale[locale] ?? translationsByLocale.en; |
| 127 | +``` |
| 128 | + |
| 129 | +## Custom UpdateBanner |
| 130 | + |
| 131 | +Use `renderBanner` when the app needs a fully branded banner but still wants the package's state and actions. |
| 132 | + |
| 133 | +```tsx |
| 134 | +import { Pressable, Text, View } from 'react-native'; |
| 135 | +import { UpdateBanner } from '@ddedic/expo-fancy-ota-updates'; |
| 136 | + |
| 137 | +<UpdateBanner |
| 138 | + renderBanner={({ |
| 139 | + isUpdateAvailable, |
| 140 | + isDownloading, |
| 141 | + isDownloaded, |
| 142 | + otaVersion, |
| 143 | + onUpdate, |
| 144 | + onRestart, |
| 145 | + onDismiss, |
| 146 | + theme, |
| 147 | + translations, |
| 148 | + }) => { |
| 149 | + if (!isUpdateAvailable && !isDownloaded) return null; |
| 150 | + |
| 151 | + return ( |
| 152 | + <View style={{ padding: 16, backgroundColor: theme.colors.primary, borderRadius: 16 }}> |
| 153 | + <Text style={{ color: 'white', fontWeight: '700' }}> |
| 154 | + {isDownloaded ? translations.updateReady : translations.updateAvailable} |
| 155 | + </Text> |
| 156 | + <Text style={{ color: 'white' }}>Version {otaVersion}</Text> |
| 157 | + <Pressable onPress={isDownloaded ? onRestart : onUpdate} disabled={isDownloading}> |
| 158 | + <Text style={{ color: 'white' }}> |
| 159 | + {isDownloaded ? translations.restartButton : translations.updateButton} |
| 160 | + </Text> |
| 161 | + </Pressable> |
| 162 | + <Pressable onPress={onDismiss}> |
| 163 | + <Text style={{ color: 'white' }}>Dismiss</Text> |
| 164 | + </Pressable> |
| 165 | + </View> |
| 166 | + ); |
| 167 | + }} |
| 168 | +/> |
| 169 | +``` |
| 170 | + |
| 171 | +## User-Facing OTAInfoScreen |
| 172 | + |
| 173 | +For production settings screens, reduce noisy diagnostics: |
| 174 | + |
| 175 | +```tsx |
| 176 | +<OTAInfoScreen |
| 177 | + mode="user" |
| 178 | + showRuntimeVersion={false} |
| 179 | + showUpdateId={false} |
| 180 | + showDebugSection={false} |
| 181 | + showCheckButton |
| 182 | + showChangelog |
| 183 | +/> |
| 184 | +``` |
| 185 | + |
| 186 | +For custom sections: |
| 187 | + |
| 188 | +```tsx |
| 189 | +<OTAInfoScreen |
| 190 | + renderChangelog={({ otaChangelog, theme }) => ( |
| 191 | + <View style={{ padding: 16 }}> |
| 192 | + <Text style={{ color: theme.colors.text, fontWeight: '700' }}>Latest changes</Text> |
| 193 | + {otaChangelog.map((item) => ( |
| 194 | + <Text key={item} style={{ color: theme.colors.textSecondary }}>• {item}</Text> |
| 195 | + ))} |
| 196 | + </View> |
| 197 | + )} |
| 198 | +/> |
| 199 | +``` |
| 200 | + |
| 201 | +## Accessibility and UX Rules |
| 202 | + |
| 203 | +- Keep update CTAs explicit: `Update`, `Download`, `Restart` should not be ambiguous. |
| 204 | +- Avoid auto-reload in user-facing flows unless the user has opted in. |
| 205 | +- Preserve high contrast for banner text over gradients. |
| 206 | +- Localize `restartToApply` carefully; users need to understand the app will reload. |
| 207 | +- Do not show raw update IDs to normal users unless support needs them. |
| 208 | + |
| 209 | +## Common Pitfalls |
| 210 | + |
| 211 | +1. **Partial colors with poor contrast.** If changing backgrounds, verify text, secondary text, border, success, warning, and error colors too. |
| 212 | +2. **Render prop loses actions.** Custom banners must still call `onUpdate`, `onRestart`, and `onDismiss` appropriately. |
| 213 | +3. **Forgetting `hideSimulation`.** Complete translation objects should include `hideSimulation` along with `simulateUpdate`. |
| 214 | +4. **Debug info in production settings.** Prefer `mode="user"`, hide update IDs, and hide debug sections for customer-facing routes. |
| 215 | +5. **Locale object recreated every render.** Memoize dynamic translations if they depend on hooks to avoid unnecessary re-renders. |
| 216 | + |
| 217 | +## Verification Checklist |
| 218 | + |
| 219 | +- [ ] Banner text remains readable in light and dark mode. |
| 220 | +- [ ] Localized copy covers banner and info-screen labels used by the selected mode. |
| 221 | +- [ ] Custom `renderBanner` returns `null` when no banner should be visible. |
| 222 | +- [ ] User-facing screens hide debug-only fields. |
| 223 | +- [ ] Update/download/restart actions still work after customization. |
0 commit comments