# API (https://hotcodepush.com/docs/sdks/expo/api)

Every method, event and type of @hotcodepush/expo-ota-updates and the useUpdates hook, with a signature and an example each.

The Expo SDK is the React Native SDK re-exported: `@hotcodepush/expo-ota-updates` exports the `HotCodePush` object with its 16 methods and 5 events, the `useUpdates()` hook and the types. Every method takes at most one options object and resolves a typed result or nothing. Nothing an app should handle is thrown.

```ts title="src/updates.ts"
import { HotCodePush, useUpdates } from '@hotcodepush/expo-ota-updates';
```

Every import comes from the Expo package, which exports every type on this page, and the examples below assume this import. A result is a discriminated union on `status`, with a `reason` where the status needs one. An app switches on typed values and never parses a message.

## Methods [#methods]

| Method | What it does | Resolves |
|---|---|---|
| [`sync()`](#sync) | Runs one cycle under the strategies: check, download, apply, report. | `SyncResult` |
| [`checkForUpdate()`](#checkforupdate) | Checks and downloads nothing. | `CheckForUpdateResult` |
| [`downloadUpdate()`](#downloadupdate) | Checks again, downloads and applies per the apply strategies. | `DownloadUpdateResult` |
| [`applyUpdate()`](#applyupdate) | Applies the downloaded update now. | `ApplyUpdateResult` |
| [`notifyReady()`](#notifyready) | Ends the readiness gate under `readySignal: manual`. | `NotifyReadyResult` |
| [`rollbackUpdate()`](#rollbackupdate) | Rolls the running release back now. | nothing |
| [`clearUpdates()`](#clearupdates) | Drops every downloaded update and reloads the embedded bundle. | nothing |
| [`setRestartAllowed()`](#setrestartallowed) | Holds or releases the restart the SDK performs on its own. | nothing |
| [`getState()`](#getstate) | Answers a snapshot of what the SDK knows. | `GetStateResult` |
| [`getChannel()`](#getchannel) | Answers the channel in effect and where it came from. | `GetChannelResult` |
| [`setChannel()`](#setchannel) | Switches the device to a channel by name or by id. | nothing |
| [`getDevice()`](#getdevice) | Answers the facts of the device report. | `GetDeviceResult` |
| [`setAttributes()`](#setattributes) | Merges the attributes that `attribute` conditions match. | nothing |
| [`showDebugScreen()`](#showdebugscreen) | Opens the native debug screen. | nothing |
| [`addListener()`](#addlistener) | Registers a listener for one event. | a handle with `remove()` |
| [`removeAllListeners()`](#removealllisteners) | Removes every listener. | nothing |

### sync() [#sync]

```ts
sync(options?: SyncOptions): Promise<SyncResult>
```

One cycle under the [strategies](https://hotcodepush.com/docs/sdks/expo/configuration#strategies), the same the SDK runs on its own at start, on resume and at the interval. It fetches the channel's index, evaluates it on the device, downloads and verifies, applies and reports. Its `options` override any of the three strategies for this cycle, and a second call while a cycle runs joins the running one.

It resolves once the outcome is known, with one of the [statuses](#statuses):

- `up_to_date`, or `available` when the download stayed manual
- `downloaded` with the `applyStrategy` in force, or `applied` with the reload following
- `skipped` or `failed` with a reason

A release already downloaded answers `downloaded` again, never `up_to_date`. A change made in the console reaches the next `sync()` within about a minute ([How an update reaches a device](https://hotcodepush.com/docs/concepts/update-delivery)).

```ts
const result = await HotCodePush.sync();
switch (result.status) {
  case 'applied':
    // the reload follows this result
    break;
  case 'downloaded':
    console.log(`release #${result.release.number} applies ${result.applyStrategy}`);
    break;
  case 'available':
    console.log(`release #${result.release.number} waits for downloadUpdate()`);
    break;
  case 'skipped':
  case 'failed':
    console.log(`${result.status}: ${result.reason}`);
    break;
  case 'up_to_date':
    break;
}
```

### checkForUpdate() [#checkforupdate]

```ts
checkForUpdate(): Promise<CheckForUpdateResult>
```

`sync()` with `downloadStrategy` pinned to `manual`: it fetches and evaluates, downloads nothing and moves nothing. It resolves `up_to_date`, `available` with the release, its notes and the pack's size, `skipped` or `failed`. A release already downloaded answers `available` too. It counts as the device's check and is recorded as `getState().lastCheck`.

```ts
const result = await HotCodePush.checkForUpdate();
if (result.status === 'available') {
  console.log(`release #${result.release.number}, ${result.downloadSizeBytes} bytes: ${result.notes ?? ''}`);
}
```

### downloadUpdate() [#downloadupdate]

```ts
downloadUpdate(options?: DownloadUpdateOptions): Promise<DownloadUpdateResult>
```

`sync()` with `downloadStrategy` pinned to `auto`: it checks the index again, downloads and verifies, and applies per `applyStrategy` or `mandatoryApplyStrategy`, which `options` overrides for this cycle. It resolves `downloaded` with the `applyStrategy` in force, `applied`, `up_to_date`, `skipped` or `failed`, never `available`. The cycle is recorded as `getState().lastCheck` and resets no interval.

```ts
const result = await HotCodePush.downloadUpdate({ applyStrategy: 'manual' });
if (result.status === 'downloaded') {
  console.log(`release #${result.release.number} is downloaded; call applyUpdate() when ready`);
}
```

### applyUpdate() [#applyupdate]

```ts
applyUpdate(): Promise<ApplyUpdateResult>
```

Applies the downloaded update now, whatever the strategy says, and reloads the app, at once or as soon as the app is up. It resolves `applied` with the release, or `nothing_to_apply` with the release that runs. A downloaded release that has left the index since is dropped first. Holding restarts with `setRestartAllowed()` never holds it.

A call still waiting when the run ends is not kept, so the app calls it again. It is how an app on `applyStrategy: manual` finishes an update, a mandatory one under `mandatoryApplyStrategy: manual` included. The pattern is in [Choose an apply strategy](https://hotcodepush.com/docs/guides/apply-strategies).

```ts
const result = await HotCodePush.applyUpdate();
if (result.status === 'applied') {
  console.log(`release #${result.release.number} loads now`);
}
```

### notifyReady() [#notifyready]

```ts
notifyReady(): Promise<NotifyReadyResult>
```

Ends the [readiness gate](https://hotcodepush.com/docs/sdks/expo/configuration#readiness) when `readySignal` is `manual`, and is safe to call at any time on either setting. It also says the app is up in this run, so a reload that waited for that runs. The result says whether this start follows a rollback: `isRolledBack` with `rollbackReason`, and `previousRelease`, each told once. `currentRelease` is `null` on the embedded bundle.

```ts
const result = await HotCodePush.notifyReady();
if (result.isRolledBack) {
  console.log(`rolled back from #${result.previousRelease?.number}: ${result.rollbackReason}`);
}
```

### rollbackUpdate() [#rollbackupdate]

```ts
rollbackUpdate(options?: RollbackUpdateOptions): Promise<void>
```

Rolls the running release back now and reloads at once. It reverts to the last confirmed release or the embedded bundle and marks the bundle as failed on this device. The device reports `app_requested` with `reason` as the detail.

It does nothing on the embedded bundle, and `reason` is at most 256 Unicode code points without a control character. A rollback that reaches every device is [Roll back a release](https://hotcodepush.com/docs/guides/rollbacks).

```ts
await HotCodePush.rollbackUpdate({ reason: 'checkout failed to load' });
```

### clearUpdates() [#clearupdates]

```ts
clearUpdates(): Promise<void>
```

Clears every downloaded update and the list of failed bundles, keeps the device id, channel choice and attributes, and reloads into the embedded bundle. Before the app is up in this run it waits, and `setRestartAllowed({ isAllowed: false })` never holds it. It is the escape hatch a support guide can name.

```ts
await HotCodePush.clearUpdates();
```

### setRestartAllowed() [#setrestartallowed]

```ts
setRestartAllowed(options: SetRestartAllowedOptions): Promise<void>
```

`isAllowed: false` holds the restart the SDK would perform on its own, at most one, until `isAllowed: true` runs it. That covers an `immediate` apply, a `next_resume` apply, a mandatory release and the reload after a readiness timeout. Until the held restart runs, `getState().currentRelease` still names what runs. An apply still held when the run ends takes effect at the next start.

A restart the app asks for itself, `applyUpdate()`, `rollbackUpdate()` or `clearUpdates()`, is never held, and every start begins allowed. It is how an app protects a checkout or an unsaved form without giving up automatic applies.

```ts
await HotCodePush.setRestartAllowed({ isAllowed: false });
// ... the flow the user must finish ...
await HotCodePush.setRestartAllowed({ isAllowed: true });
```

### getState() [#getstate]

```ts
getState(): Promise<GetStateResult>
```

A snapshot of what the SDK knows, everything the debug screen shows:

- `currentRelease` is the running release, `null` on the embedded bundle, and what an app attaches to its own telemetry.
- `nextRelease` is downloaded and waiting for its apply moment.
- `fallbackRelease` is the last confirmed release, the one a rollback returns to.
- `embeddedBundleId` comes from the resource file and is `null` in a build whose step created no binary.
- `lastCheck` holds the last cycle's time, trigger and result.
- `index` is the channel index the device holds, with `sequence` its publish time in milliseconds, the debug screen's Published at, and `fetchedAt` when the device fetched it.
- `failedBundleIds` lists the bundles that failed on this device and are never tried again here.
- `reportedAt` is the server time of the month's first acknowledged report.

```ts
const state = await HotCodePush.getState();
console.log(state.currentRelease?.bundleVersion ?? 'embedded', state.lastCheck?.result.status);
```

### getChannel() [#getchannel]

```ts
getChannel(): Promise<GetChannelResult>
```

The channel in effect with its `source`, `runtime` after `setChannel()` or `config` from the build. The `id` is `null` while no id is known, on a build without a channel and for a runtime name no sync has resolved yet. The `name` is the runtime name, `null` for the build's channel, which the build carries by id alone.

```ts
const channel = await HotCodePush.getChannel();
console.log(channel.source, channel.id ?? 'unresolved', channel.name ?? '');
```

### setChannel() [#setchannel]

```ts
setChannel(options: SetChannelOptions): Promise<void>
```

Switches the device to a [channel](https://hotcodepush.com/docs/concepts/channels), by `{ name }` or by `{ id }`. A name works for a channel flagged discoverable with [`channel update --discoverable`](https://hotcodepush.com/docs/cli/commands/channel). An id works for a channel the app was handed at runtime: a preview from a QR code, or a tenant's channel from your backend.

Passing `null` clears the runtime choice and returns to the build's channel. A name is stored and resolved at the next sync, which answers `channel_unknown` while the channel is not discoverable. A channel flagged a moment ago reaches the device within about a minute.

A deleted or expired channel serves no index: every check fails with `channel_unknown`, the device keeps the bundle it runs and its channel, and the SDK never changes the channel on its own. A runtime choice is yours to clear with `setChannel(null)`, as [Switch channels by link](https://hotcodepush.com/docs/guides/deep-links#4-return-to-the-builds-channel) shows.

The patterns around the call are [Test with preview channels](https://hotcodepush.com/docs/guides/preview-channels), [Switch channels by link](https://hotcodepush.com/docs/guides/deep-links) and [Set up environments and tenants](https://hotcodepush.com/docs/guides/environments).

```ts
await HotCodePush.setChannel({ name: 'staging' });
const result = await HotCodePush.sync();
if (result.status === 'failed' && result.reason === 'channel_unknown') {
  await HotCodePush.setChannel(null);
}
```

### getDevice() [#getdevice]

```ts
getDevice(): Promise<GetDeviceResult>
```

The facts of the device report and nothing more:

- `id` is the anonymous per-install id.
- `platform`, `osVersion` and `sdkVersion` are `ios` or `android`, the OS version and the SDK version.
- `binaryVersion` and `binaryBuild` are the store build's version and build number.
- `fingerprint` is the native fingerprint the build carries, `null` in a build that carries none.
- `channel` reads as `getChannel()` answers it.
- `attributes` holds every attribute the app set.

What leaves the device is on [Privacy and device data](https://hotcodepush.com/docs/privacy).

```ts
const device = await HotCodePush.getDevice();
console.log(device.id, device.platform, device.binaryVersion, device.fingerprint);
```

### setAttributes() [#setattributes]

```ts
setAttributes(options: SetAttributesOptions): Promise<void>
```

Merges string attributes for `attribute` conditions, `userId`, `tenant`, `plan`, `cohort`. A `null` value removes a key. They are evaluated on the device and reported when they change, which is what [Target devices with attributes](https://hotcodepush.com/docs/guides/targeting) builds on. A key is letters, digits, `_`, `-` and `.`, at most 64 characters, and a value is at most 256 Unicode code points without a control character.

Attributes are labels, never personal data: an opaque user id yes, an email never.

```ts
await HotCodePush.setAttributes({ userId: '42', plan: 'pro', cohort: null });
```

### showDebugScreen() [#showdebugscreen]

```ts
showDebugScreen(): Promise<void>
```

Opens the SDK's native debug screen over the app, which shows what the device knows. Check now runs a cycle with the trigger `manual`, a `sync()` on iOS and a `checkForUpdate()` on Android. Share opens the system share sheet with the screen as text. While a runtime choice is in effect, the screen offers Return to configured channel, which calls `setChannel(null)`.

Put the call behind a settings entry or a shake handler. An app may ship it, since nothing on it is secret and the log never leaves the device on its own.

```ts
await HotCodePush.showDebugScreen();
```

| Section | Line | Means |
|---|---|---|
| Device | Device id | Shows the anonymous id the SDK generated at first start, the one [`device probe`](https://hotcodepush.com/docs/cli/commands/device) takes. |
| Device | Binary, OS, SDK | Shows the build's version and build number, the OS version and the SDK version. |
| Device | Fingerprint | Shows the native [fingerprint](https://hotcodepush.com/docs/concepts/compatibility) the build carries, or `none`. |
| Device | Debug build, Attributes | Shows `yes` or `no`, then every `key=value` the app set, or `none`. |
| Channel | Channel id, Name, Source | Shows the channel in effect, `unresolved` for a runtime name no sync has resolved yet, then its name, then `config` from the build or `runtime` after `setChannel()`. |
| Releases | Running, Downloaded, Fallback | Shows the release that runs, the one waiting for its apply moment and the one a rollback returns to. |
| Releases | Embedded bundle | Shows the id of the bundle the build ships, or `not registered` when the build step created no binary. |
| Releases | Failed bundles | Lists the bundles that failed on this device and are never tried again here. |
| Last check | When, Trigger, Result | Shows the last cycle's time, what started it, and its status with reason and condition as one code, `skipped device_incompatible fingerprint`. |
| Last check | Message | Shows the sentence behind the code. |
| Index | Published at | Shows when the channel index the device holds was published. |
| Index | Fetched at | Shows when the device fetched it, or `never`. |
| Index | Reported at | Shows the server time of the month's first acknowledged report, the moment that counts the device as active this month. |
| Configuration | Built at | Shows the build time. No release created before it is applied on this binary. |
| Configuration | Strategies, Ready signal, Debug builds, Public keys | Shows the values the build carries, the public keys as a count. |
| Log | every line | Shows this session's log, newest last: the time, a code and a sentence. It holds 200 entries and starts empty at every launch. |

### addListener() [#addlistener]

```ts
addListener<EventName extends HotCodePushEventName>(
  eventName: EventName,
  listener: (event: HotCodePushEvents[EventName]) => void,
): Promise<HotCodePushListenerHandle>
```

Registers a listener for one of the five [events](#events) and resolves a `HotCodePushListenerHandle`, `{ remove: () => Promise<void> }`. The `updateRolledBack` event belongs to the start that follows a rollback, before any listener exists. The native side keeps it, and every listener added during that start receives it once.

```ts
const handle = await HotCodePush.addListener('updateDownloaded', event => {
  console.log(`release #${event.release.number} applies ${event.applyStrategy}`);
});
// later, when the screen goes
await handle.remove();
```

### removeAllListeners() [#removealllisteners]

```ts
removeAllListeners(): Promise<void>
```

Removes every listener added through `addListener()`, the `updateRolledBack` ones included.

```ts
await HotCodePush.removeAllListeners();
```

## Events [#events]

No event fires for a cycle's start or end, for a check that found nothing or for a skipped release. The last cycle's outcome is `getState().lastCheck`. No event fires before a reload.

| Event | Payload | When |
|---|---|---|
| [`updateAvailable`](#updateavailable) | `UpdateAvailableEvent` | A check found a release the device qualifies for. |
| [`updateDownloaded`](#updatedownloaded) | `UpdateDownloadedEvent` | The download completed and the update waits for its apply moment. |
| [`updateFailed`](#updatefailed) | `UpdateFailedEvent` | A check or a download failed. |
| [`downloadProgress`](#downloadprogress) | `DownloadProgressEvent` | A pack is downloading. |
| [`updateRolledBack`](#updaterolledback) | `UpdateRolledBackEvent` | The app starts after a rollback. |

### updateAvailable [#updateavailable]

```ts
{ downloadSizeBytes: number | null; notes: string | null; release: Release; trigger: SyncTrigger }
```

A check found a release the device qualifies for. `downloadSizeBytes` is the pack's size, `0` for a release of the embedded bundle, which downloads nothing. The `trigger` says what started the cycle: `start`, `resume`, `interval` or `manual`. It is the event a manual download waits on, and the one behind a "new version" badge.

```ts
await HotCodePush.addListener('updateAvailable', event => {
  console.log(`release #${event.release.number}, ${event.downloadSizeBytes} bytes, found on ${event.trigger}`);
});
```

### updateDownloaded [#updatedownloaded]

```ts
{ applyStrategy: 'next_start' | 'next_resume' | 'manual'; release: Release; trigger: SyncTrigger }
```

The download completed and the update waits for its apply. `applyStrategy` names the moment, or `manual` for the app's own `applyUpdate()`. It never fires for an `immediate` apply, whose download goes straight to the reload. It is the event behind the restart dialog for a mandatory release under `mandatoryApplyStrategy: manual`, with `release.isMandatory` saying so.

```ts
await HotCodePush.addListener('updateDownloaded', event => {
  if (event.applyStrategy === 'manual' && event.release.isMandatory) {
    void HotCodePush.applyUpdate();
  }
});
```

### updateFailed [#updatefailed]

```ts
{ message: string; reason: FailedReason; release: Release | null; trigger: SyncTrigger }
```

A check or a download failed, with the reason from the catalog and a message for the log. A build without a channel starts no automatic check, so there the event follows a call the app made.

```ts
await HotCodePush.addListener('updateFailed', event => {
  console.log(`${event.reason}: ${event.message}`);
});
```

### downloadProgress [#downloadprogress]

```ts
{ downloadSizeBytes: number; progress: number; progressBytes: number; releaseId: string }
```

While a pack downloads: `progressBytes` of `downloadSizeBytes` have arrived, and `progress` is their share from `0` to `1`.

```ts
await HotCodePush.addListener('downloadProgress', event => {
  console.log(`${Math.round(event.progress * 100)} % of ${event.downloadSizeBytes} bytes`);
});
```

### updateRolledBack [#updaterolledback]

```ts
{ fromRelease: Release; reason: RollbackReason; toRelease: Release | null }
```

Fires at the start that follows a rollback, before the readiness gate, and again at each start until the app has come up once. The `toRelease` is `null` for the embedded bundle. The event is kept until the first listener of that start registers, so a listener added after the start still receives it.

```ts
await HotCodePush.addListener('updateRolledBack', event => {
  console.log(`release #${event.fromRelease.number} rolled back: ${event.reason}`);
});
```

## Hooks [#hooks]

### useUpdates() [#useupdates]

```ts
useUpdates(): UseUpdatesResult
```

A React hook over the five events and `getState()`, sugar over the common API and never a capability of its own:

- `state` is `getState()` read on mount and again after each event and each cycle the app runs, and the embedded state until the first read.
- `lastSync` is `state.lastCheck.result`, the SDK's own cycles included, or `null` before the first.
- `isSyncing` is `true` while a cycle the app started runs or a pack downloads.
- `downloadProgress` is the download's progress from `0` to `1` while a pack downloads, `null` otherwise.

```tsx title="src/ReleaseLabel.tsx"
import { HotCodePush, useUpdates } from '@hotcodepush/expo-ota-updates';
import { Pressable, Text, View } from 'react-native';

export function ReleaseLabel() {
  const { downloadProgress, isSyncing, lastSync, state } = useUpdates();
  const release = state.currentRelease;
  return (
    <View>
      <Text>{release ? `#${release.number} ${release.bundleVersion}` : 'embedded'}</Text>
      <Text>{isSyncing ? 'syncing…' : (lastSync?.status ?? 'no sync yet')}</Text>
      {downloadProgress !== null && <Text>{Math.round(downloadProgress * 100)}%</Text>}
      <Pressable disabled={isSyncing} onPress={() => void HotCodePush.sync()}>
        <Text>Sync now</Text>
      </Pressable>
    </View>
  );
}
```

## Types [#types]

The types as the package exports them, the SDK's own first. The `HotCodePush` object is typed by the React Native package's `HotCodePush` interface: the 16 methods above, with `addListener` resolving `HotCodePushListenerHandle`.

```ts
interface HotCodePushListenerHandle {
  remove: () => Promise<void>;
}

interface UseUpdatesResult {
  // The download's progress from 0 to 1 while a pack downloads, null otherwise.
  downloadProgress: number | null;
  // A cycle the app started is running, or a pack is downloading.
  isSyncing: boolean;
  // The result of the last cycle, the SDK's own included; null before the first.
  lastSync: SyncResult | null;
  state: GetStateResult;
}
```

The shared types, the same in every SDK:

```ts
// The strategies, the values of the matching hotcodepush.json keys.
type CheckStrategy = 'auto' | 'manual';
type DownloadStrategy = 'auto' | 'manual' | 'unmetered';
type ApplyStrategy = 'immediate' | 'manual' | 'next_resume' | 'next_start';
type MandatoryApplyStrategy = 'immediate' | 'manual';
type ReadySignal = 'manual' | 'render';
// When a scheduled apply runs; `manual` is not a moment.
type ApplyMoment = Exclude<ApplyStrategy, 'manual'>;
// What started a cycle: the SDK on its own, or the app's call.
type SyncTrigger = 'interval' | 'manual' | 'resume' | 'start';
type ConditionType = 'attribute' | 'binary' | 'device' | 'fingerprint' | 'os';
type Platform = 'android' | 'ios';

// The SDK's own release type: 5 fields mapped from an index entry.
interface Release {
  bundleId: string;
  bundleVersion: string;
  id: string;
  isMandatory: boolean;
  number: number;
}

// The statuses, as the Statuses table below reads them.
type SyncStatus = 'applied' | 'available' | 'downloaded' | 'failed' | 'skipped' | 'up_to_date';
type ApplyUpdateStatus = 'applied' | 'nothing_to_apply';

type SyncResult =
  | { release: Release | null; status: 'up_to_date' }
  | { downloadSizeBytes: number | null; notes: string | null; release: Release; status: 'available' }
  | { applyStrategy: Exclude<ApplyStrategy, 'immediate'>; notes: string | null; release: Release; status: 'downloaded' }
  | { notes: string | null; release: Release; status: 'applied' }
  | { conditionType?: ConditionType; reason: SkippedReason; release: Release | null; status: 'skipped' }
  | { message: string; reason: FailedReason; release: Release | null; status: 'failed' };

// `sync()` with the download pinned to `manual`: nothing is downloaded or applied.
type CheckForUpdateResult = Exclude<SyncResult, { status: 'applied' | 'downloaded' }>;
// `sync()` with the download pinned to `auto`: the cycle never stops at `available`.
type DownloadUpdateResult = Exclude<SyncResult, { status: 'available' }>;

type ApplyUpdateResult =
  | { release: Release; status: 'applied' }
  | { release: Release | null; status: 'nothing_to_apply' };

interface NotifyReadyResult {
  currentRelease: Release | null;
  isRolledBack: boolean;
  // The release before this start, when it changed.
  previousRelease: Release | null;
  rollbackReason?: RollbackReason;
}

interface GetStateResult {
  currentRelease: Release | null;
  // From the resource file; `null` in a build whose step created no binary.
  embeddedBundleId: string | null;
  failedBundleIds: string[];
  fallbackRelease: Release | null;
  index: { fetchedAt: string; sequence: number } | null;
  lastCheck: { checkedAt: string; result: SyncResult; trigger: SyncTrigger } | null;
  nextRelease: Release | null;
  // The server time of the month's first acknowledged report.
  reportedAt: string | null;
}

type ChannelSource = 'config' | 'runtime';

interface GetChannelResult {
  // `null` while no id is known: a build without a channel, or a runtime name no sync has resolved yet.
  id: string | null;
  name: string | null;
  source: ChannelSource;
}

type SetChannelOptions = { id: string } | { name: string } | null;

interface GetDeviceResult {
  attributes: Record<string, string>;
  binaryBuild: string;
  binaryVersion: string;
  channel: GetChannelResult;
  // `fp1:<sha256>` from the resource file, `null` in a build that carries none.
  fingerprint: string | null;
  id: string;
  osVersion: string;
  platform: Platform;
  sdkVersion: string;
}

// `null` removes a key; stored and reported stays Record<string, string>.
type SetAttributesOptions = Record<string, string | null>;

interface RollbackUpdateOptions {
  // The app-side cause, carried as the failure's detail: at most 256 code points, no control character.
  reason?: string;
}

// Each stage's strategy for this call, overriding the configuration.
interface SyncOptions {
  applyStrategy?: ApplyStrategy;
  downloadStrategy?: DownloadStrategy;
  mandatoryApplyStrategy?: MandatoryApplyStrategy;
}

// The apply strategies for this call; the download strategy is pinned to `auto`.
type DownloadUpdateOptions = Omit<SyncOptions, 'downloadStrategy'>;

interface SetRestartAllowedOptions {
  isAllowed: boolean;
}

type SkippedReason =
  | 'build_debug' | 'bundle_failed_before' | 'channel_paused' | 'condition_unsupported' | 'connection_metered'
  | 'device_incompatible' | 'device_not_in_rollout' | 'device_not_targeted' | 'release_older_than_binary'
  | 'release_revoked' | 'spending_cap_reached';
type FailedReason =
  | 'channel_unknown' | 'content_mismatched' | 'device_offline' | 'download_failed' | 'index_invalid'
  | 'manifest_invalid' | 'signature_invalid';
type RollbackReason = 'app_crashed' | 'app_requested' | 'readiness_timed_out';

// What the device reports: the event rows of Results and reasons, and the pack a download came from.
type DeviceEventType = 'applied' | 'checked' | 'confirmed' | 'downloaded' | 'failed';
type PackKind = 'delta' | 'files' | 'full' | 'streamed';

// The events, by name.
interface UpdateAvailableEvent {
  downloadSizeBytes: number | null;
  notes: string | null;
  release: Release;
  trigger: SyncTrigger;
}
interface UpdateDownloadedEvent {
  applyStrategy: Exclude<ApplyStrategy, 'immediate'>;
  release: Release;
  trigger: SyncTrigger;
}
interface UpdateFailedEvent {
  message: string;
  reason: FailedReason;
  release: Release | null;
  trigger: SyncTrigger;
}
interface DownloadProgressEvent {
  downloadSizeBytes: number;
  progress: number;
  progressBytes: number;
  releaseId: string;
}
interface UpdateRolledBackEvent {
  fromRelease: Release;
  reason: RollbackReason;
  toRelease: Release | null;
}
interface HotCodePushEvents {
  downloadProgress: DownloadProgressEvent;
  updateAvailable: UpdateAvailableEvent;
  updateDownloaded: UpdateDownloadedEvent;
  updateFailed: UpdateFailedEvent;
  updateRolledBack: UpdateRolledBackEvent;
}
type HotCodePushEventName = keyof HotCodePushEvents;
```

The `release` is `null` where none could be evaluated: the index holds nothing for the embedded bundle, could not be read, or the cycle stopped before it. A failure after the evaluation names the release it tried. `conditionType` on `skipped` names the condition type behind `device_incompatible` or `device_not_targeted`, never a value.

## Statuses [#statuses]

Seven statuses, named by what the app should do next.

| Status | From | Meaning |
|---|---|---|
| `up_to_date` | `sync()`, `checkForUpdate()`, `downloadUpdate()` | There is nothing to do. A release that carries the running bundle answers it too. |
| `available` | `checkForUpdate()`, and `sync()` when the download stayed manual | A release this device can take is not downloaded yet, or was downloaded already when `checkForUpdate()` asked. Call `downloadUpdate()`. |
| `downloaded` | `sync()`, `downloadUpdate()` | The update is downloaded and verified. Under `applyStrategy: manual` call `applyUpdate()`, otherwise the strategy's moment applies it. |
| `applied` | `sync()`, `downloadUpdate()`, `applyUpdate()` | The apply happened, and the update becomes the current release as the app reloads, now or once the app is up in this run. |
| `nothing_to_apply` | `applyUpdate()` | Nothing waits. |
| `skipped` | `sync()`, `checkForUpdate()`, `downloadUpdate()` | There is a newer release and this device will not take it now, and `reason` says why. |
| `failed` | `sync()`, `checkForUpdate()`, `downloadUpdate()` | Something went wrong on the way, and `reason` says what. |

The `reason` on `skipped` and `failed`, and the rollback reasons on `updateRolledBack` and `notifyReady()`, are the unions above. [Results and reasons](https://hotcodepush.com/docs/troubleshooting/results-and-reasons) is the catalog: what each code means and what to do.

## Errors [#errors]

No error carries a code, and every outcome an app can act on is a result. A method with nothing to do does nothing: `applyUpdate()` without a downloaded update answers `nothing_to_apply`, and `rollbackUpdate()` on the embedded bundle resolves. A method rejects with a plain error, a message and nothing else, for a programming mistake found in development:

- a strategy value outside its choices on `sync()` or `downloadUpdate()`
- `isAllowed` that is not a boolean
- an attribute key or value that breaks its rule, or a value that is neither a string nor `null`
- a rollback `reason` that breaks the value rule
- a channel `id` that is not a UUID

A build whose build step never ran has no resource file, and a build whose file the reader refused is the same case with the reason. Every method then rejects with one error until the app is rebuilt. The error says HotCodePush is not configured, names the missing file or the reader's reason, and points at `npx hotcodepush init` and a rebuild.

In Expo Go and on the web the package loads and every method rejects with one error, `removeAllListeners()` included, while `useUpdates()` keeps the embedded state. The error says the SDK needs a development or release build.

Guard the calls with the platform check you use for every native-only module, or catch the rejection. A development build with `expo-dev-client` is the way to run the SDK during development.
