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// @docsMissing 66/** 67 * @hidden 68 */ 69export const isUsingEmbeddedAssets: boolean = ExpoUpdates.isUsingEmbeddedAssets || false; 70 71/** 72 * If `expo-updates` is enabled, this is the 73 * [manifest](/guides/how-expo-works#expo-development-server) object for the update that's currently 74 * running. 75 * 76 * In development mode, or any other environment in which `expo-updates` is disabled, this object is 77 * empty. 78 */ 79export const manifest: Partial<Manifest> = 80 (ExpoUpdates.manifestString ? JSON.parse(ExpoUpdates.manifestString) : ExpoUpdates.manifest) ?? 81 {}; 82 83/** 84 * 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). 85 * 86 * In development mode, or any other environment in which `expo-updates` is disabled, this value is 87 * null. 88 */ 89export const createdAt: Date | null = ExpoUpdates.commitTime 90 ? new Date(ExpoUpdates.commitTime) 91 : null; 92 93const isUsingDeveloperTool = !!(manifest as any).developer?.tool; 94const isUsingExpoDevelopmentClient = NativeModulesProxy.ExponentConstants?.appOwnership === 'expo'; 95const manualUpdatesInstructions = isUsingExpoDevelopmentClient 96 ? 'To test manual updates, publish your project using `expo publish` and open the published ' + 97 'version in this development client.' 98 : 'To test manual updates, make a release build with `npm run ios --configuration Release` or ' + 99 '`npm run android --variant Release`.'; 100 101/** 102 * Instructs the app to reload using the most recently downloaded version. This is useful for 103 * triggering a newly downloaded update to launch without the user needing to manually restart the 104 * app. 105 * 106 * It is not recommended to place any meaningful logic after a call to `await 107 * Updates.reloadAsync()`. This is because the promise is resolved after verifying that the app can 108 * be reloaded, and immediately before posting an asynchronous task to the main thread to actually 109 * reload the app. It is unsafe to make any assumptions about whether any more JS code will be 110 * executed after the `Updates.reloadAsync` method call resolves, since that depends on the OS and 111 * the state of the native module and main threads. 112 * 113 * This method cannot be used in development mode, and the returned promise will be rejected if you 114 * try to do so. 115 * 116 * @return A promise that fulfills right before the reload instruction is sent to the JS runtime, or 117 * rejects if it cannot find a reference to the JS runtime. If the promise is rejected in production 118 * mode, it most likely means you have installed the module incorrectly. Double check you've 119 * followed the installation instructions. In particular, on iOS ensure that you set the `bridge` 120 * property on `EXUpdatesAppController` with a pointer to the `RCTBridge` you want to reload, and on 121 * Android ensure you either call `UpdatesController.initialize` with the instance of 122 * `ReactApplication` you want to reload, or call `UpdatesController.setReactNativeHost` with the 123 * proper instance of `ReactNativeHost`. 124 */ 125export async function reloadAsync(): Promise<void> { 126 if (!ExpoUpdates.reload) { 127 throw new UnavailabilityError('Updates', 'reloadAsync'); 128 } 129 if (__DEV__ && !isUsingExpoDevelopmentClient) { 130 throw new CodedError( 131 'ERR_UPDATES_DISABLED', 132 `You cannot use the Updates module in development mode in a production app. ${manualUpdatesInstructions}` 133 ); 134 } 135 await ExpoUpdates.reload(); 136} 137 138/** 139 * Checks the server to see if a newly deployed update to your project is available. Does not 140 * actually download the update. This method cannot be used in development mode, and the returned 141 * promise will be rejected if you try to do so. 142 * 143 * Checking for an update uses a device's bandwidth and battery life like any network call. 144 * Additionally, updates served by Expo may be rate limited. A good rule of thumb to check for 145 * updates judiciously is to check when the user launches or foregrounds the app. Avoid polling for 146 * updates in a frequent loop. 147 * 148 * @return A promise that fulfills with an [`UpdateCheckResult`](#updatecheckresult) object. 149 * 150 * The promise rejects if the app is in development mode, or if there is an unexpected error or 151 * timeout communicating with the server. 152 */ 153export async function checkForUpdateAsync(): Promise<UpdateCheckResult> { 154 if (!ExpoUpdates.checkForUpdateAsync) { 155 throw new UnavailabilityError('Updates', 'checkForUpdateAsync'); 156 } 157 if (__DEV__ || isUsingDeveloperTool) { 158 throw new CodedError( 159 'ERR_UPDATES_DISABLED', 160 `You cannot check for updates in development mode. ${manualUpdatesInstructions}` 161 ); 162 } 163 164 const result = await ExpoUpdates.checkForUpdateAsync(); 165 if (result.manifestString) { 166 result.manifest = JSON.parse(result.manifestString); 167 delete result.manifestString; 168 } 169 170 return result; 171} 172 173/** 174 * Retrieves the most recent expo-updates log entries. 175 * 176 * @param maxAge Sets the max age of retrieved log entries in milliseconds. Default to 3600000 ms (1 hour). 177 * 178 * @return A promise that fulfills with an array of [`UpdatesLogEntry`](#updateslogentry) objects; 179 * 180 * The promise rejects if there is an unexpected error in retrieving the logs. 181 */ 182export async function readLogEntriesAsync(maxAge: number = 3600000): Promise<UpdatesLogEntry[]> { 183 if (!ExpoUpdates.readLogEntriesAsync) { 184 throw new UnavailabilityError('Updates', 'readLogEntriesAsync'); 185 } 186 return await ExpoUpdates.readLogEntriesAsync(maxAge); 187} 188 189/** 190 * Clears existing expo-updates log entries. 191 * 192 * > For now, this operation does nothing on the client. Once log persistence has been 193 * > implemented, this operation will actually remove existing logs. 194 * 195 * @return A promise that fulfills if the clear operation was successful. 196 * 197 * The promise rejects if there is an unexpected error in clearing the logs. 198 * 199 */ 200export async function clearLogEntriesAsync(): Promise<void> { 201 if (!ExpoUpdates.clearLogEntriesAsync) { 202 throw new UnavailabilityError('Updates', 'clearLogEntriesAsync'); 203 } 204 await ExpoUpdates.clearLogEntriesAsync(); 205} 206 207/** 208 * Downloads the most recently deployed update to your project from server to the device's local 209 * storage. This method cannot be used in development mode, and the returned promise will be 210 * rejected if you try to do so. 211 * 212 * @return A promise that fulfills with an [`UpdateFetchResult`](#updatefetchresult) object. 213 * 214 * The promise rejects if the app is in development mode, or if there is an unexpected error or 215 * timeout communicating with the server. 216 */ 217export async function fetchUpdateAsync(): Promise<UpdateFetchResult> { 218 if (!ExpoUpdates.fetchUpdateAsync) { 219 throw new UnavailabilityError('Updates', 'fetchUpdateAsync'); 220 } 221 if (__DEV__ || isUsingDeveloperTool) { 222 throw new CodedError( 223 'ERR_UPDATES_DISABLED', 224 `You cannot fetch updates in development mode. ${manualUpdatesInstructions}` 225 ); 226 } 227 228 const result = await ExpoUpdates.fetchUpdateAsync(); 229 if (result.manifestString) { 230 result.manifest = JSON.parse(result.manifestString); 231 delete result.manifestString; 232 } 233 234 return result; 235} 236 237/** 238 * @hidden 239 */ 240export function clearUpdateCacheExperimentalAsync(_sdkVersion?: string) { 241 console.warn( 242 "This method is no longer necessary. `expo-updates` now automatically deletes your app's old bundle files!" 243 ); 244} 245 246let _emitter: EventEmitter | null; 247 248function _getEmitter(): EventEmitter { 249 if (!_emitter) { 250 _emitter = new EventEmitter(); 251 RCTDeviceEventEmitter.addListener('Expo.nativeUpdatesEvent', _emitEvent); 252 } 253 return _emitter; 254} 255 256function _emitEvent(params): void { 257 let newParams = params; 258 if (typeof params === 'string') { 259 newParams = JSON.parse(params); 260 } 261 if (newParams.manifestString) { 262 newParams.manifest = JSON.parse(newParams.manifestString); 263 delete newParams.manifestString; 264 } 265 266 if (!_emitter) { 267 throw new Error(`EventEmitter must be initialized to use from its listener`); 268 } 269 _emitter.emit('Expo.updatesEvent', newParams); 270} 271 272/** 273 * Adds a callback to be invoked when updates-related events occur (such as upon the initial app 274 * load) due to auto-update settings chosen at build-time. 275 * 276 * @param listener A function that will be invoked with an [`UpdateEvent`](#updateevent) instance 277 * and should not return any value. 278 * @return An `EventSubscription` object on which you can call `remove()` to unsubscribe the 279 * listener. 280 */ 281export function addListener(listener: (event: UpdateEvent) => void): EventSubscription { 282 const emitter = _getEmitter(); 283 return emitter.addListener('Expo.updatesEvent', listener); 284} 285