diff --git a/CodePush.js b/CodePush.js
index 919d13a7..c48a1367 100644
--- a/CodePush.js
+++ b/CodePush.js
@@ -1,5 +1,7 @@
import { AcquisitionManager as Sdk, DownloadStatus } from "./lib/commonjs/acquisition-sdk/acquisition-sdk";
import { createCodePushProvider } from "./lib/commonjs/react/code-push-provider";
+import { useCodePush } from "./lib/commonjs/react/use-code-push";
+import { createUpdateStore } from "./lib/commonjs/update-state/update-store";
import { onceUntilRejected } from "./lib/commonjs/once-until-rejected";
import { Alert } from "./AlertAdapter";
import requestFetchAdapter from "./request-fetch-adapter";
@@ -80,7 +82,32 @@ async function downloadUpdate(remotePackage, downloadProgressCallback, reportSta
}
}
+// The state that useCodePush() returns. It lives at module level, outside React, because the legacy public functions
+// (checkForUpdate(), sync(), the codePush() HOC) must keep updating it. sync() and the HOC check through
+// checkForUpdate(), so reporting here covers every check.
+const updateStore = createUpdateStore();
+
async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
+ updateStore.dispatch({ type: "checkStarted" });
+ try {
+ // The legacy API reports a binary mismatch only through this callback (the check resolves to null), so it is
+ // wrapped to also capture it for the state.
+ let incompatibleUpdate;
+ const update = await queryUpdate(deploymentKey, (mismatch) => {
+ incompatibleUpdate = { targetBinaryVersion: mismatch.appVersion };
+ if (typeof handleBinaryVersionMismatchCallback === "function") {
+ handleBinaryVersionMismatchCallback(mismatch);
+ }
+ });
+ updateStore.dispatch({ type: "checkCompleted", update, incompatibleUpdate });
+ return update;
+ } catch (error) {
+ updateStore.dispatch({ type: "checkFailed", error });
+ throw error;
+ }
+}
+
+async function queryUpdate(deploymentKey, handleBinaryVersionMismatchCallback) {
/*
* Before we ask the server if an update exists, we
* need to retrieve three pieces of information from the
@@ -221,6 +248,7 @@ async function installUpdate(localPackage, installMode = NativeCodePush.codePush
log.info(`Installing update. ${logFields({ ...packageLogFields(localPackage), installMode: installModeName(installMode), minimumBackgroundDuration })}`);
await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
log.info(`Installed update. ${installedUpdateNextStep(installMode, minimumBackgroundDuration)}`);
+ updateStore.dispatch({ type: "updateInstalled", packageHash: localPackage.packageHash });
updateInstalledCallback && updateInstalledCallback();
if (installMode == NativeCodePush.codePushInstallModeImmediate) {
NativeCodePush.restartApp(false);
@@ -797,5 +825,15 @@ if (NativeCodePush) {
notifyAppReady: () => CodePush.notifyAppReady(),
appState: AppState,
CheckFrequency: CodePush.CheckFrequency,
+ }, {
+ store: updateStore,
+ // Narrower than the legacy functions they wrap: the hook takes no positional callbacks, and reports through the
+ // state instead.
+ actions: {
+ checkForUpdate: (options = {}) => checkForUpdate(options.deploymentKey),
+ sync: (options) => sync(options),
+ restartApp,
+ },
});
+ module.exports.useCodePush = useCodePush;
}
diff --git a/docs/api-js.md b/docs/api-js.md
index e404ecd1..f1bfbddd 100644
--- a/docs/api-js.md
+++ b/docs/api-js.md
@@ -41,7 +41,7 @@ After the app's first render, the Provider runs the launch step:
- With the default `checkFrequency`, it checks for an update, downloads and installs it according to the props. The check calls [`notifyAppReady`](#codepushnotifyappready) first.
- With `checkFrequency={codePush.CheckFrequency.MANUAL}`, it only calls [`notifyAppReady`](#codepushnotifyappready).
-Either way, CodePush keeps an update that started fine. The Provider catches every failure of the work it starts and passes it to `onError`, so no promise rejection goes unhandled. When it unmounts, it removes its `AppState` listener.
+Either way, CodePush keeps an update that started fine. The Provider catches every failure of the work it starts and passes it to `onError`, so no promise rejection goes unhandled. When it unmounts, it removes its `AppState` listener. Components inside it can read the update state with [`useCodePush`](#usecodepush).
*NOTE: Put the Provider at the app root, outside auth gates, navigation conditions, and screens that render late. The Provider calls `notifyAppReady` when it mounts. If it only mounts after login, and the user closes the app before logging in, CodePush rolls the new update back on the next start.*
@@ -63,6 +63,59 @@ Props:
If a prop changes after the first render, the next check on resume and the next `onError` call use the new value. The launch step does not run again.
+#### useCodePush
+
+```tsx
+import { useCodePush } from "@bitrise/code-push-sdk";
+
+const { isChecking, isUpdateAvailable, availableUpdate, incompatibleUpdate, checkError, checkForUpdate, sync, restartApp } = useCodePush();
+```
+
+Returns the update state, and re-renders the component when it changes. Call it in any component inside [`CodePushProvider`](#codepushprovider). It throws outside the Provider, because without the Provider nothing calls [`notifyAppReady`](#codepushnotifyappready), and every update rolls back.
+
+The state shows every update check: the Provider's, and those of [`checkForUpdate`](#codepushcheckforupdate), [`sync`](#codepushsync) and the `codePush()` higher-order component. It starts empty each time the app starts.
+
+* __isChecking__ *(Boolean)* - A check for an update runs.
+
+* __isUpdateAvailable__ *(Boolean)* - The last check found an update that did not fail to install before, and that is not installed yet.
+
+* __availableUpdate__ *([RemotePackage](#remotepackage))* - The update that the last check found, if `isUpdateAvailable`.
+
+* __incompatibleUpdate__ *({ targetBinaryVersion?: String })* - A release that the last check found, but that this binary cannot install. For example, show a banner that asks the user to update the app from the store. `targetBinaryVersion` is the semver range the release targets (e.g. `"^2.0.0"`), when that range is why it is incompatible. Check `incompatibleUpdate` itself, not `targetBinaryVersion`, to decide whether to show the banner.
+
+* __checkError__ *(Error)* - Why the last check failed. A new check clears it.
+
+* __checkForUpdate__ *(options?: { deploymentKey?: String })* - Like [`codePush.checkForUpdate`](#codepushcheckforupdate).
+
+* __sync__ *(options?: [SyncOptions](#codepushoptions))* - Like [`codePush.sync`](#codepushsync).
+
+* __restartApp__ *(onlyIfUpdateIsPending?)* - The same as [`codePush.restartApp`](#codepushrestartapp).
+
+The `sync` and `checkForUpdate` of the hook use the props of the Provider, such as `deploymentKey`, for every option that you do not pass. They take no callbacks. If you need a status or progress callback, call `codePush.sync` directly: its checks show in the state too.
+
+Example: a "Check for updates" button on a settings screen.
+
+```tsx
+import codePush, { useCodePush } from "@bitrise/code-push-sdk";
+import { Button, Text, View } from "react-native";
+
+function CheckForUpdatesButton() {
+ const { isChecking, checkError, sync } = useCodePush();
+
+ return (
+
+
+ );
+}
+```
+
#### codePush
You can also call [`sync`](#codepushsync) from a `useEffect` in your root component instead of using [`CodePushProvider`](#codepushprovider). Below are some examples of ways you can set this up (you can pick one or even use a combination):
diff --git a/src/react/code-push-provider.tsx b/src/react/code-push-provider.tsx
index 1ae56057..d0808cfd 100644
--- a/src/react/code-push-provider.tsx
+++ b/src/react/code-push-provider.tsx
@@ -1,14 +1,15 @@
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import type { CodePushProviderProps } from '../../typings/react-native-code-push';
import { AppLifecycleDeps, startAppLifecycle } from '../app-lifecycle/app-lifecycle';
+import { CodePushContextValue, StoreContext } from './store-context';
-export function createCodePushProvider(deps: AppLifecycleDeps) {
+export function createCodePushProvider(deps: AppLifecycleDeps, { store, actions }: CodePushContextValue) {
function CodePushProvider({ children, checkFrequency, onError, ...syncOptions }: CodePushProviderProps) {
const latest = useRef({ checkFrequency, onError, syncOptions });
- // Declared before the start effect, so it runs first. It runs after every render, so resume checks and onError
- // read the latest props.
+ // Declared before the start effect, so it runs first. It runs after every render, so resume checks, onError and
+ // the hook's actions read the latest props.
useEffect(() => {
latest.current = { checkFrequency, onError, syncOptions };
});
@@ -17,7 +18,19 @@ export function createCodePushProvider(deps: AppLifecycleDeps) {
// Prop changes do not run the launch step again.
useEffect(() => startAppLifecycle(deps, () => latest.current), []);
- return <>{children}>;
+ // Created once, so state changes re-render only the components that call useCodePush(). The hook's calls default
+ // to the Provider's props.
+ const [contextValue] = useState(() => ({
+ store,
+ actions: {
+ ...actions,
+ sync: (options) => actions.sync({ ...latest.current.syncOptions, ...options }),
+ checkForUpdate: (options) =>
+ actions.checkForUpdate({ deploymentKey: latest.current.syncOptions.deploymentKey, ...options }),
+ },
+ }));
+
+ return {children};
}
return CodePushProvider;
diff --git a/src/react/store-context.ts b/src/react/store-context.ts
new file mode 100644
index 00000000..658123f4
--- /dev/null
+++ b/src/react/store-context.ts
@@ -0,0 +1,16 @@
+import { createContext } from 'react';
+
+import type { UseCodePushResult } from '../../typings/react-native-code-push';
+import { UpdateStore } from '../update-state/update-store';
+
+export type UpdateActions = Pick;
+
+// The actions travel through the context, not an import, because they are implemented in the root CodePush.js, which
+// src/ cannot import.
+export interface CodePushContextValue {
+ store: Pick;
+ actions: UpdateActions;
+}
+
+export const StoreContext = createContext(null);
+StoreContext.displayName = 'CodePushContext';
diff --git a/src/react/use-code-push.ts b/src/react/use-code-push.ts
new file mode 100644
index 00000000..42edbe30
--- /dev/null
+++ b/src/react/use-code-push.ts
@@ -0,0 +1,16 @@
+import { useContext, useMemo, useSyncExternalStore } from 'react';
+
+import type { UseCodePushResult } from '../../typings/react-native-code-push';
+import { StoreContext } from './store-context';
+
+export function useCodePush(): UseCodePushResult {
+ const context = useContext(StoreContext);
+ // Without the Provider nothing calls notifyAppReady(), so every update would roll back.
+ if (!context) {
+ throw new Error('useCodePush must be used inside .');
+ }
+ const { store, actions } = context;
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
+ // A new object only when the state changes, so it is safe in dependency arrays.
+ return useMemo(() => ({ ...state, ...actions }), [state, actions]);
+}
diff --git a/src/update-state/__tests__/update-state.test.ts b/src/update-state/__tests__/update-state.test.ts
new file mode 100644
index 00000000..c1814b37
--- /dev/null
+++ b/src/update-state/__tests__/update-state.test.ts
@@ -0,0 +1,150 @@
+import * as assert from 'assert';
+
+import type { RemotePackage } from '../../../typings/react-native-code-push';
+import { initialState, reduce, StoreState, UpdateEvent } from '../update-state';
+
+function reduceAll(events: UpdateEvent[], state: StoreState = initialState): StoreState {
+ return events.reduce(reduce, state);
+}
+
+const update = { label: 'v2', packageHash: 'hash', failedInstall: false } as RemotePackage;
+const started: UpdateEvent = { type: 'checkStarted' };
+
+describe('reduce', () => {
+ it('starts idle, with no update and no error', () => {
+ assert.deepStrictEqual(initialState, {
+ runningChecks: 0,
+ snapshot: { isChecking: false, isUpdateAvailable: false },
+ });
+ });
+
+ it('checkStarted sets isChecking', () => {
+ const { snapshot } = reduce(initialState, started);
+
+ assert.strictEqual(snapshot.isChecking, true);
+ assert.strictEqual(snapshot.isUpdateAvailable, false);
+ });
+
+ it('a check that returns an update makes it available', () => {
+ const { snapshot } = reduceAll([started, { type: 'checkCompleted', update }]);
+
+ assert.strictEqual(snapshot.isChecking, false);
+ assert.strictEqual(snapshot.isUpdateAvailable, true);
+ assert.strictEqual(snapshot.availableUpdate, update);
+ assert.strictEqual(snapshot.checkError, undefined);
+ });
+
+ it('a check that finds no update clears the update of the previous check', () => {
+ const { snapshot } = reduceAll([
+ started,
+ { type: 'checkCompleted', update },
+ started,
+ { type: 'checkCompleted', update: null },
+ ]);
+
+ assert.strictEqual(snapshot.isChecking, false);
+ assert.strictEqual(snapshot.isUpdateAvailable, false);
+ assert.strictEqual(snapshot.availableUpdate, undefined);
+ });
+
+ it('an update that failed to install before is not available, as sync() ignores it', () => {
+ const { snapshot } = reduceAll([started, { type: 'checkCompleted', update: { ...update, failedInstall: true } }]);
+
+ assert.strictEqual(snapshot.isUpdateAvailable, false);
+ assert.strictEqual(snapshot.availableUpdate, undefined);
+ });
+
+ it('a check that finds an incompatible release sets incompatibleUpdate, and the next check clears it', () => {
+ const incompatibleUpdate = { targetBinaryVersion: '^2.0.0' };
+ const found = reduceAll([started, { type: 'checkCompleted', update: null, incompatibleUpdate }]);
+
+ assert.strictEqual(found.snapshot.incompatibleUpdate, incompatibleUpdate);
+ assert.strictEqual(found.snapshot.isUpdateAvailable, false);
+
+ const cleared = reduceAll([started, { type: 'checkCompleted', update: null }], found);
+ assert.strictEqual(cleared.snapshot.incompatibleUpdate, undefined);
+ });
+
+ it('a failed check keeps incompatibleUpdate', () => {
+ const incompatibleUpdate = { targetBinaryVersion: '^2.0.0' };
+ const { snapshot } = reduceAll([
+ started,
+ { type: 'checkCompleted', update: null, incompatibleUpdate },
+ started,
+ { type: 'checkFailed', error: new Error('network') },
+ ]);
+
+ assert.strictEqual(snapshot.incompatibleUpdate, incompatibleUpdate);
+ });
+
+ it('keeps the update of the previous check while a new check runs', () => {
+ const { snapshot } = reduceAll([started, { type: 'checkCompleted', update }, started]);
+
+ assert.strictEqual(snapshot.isChecking, true);
+ assert.strictEqual(snapshot.isUpdateAvailable, true);
+ assert.strictEqual(snapshot.availableUpdate, update);
+ });
+
+ it('checkFailed clears isChecking and sets checkError', () => {
+ const error = new Error('network');
+ const { snapshot } = reduceAll([started, { type: 'checkFailed', error }]);
+
+ assert.strictEqual(snapshot.isChecking, false);
+ assert.strictEqual(snapshot.checkError, error);
+ });
+
+ it('a new check clears checkError', () => {
+ const failed = reduceAll([started, { type: 'checkFailed', error: new Error('network') }]);
+
+ assert.strictEqual(reduce(failed, started).snapshot.checkError, undefined);
+ assert.strictEqual(reduceAll([started, { type: 'checkCompleted', update }], failed).snapshot.checkError, undefined);
+ });
+
+ it('isChecking stays true until every overlapping check ends', () => {
+ const twoRunning = reduceAll([started, started]);
+ assert.strictEqual(twoRunning.runningChecks, 2);
+ assert.strictEqual(twoRunning.snapshot.isChecking, true);
+
+ const oneRunning = reduce(twoRunning, { type: 'checkCompleted', update });
+ assert.strictEqual(oneRunning.snapshot.isChecking, true);
+ assert.strictEqual(oneRunning.snapshot.availableUpdate, update, 'the result of the first check shows');
+
+ const error = new Error('network');
+ const noneRunning = reduce(oneRunning, { type: 'checkFailed', error });
+ assert.strictEqual(noneRunning.runningChecks, 0);
+ assert.strictEqual(noneRunning.snapshot.isChecking, false);
+ assert.strictEqual(noneRunning.snapshot.checkError, error);
+ });
+
+ it('a second overlapping check changes only the internal count, not the snapshot', () => {
+ const one = reduce(initialState, started);
+ const two = reduce(one, started);
+
+ assert.notStrictEqual(two, one);
+ assert.strictEqual(two.snapshot, one.snapshot);
+ });
+
+ it('installing the available update clears it', () => {
+ const { snapshot } = reduceAll([
+ started,
+ { type: 'checkCompleted', update },
+ { type: 'updateInstalled', packageHash: 'hash' },
+ ]);
+
+ assert.strictEqual(snapshot.isUpdateAvailable, false);
+ assert.strictEqual(snapshot.availableUpdate, undefined);
+ });
+
+ it('installing another update keeps the available one', () => {
+ const found = reduceAll([started, { type: 'checkCompleted', update }]);
+
+ assert.strictEqual(reduce(found, { type: 'updateInstalled', packageHash: 'other' }), found);
+ });
+
+ it('does not change the state it is given', () => {
+ const before = JSON.parse(JSON.stringify(initialState));
+ reduceAll([started, { type: 'checkCompleted', update }]);
+
+ assert.deepStrictEqual(initialState, before);
+ });
+});
diff --git a/src/update-state/__tests__/update-store.test.ts b/src/update-state/__tests__/update-store.test.ts
new file mode 100644
index 00000000..bec9f176
--- /dev/null
+++ b/src/update-state/__tests__/update-store.test.ts
@@ -0,0 +1,73 @@
+import * as assert from 'assert';
+
+import { createUpdateStore } from '../update-store';
+
+describe('createUpdateStore', () => {
+ it('notifies subscribers of each change, until they unsubscribe', () => {
+ const store = createUpdateStore();
+ let calls = 0;
+ const unsubscribe = store.subscribe(() => calls++);
+
+ store.dispatch({ type: 'checkStarted' });
+ assert.strictEqual(calls, 1);
+
+ unsubscribe();
+ store.dispatch({ type: 'checkCompleted', update: null });
+ assert.strictEqual(calls, 1);
+ assert.strictEqual(store.getSnapshot().isChecking, false);
+ });
+
+ it('changes the snapshot identity only when the snapshot changes', () => {
+ const store = createUpdateStore();
+ let calls = 0;
+ store.subscribe(() => calls++);
+ const initial = store.getSnapshot();
+
+ store.dispatch({ type: 'checkStarted' });
+ const checking = store.getSnapshot();
+ assert.notStrictEqual(checking, initial);
+ assert.strictEqual(store.getSnapshot(), checking, 'the same object until the next change');
+
+ // A second overlapping check only changes the internal count.
+ store.dispatch({ type: 'checkStarted' });
+ assert.strictEqual(store.getSnapshot(), checking);
+ assert.strictEqual(calls, 1);
+
+ store.dispatch({ type: 'checkCompleted', update: null });
+ assert.strictEqual(store.getSnapshot(), checking, 'still checking: one check runs');
+ store.dispatch({ type: 'checkCompleted', update: null });
+ assert.strictEqual(store.getSnapshot().isChecking, false);
+ assert.strictEqual(calls, 2);
+ });
+
+ it('works with unbound functions, as useSyncExternalStore calls them', () => {
+ const { getSnapshot, subscribe, dispatch } = createUpdateStore();
+ let calls = 0;
+ subscribe(() => calls++);
+
+ dispatch({ type: 'checkStarted' });
+ assert.strictEqual(getSnapshot().isChecking, true);
+ assert.strictEqual(calls, 1);
+ });
+
+ it('notifies every subscriber when one unsubscribes during the notification', () => {
+ const store = createUpdateStore();
+ const calls: string[] = [];
+ const unsubscribeFirst = store.subscribe(() => {
+ calls.push('first');
+ unsubscribeFirst();
+ });
+ store.subscribe(() => calls.push('second'));
+
+ store.dispatch({ type: 'checkStarted' });
+ assert.deepStrictEqual(calls, ['first', 'second']);
+ });
+
+ it('keeps a separate state for each store', () => {
+ const first = createUpdateStore();
+ const second = createUpdateStore();
+
+ first.dispatch({ type: 'checkStarted' });
+ assert.strictEqual(second.getSnapshot().isChecking, false);
+ });
+});
diff --git a/src/update-state/update-state.ts b/src/update-state/update-state.ts
new file mode 100644
index 00000000..98c249a5
--- /dev/null
+++ b/src/update-state/update-state.ts
@@ -0,0 +1,57 @@
+import type { CodePushState, IncompatibleUpdate, RemotePackage } from '../../typings/react-native-code-push';
+
+export type UpdateEvent =
+ | { type: 'checkStarted' }
+ | { type: 'checkCompleted'; update: RemotePackage | null; incompatibleUpdate?: IncompatibleUpdate }
+ | { type: 'checkFailed'; error: Error }
+ | { type: 'updateInstalled'; packageHash: string };
+
+export interface StoreState {
+ /** A button check can overlap a resume sync, so `isChecking` stays true until all end. */
+ runningChecks: number;
+ snapshot: CodePushState;
+}
+
+export const initialState: StoreState = {
+ runningChecks: 0,
+ snapshot: {
+ isChecking: false,
+ isUpdateAvailable: false,
+ },
+};
+
+export function reduce(state: StoreState, event: UpdateEvent): StoreState {
+ switch (event.type) {
+ case 'checkStarted':
+ // The result of the previous check stays until this one completes.
+ return next(state, state.runningChecks + 1, { checkError: undefined });
+ case 'checkCompleted': {
+ // sync() skips a previously rolled-back update by default, so it does not count as available.
+ const update = event.update && !event.update.failedInstall ? event.update : undefined;
+ return next(state, state.runningChecks - 1, {
+ isUpdateAvailable: !!update,
+ availableUpdate: update,
+ incompatibleUpdate: event.incompatibleUpdate,
+ checkError: undefined,
+ });
+ }
+ case 'checkFailed':
+ return next(state, state.runningChecks - 1, { checkError: event.error });
+ case 'updateInstalled':
+ // Only the update it installed: a newer check may have found another one meanwhile.
+ if (state.snapshot.availableUpdate?.packageHash !== event.packageHash) {
+ return state;
+ }
+ return next(state, state.runningChecks, { isUpdateAvailable: false, availableUpdate: undefined });
+ }
+}
+
+function next(state: StoreState, runningChecks: number, changes: Partial): StoreState {
+ return { runningChecks, snapshot: updateSnapshot(state.snapshot, { ...changes, isChecking: runningChecks > 0 }) };
+}
+
+/** Keeps the snapshot identity when no value changes, so subscribers are not notified. */
+function updateSnapshot(snapshot: CodePushState, changes: Partial): CodePushState {
+ const changed = (Object.keys(changes) as Array).some((key) => changes[key] !== snapshot[key]);
+ return changed ? { ...snapshot, ...changes } : snapshot;
+}
diff --git a/src/update-state/update-store.ts b/src/update-state/update-store.ts
new file mode 100644
index 00000000..1226cee4
--- /dev/null
+++ b/src/update-state/update-store.ts
@@ -0,0 +1,35 @@
+import type { CodePushState } from '../../typings/react-native-code-push';
+import { initialState, reduce, UpdateEvent } from './update-state';
+
+// An external store, not React state, because the legacy non-React functions in CodePush.js also update it.
+// getSnapshot() and subscribe() have the shape that React's useSyncExternalStore expects.
+export interface UpdateStore {
+ getSnapshot(): CodePushState;
+ subscribe(listener: () => void): () => void;
+ dispatch(event: UpdateEvent): void;
+}
+
+export function createUpdateStore(): UpdateStore {
+ let state = initialState;
+ const listeners = new Set<() => void>();
+
+ // Closures, not methods: useSyncExternalStore calls them unbound.
+ return {
+ getSnapshot: () => state.snapshot,
+ subscribe: (listener) => {
+ listeners.add(listener);
+ return () => {
+ listeners.delete(listener);
+ };
+ },
+ dispatch: (event) => {
+ const previous = state;
+ state = reduce(state, event);
+ if (state.snapshot === previous.snapshot) {
+ return;
+ }
+ // A copy, so a listener that unsubscribes during the loop does not skip the next one.
+ Array.from(listeners).forEach((listener) => listener());
+ },
+ };
+}
diff --git a/typings/react-native-code-push.d.ts b/typings/react-native-code-push.d.ts
index 6cdac242..a68b191f 100644
--- a/typings/react-native-code-push.d.ts
+++ b/typings/react-native-code-push.d.ts
@@ -494,4 +494,62 @@ export interface CodePushProviderProps extends SyncOptions {
*/
export function CodePushProvider(props: CodePushProviderProps): React.ReactElement | null;
+export interface CodePushState {
+ /**
+ * Whether an update check is currently running.
+ */
+ isChecking: boolean;
+
+ /**
+ * Whether an update is available.
+ * Note: updates that previously failed to install are not considered here, and an update stops being available
+ * once it is installed.
+ */
+ isUpdateAvailable: boolean;
+
+ availableUpdate?: RemotePackage;
+
+ /**
+ * A release that the last check found but this binary cannot install.
+ * Can be used to show a banner that asks the user to update from the store.
+ */
+ incompatibleUpdate?: IncompatibleUpdate;
+
+ /**
+ * Why the last check failed. A new check clears it.
+ */
+ checkError?: Error;
+}
+
+export interface IncompatibleUpdate {
+ /**
+ * The binary versions the release targets, as a semver range (e.g. "^2.0.0"), when the release is incompatible
+ * because of that range. Optional, so other reasons for incompatibility can be added without a breaking change.
+ */
+ targetBinaryVersion?: string;
+}
+
+export interface UseCodePushResult extends CodePushState {
+ /**
+ * Asks the CodePush service whether the configured app deployment has an update available.
+ *
+ * @param options.deploymentKey Overrides the deployment key configured in the native app.
+ */
+ checkForUpdate: (options?: { deploymentKey?: string }) => Promise;
+
+ /**
+ * Checks for an update, downloads it and installs it, all with a single call.
+ *
+ * @param options Configures the end-user update experience (e.g. show a prompt?, install the update immediately?).
+ */
+ sync: (options?: SyncOptions) => Promise;
+
+ restartApp: typeof CodePush.restartApp;
+}
+
+/**
+ * Returns the update state, and re-renders the component when it changes. Throws outside `CodePushProvider`.
+ */
+export function useCodePush(): UseCodePushResult;
+
export default CodePush;