From 3bc0c58b490fcc304f0863febafa907c632564a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Fri, 30 Oct 2026 08:32:55 +0100 Subject: [PATCH] Add useCodePush() --- CodePush.js | 38 +++++ docs/api-js.md | 55 ++++++- src/react/code-push-provider.tsx | 23 ++- src/react/store-context.ts | 16 ++ src/react/use-code-push.ts | 16 ++ .../__tests__/update-state.test.ts | 150 ++++++++++++++++++ .../__tests__/update-store.test.ts | 73 +++++++++ src/update-state/update-state.ts | 57 +++++++ src/update-state/update-store.ts | 35 ++++ typings/react-native-code-push.d.ts | 58 +++++++ 10 files changed, 515 insertions(+), 6 deletions(-) create mode 100644 src/react/store-context.ts create mode 100644 src/react/use-code-push.ts create mode 100644 src/update-state/__tests__/update-state.test.ts create mode 100644 src/update-state/__tests__/update-store.test.ts create mode 100644 src/update-state/update-state.ts create mode 100644 src/update-state/update-store.ts 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 ( + +