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