xref: /expo/packages/expo-updates/src/Updates.ts (revision 6eefacaa)
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