1import { CodedError, NativeModulesProxy, UnavailabilityError } from 'expo-modules-core'; 2 3import ExpoUpdates from './ExpoUpdates'; 4import { 5 LocalAssets, 6 Manifest, 7 UpdateCheckResult, 8 UpdateFetchResult, 9 UpdatesCheckAutomaticallyValue, 10 UpdatesLogEntry, 11 UpdatesNativeStateMachineContext, 12} from './Updates.types'; 13 14/** 15 * The UUID that uniquely identifies the currently running update. The 16 * UUID is represented in its canonical string form (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) and 17 * will always use lowercase letters. This value is `null` when running in a local development environment or any other environment where `expo-updates` is disabled. 18 */ 19export const updateId: string | null = 20 ExpoUpdates.updateId && typeof ExpoUpdates.updateId === 'string' 21 ? ExpoUpdates.updateId.toLowerCase() 22 : null; 23 24/** 25 * The name of the release channel currently configured in this standalone or bare app when using 26 * classic updates. When using Expo Updates, the value of this field is always `"default"`. 27 */ 28export const releaseChannel: string = ExpoUpdates.releaseChannel ?? 'default'; 29 30/** 31 * The channel name of the current build, if configured for use with EAS Update. `null` otherwise. 32 * 33 * Expo Go and development builds are not set to a specific channel and can run any updates compatible with their native runtime. Therefore, this value will always be `null` when running an update on Expo Go or a development build. 34 */ 35export const channel: string | null = ExpoUpdates.channel ?? null; 36 37/** 38 * The runtime version of the current build. 39 */ 40export const runtimeVersion: string | null = ExpoUpdates.runtimeVersion ?? null; 41 42const _checkAutomaticallyMapNativeToJS = { 43 ALWAYS: 'ON_LOAD', 44 ERROR_RECOVERY_ONLY: 'ON_ERROR_RECOVERY', 45 NEVER: 'NEVER', 46 WIFI_ONLY: 'WIFI_ONLY', 47}; 48 49/** 50 * Determines if and when expo-updates checks for and downloads updates automatically on startup. 51 */ 52export const checkAutomatically: UpdatesCheckAutomaticallyValue | null = 53 _checkAutomaticallyMapNativeToJS[ExpoUpdates.checkAutomatically] ?? null; 54 55// @docsMissing 56/** 57 * @hidden 58 */ 59export const localAssets: LocalAssets = ExpoUpdates.localAssets ?? {}; 60 61/** 62 * `expo-updates` does its very best to always launch monotonically newer versions of your app so 63 * you don't need to worry about backwards compatibility when you put out an update. In very rare 64 * cases, it's possible that `expo-updates` may need to fall back to the update that's embedded in 65 * the app binary, even after newer updates have been downloaded and run (an "emergency launch"). 66 * This boolean will be `true` if the app is launching under this fallback mechanism and `false` 67 * otherwise. If you are concerned about backwards compatibility of future updates to your app, you 68 * can use this constant to provide special behavior for this rare case. 69 */ 70export const isEmergencyLaunch: boolean = ExpoUpdates.isEmergencyLaunch || false; 71 72/** 73 * This will be true if the currently running update is the one embedded in the build, 74 * and not one downloaded from the updates server. 75 */ 76export const isEmbeddedLaunch: boolean = ExpoUpdates.isEmbeddedLaunch || false; 77 78// @docsMissing 79/** 80 * @hidden 81 */ 82export const isUsingEmbeddedAssets: boolean = ExpoUpdates.isUsingEmbeddedAssets || false; 83 84/** 85 * If `expo-updates` is enabled, this is the 86 * [manifest](/versions/latest/sdk/constants/#manifest) (or 87 * [classic manifest](/versions/latest/sdk/constants/#appmanifest)) 88 * object for the update that's currently running. 89 * 90 * In development mode, or any other environment in which `expo-updates` is disabled, this object is 91 * empty. 92 */ 93export const manifest: Partial<Manifest> = 94 (ExpoUpdates.manifestString ? JSON.parse(ExpoUpdates.manifestString) : ExpoUpdates.manifest) ?? 95 {}; 96 97/** 98 * If `expo-updates` is enabled, this is a `Date` object representing the creation time of the update that's currently running (whether it was embedded or downloaded at runtime). 99 * 100 * In development mode, or any other environment in which `expo-updates` is disabled, this value is 101 * null. 102 */ 103export const createdAt: Date | null = ExpoUpdates.commitTime 104 ? new Date(ExpoUpdates.commitTime) 105 : null; 106 107const isUsingDeveloperTool = !!(manifest as any).developer?.tool; 108const isUsingExpoDevelopmentClient = NativeModulesProxy.ExponentConstants?.appOwnership === 'expo'; 109const manualUpdatesInstructions = isUsingExpoDevelopmentClient 110 ? 'To test manual updates, publish your project using `expo publish` and open the published ' + 111 'version in this development client.' 112 : 'To test manual updates, make a release build with `npm run ios --configuration Release` or ' + 113 '`npm run android --variant Release`.'; 114 115/** 116 * Instructs the app to reload using the most recently downloaded version. This is useful for 117 * triggering a newly downloaded update to launch without the user needing to manually restart the 118 * app. 119 * 120 * It is not recommended to place any meaningful logic after a call to `await 121 * Updates.reloadAsync()`. This is because the promise is resolved after verifying that the app can 122 * be reloaded, and immediately before posting an asynchronous task to the main thread to actually 123 * reload the app. It is unsafe to make any assumptions about whether any more JS code will be 124 * executed after the `Updates.reloadAsync` method call resolves, since that depends on the OS and 125 * the state of the native module and main threads. 126 * 127 * This method cannot be used in development mode, and the returned promise will be rejected if you 128 * try to do so. 129 * 130 * @return A promise that fulfills right before the reload instruction is sent to the JS runtime, or 131 * rejects if it cannot find a reference to the JS runtime. If the promise is rejected in production 132 * mode, it most likely means you have installed the module incorrectly. Double check you've 133 * followed the installation instructions. In particular, on iOS ensure that you set the `bridge` 134 * property on `EXUpdatesAppController` with a pointer to the `RCTBridge` you want to reload, and on 135 * Android ensure you either call `UpdatesController.initialize` with the instance of 136 * `ReactApplication` you want to reload, or call `UpdatesController.setReactNativeHost` with the 137 * proper instance of `ReactNativeHost`. 138 */ 139export async function reloadAsync(): Promise<void> { 140 if (!ExpoUpdates.reload) { 141 throw new UnavailabilityError('Updates', 'reloadAsync'); 142 } 143 if (!ExpoUpdates?.nativeDebug && (__DEV__ || isUsingExpoDevelopmentClient)) { 144 throw new CodedError( 145 'ERR_UPDATES_DISABLED', 146 `You cannot use the Updates module in development mode in a production app. ${manualUpdatesInstructions}` 147 ); 148 } 149 await ExpoUpdates.reload(); 150} 151 152/** 153 * Checks the server to see if a newly deployed update to your project is available. Does not 154 * actually download the update. This method cannot be used in development mode, and the returned 155 * promise will be rejected if you try to do so. 156 * 157 * Checking for an update uses a device's bandwidth and battery life like any network call. 158 * Additionally, updates served by Expo may be rate limited. A good rule of thumb to check for 159 * updates judiciously is to check when the user launches or foregrounds the app. Avoid polling for 160 * updates in a frequent loop. 161 * 162 * @return A promise that fulfills with an [`UpdateCheckResult`](#updatecheckresult) object. 163 * 164 * The promise rejects if the app is in development mode, or if there is an unexpected error or 165 * timeout communicating with the server. 166 */ 167export async function checkForUpdateAsync(): Promise<UpdateCheckResult> { 168 if (!ExpoUpdates.checkForUpdateAsync) { 169 throw new UnavailabilityError('Updates', 'checkForUpdateAsync'); 170 } 171 if (!ExpoUpdates?.nativeDebug && (__DEV__ || isUsingDeveloperTool)) { 172 throw new CodedError( 173 'ERR_UPDATES_DISABLED', 174 `You cannot check for updates in development mode. ${manualUpdatesInstructions}` 175 ); 176 } 177 178 const result = await ExpoUpdates.checkForUpdateAsync(); 179 if (result.manifestString) { 180 result.manifest = JSON.parse(result.manifestString); 181 delete result.manifestString; 182 } 183 184 return result; 185} 186 187/** 188 * Retrieves the current extra params. 189 */ 190export async function getExtraParamsAsync(): Promise<{ [key: string]: string }> { 191 if (!ExpoUpdates.getExtraParamsAsync) { 192 throw new UnavailabilityError('Updates', 'getExtraParamsAsync'); 193 } 194 195 return await ExpoUpdates.getExtraParamsAsync(); 196} 197 198/** 199 * Sets an extra param if value is non-null, otherwise unsets the param. 200 * Extra params are sent as an [Expo Structured Field Value Dictionary](https://docs.expo.dev/technical-specs/expo-sfv-0/) 201 * in the `Expo-Extra-Params` header of update requests. A compliant update server may use these params when selecting an update to serve. 202 */ 203export async function setExtraParamAsync( 204 key: string, 205 value: string | null | undefined 206): Promise<void> { 207 if (!ExpoUpdates.setExtraParamAsync) { 208 throw new UnavailabilityError('Updates', 'setExtraParamAsync'); 209 } 210 211 return await ExpoUpdates.setExtraParamAsync(key, value ?? null); 212} 213 214/** 215 * Retrieves the most recent expo-updates log entries. 216 * 217 * @param maxAge Sets the max age of retrieved log entries in milliseconds. Default to 3600000 ms (1 hour). 218 * 219 * @return A promise that fulfills with an array of [`UpdatesLogEntry`](#updateslogentry) objects; 220 * 221 * The promise rejects if there is an unexpected error in retrieving the logs. 222 */ 223export async function readLogEntriesAsync(maxAge: number = 3600000): Promise<UpdatesLogEntry[]> { 224 if (!ExpoUpdates.readLogEntriesAsync) { 225 throw new UnavailabilityError('Updates', 'readLogEntriesAsync'); 226 } 227 return await ExpoUpdates.readLogEntriesAsync(maxAge); 228} 229 230/** 231 * Clears existing expo-updates log entries. 232 * 233 * > For now, this operation does nothing on the client. Once log persistence has been 234 * > implemented, this operation will actually remove existing logs. 235 * 236 * @return A promise that fulfills if the clear operation was successful. 237 * 238 * The promise rejects if there is an unexpected error in clearing the logs. 239 * 240 */ 241export async function clearLogEntriesAsync(): Promise<void> { 242 if (!ExpoUpdates.clearLogEntriesAsync) { 243 throw new UnavailabilityError('Updates', 'clearLogEntriesAsync'); 244 } 245 await ExpoUpdates.clearLogEntriesAsync(); 246} 247 248/** 249 * Downloads the most recently deployed update to your project from server to the device's local 250 * storage. This method cannot be used in development mode, and the returned promise will be 251 * rejected if you try to do so. 252 * 253 > **Note:** [`reloadAsync()`](#updatesreloadasync) can be called after promise resolution to 254 * reload the app using the most recently downloaded version. Otherwise, the update will be applied 255 * on the next app cold start. 256 * 257 * @return A promise that fulfills with an [`UpdateFetchResult`](#updatefetchresult) object. 258 * 259 * The promise rejects if the app is in development mode, or if there is an unexpected error or 260 * timeout communicating with the server. 261 */ 262export async function fetchUpdateAsync(): Promise<UpdateFetchResult> { 263 if (!ExpoUpdates.fetchUpdateAsync) { 264 throw new UnavailabilityError('Updates', 'fetchUpdateAsync'); 265 } 266 if (!ExpoUpdates?.nativeDebug && (__DEV__ || isUsingDeveloperTool)) { 267 throw new CodedError( 268 'ERR_UPDATES_DISABLED', 269 `You cannot fetch updates in development mode. ${manualUpdatesInstructions}` 270 ); 271 } 272 273 const result = await ExpoUpdates.fetchUpdateAsync(); 274 if (result.manifestString) { 275 result.manifest = JSON.parse(result.manifestString); 276 delete result.manifestString; 277 } 278 279 return result; 280} 281 282/** 283 * @hidden 284 */ 285export function clearUpdateCacheExperimentalAsync(_sdkVersion?: string) { 286 console.warn( 287 "This method is no longer necessary. `expo-updates` now automatically deletes your app's old bundle files!" 288 ); 289} 290 291/** 292 * @hidden 293 */ 294export function transformNativeStateMachineContext(originalNativeContext: any) { 295 const nativeContext = { ...originalNativeContext }; 296 if (nativeContext.latestManifestString) { 297 nativeContext.latestManifest = JSON.parse(nativeContext.latestManifestString); 298 delete nativeContext.latestManifestString; 299 } 300 if (nativeContext.downloadedManifestString) { 301 nativeContext.downloadedManifest = JSON.parse(nativeContext.downloadedManifestString); 302 delete nativeContext.downloadedManifestString; 303 } 304 if (nativeContext.lastCheckForUpdateTimeString) { 305 nativeContext.lastCheckForUpdateTime = new Date(nativeContext.lastCheckForUpdateTimeString); 306 delete nativeContext.lastCheckForUpdateTimeString; 307 } 308 if (nativeContext.rollbackString) { 309 nativeContext.rollback = JSON.parse(nativeContext.rollbackString); 310 delete nativeContext.rollbackString; 311 } 312 return nativeContext; 313} 314 315/** 316 * @hidden 317 */ 318export async function getNativeStateMachineContextAsync(): Promise<UpdatesNativeStateMachineContext> { 319 // Return the current state machine context 320 if (!ExpoUpdates.getNativeStateMachineContextAsync) { 321 throw new UnavailabilityError('Updates', 'getNativeStateMachineContextAsync'); 322 } 323 const nativeContext = await ExpoUpdates.getNativeStateMachineContextAsync(); 324 return transformNativeStateMachineContext(nativeContext); 325} 326