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