Checking for updates
Checking for updates
sync() is the one call most apps need on the client. It checks the server, downloads an update if one matches this deployment key and binary version, installs it, and confirms the running bundle is healthy, all in one invocation.
This page covers when and how to check and download. For when a downloaded update becomes active, see Applying updates.
What sync() does
On each call, in order:
notifyAppReady(): marks the currently running bundle as healthy. If a pending bundle repeatedly starts without reachingnotifyAppReady(), the SDK rolls it back once its launch-attempt budget is exhausted. Becausesync()calls this first, running it on every startup confirms the previous update and arms rollback for the next one.- Check the server for a matching update.
- Download the new bundle (or a binary patch, with automatic fallback to the full bundle).
- Install according to the install mode you choose (see Applying updates).
sync() never throws; it resolves to a SyncStatus: "up-to-date", "update-installed", "embedded-revert-applied", "sync-in-progress", or "error". In React Native development builds an "error" result is also logged as a warning with its reason (an unreachable server, a failed download or install). A wrong deployment key or path is not an error: the server answers 404, which the SDK treats as "no update", so silence there does not confirm the configuration. With no options, non-mandatory releases wait for the next cold start (installMode); mandatory releases apply immediately (mandatoryInstallMode).
Minimal integration
- React Native
- Capacitor
Wrap your root component with Patch.wrap(App) (cmpatch wire adds this for you). Your app renders immediately, and the wrapper calls sync() after the root mounts:
// App.tsx
import * as Patch from "@codemagic/react-native-patch";
function App() {
return <YourApp />;
}
export default Patch.wrap(App);
Pass WrapOptions as the second argument to customize install timing, for example Patch.wrap(App, { installMode: Patch.InstallMode.ON_NEXT_RESUME }). With no options, ordinary updates install on the next restart and mandatory updates install immediately. Set checkFrequency: "ON_APP_RESUME" to also check on foreground return. The default "ON_APP_START" checks only on mount. See the wrapper reference for API details.
If your app must finish asynchronous initialization before confirming readiness, call sync() after it succeeds instead of using wrap(); see the readiness guidance.
Call start() after your app renders its first screen. start() runs sync() and is the Capacitor equivalent of Patch.wrap(App) in React Native:
import { start } from "@codemagic/capacitor-patch";
start();
start() marks the running bundle as healthy, so call it after the first screen renders rather than early in app startup. A crash before start() counts toward rolling back a bad update; a crash after it does not. In Angular, call it from ngAfterViewInit() of AppComponent or the first page. In React or Vue, call it from the root component's mount hook. If the app needs asynchronous initialization before it is usable, call start() after that completes.
Pass StartOptions to change install timing, for example start({ installMode: InstallMode.ON_NEXT_RESUME }). By default, regular updates install on the next restart and mandatory updates install immediately. To also check when the app returns to the foreground, set checkFrequency: CheckFrequency.ON_APP_RESUME; the default, ON_APP_START, checks once. start() never throws and resolves to the SyncStatus of the first check. See the start() reference.
For step-by-step control (checkForUpdate, downloadUpdate, installUpdate), see Manual control. For rollouts and mandatory flags on the server, see Production control.
When to check for updates
sync() is safe to call more than once. Concurrent calls resolve to "sync-in-progress".
| Trigger | Typical use |
|---|---|
| App launch | Catch updates published while the app was closed |
| Return to foreground | Catch updates published while the user had the app open |
| User action | "Check for updates" in settings, after login, etc. |
To check on launch and foreground return:
- React Native
- Capacitor
export default Patch.wrap(App, {
checkFrequency: Patch.CheckFrequency.ON_APP_RESUME,
installMode: Patch.InstallMode.ON_NEXT_RESTART,
});
For manual timing or asynchronous readiness, skip the wrapper and call sync() from your own lifecycle code.
import { CheckFrequency, InstallMode, start } from "@codemagic/capacitor-patch";
start({
checkFrequency: CheckFrequency.ON_APP_RESUME,
installMode: InstallMode.ON_NEXT_RESTART,
});
The plugin detects foreground transitions natively, so you do not need @capacitor/app or another plugin. To check at other times, call sync() from your own code.
Combine with install modes from Applying updates so resume-based installs do not surprise users mid-flow.
Download progress and status
- React Native
- Capacitor
For progress UI, remove Patch.wrap() and call sync() from your chosen lifecycle or button handler with the optional progress callback. Keep startup readiness confirmation as described in Manual control. A second sync() cannot observe the wrapper's in-flight operation: it returns "sync-in-progress" without attaching its callback.
Pass your install options and progress callback to the direct call:
import { InstallMode, sync } from "@codemagic/react-native-patch";
const result = await sync(
{
installMode: InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: InstallMode.IMMEDIATE,
},
({ receivedBytes, totalBytes }) => {
const pct = totalBytes > 0 ? receivedBytes / totalBytes : 0;
// drive UI
},
);
start() does not accept a progress callback. For progress UI, call sync() with a progress callback from a lifecycle hook or button handler. If a sync is already running (for example, one started by start()), the call returns "sync-in-progress" and the callback is not attached. If you remove start(), confirm readiness on startup as described in Manual control.
import { InstallMode, sync } from "@codemagic/capacitor-patch";
const result = await sync(
{
installMode: InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: InstallMode.IMMEDIATE,
},
({ receivedBytes, totalBytes }) => {
const pct = totalBytes > 0 ? receivedBytes / totalBytes : 0;
// drive UI
},
);
With manual control, the same { receivedBytes, totalBytes } shape is passed to downloadUpdate().
Use sync() result statuses ("update-installed", "up-to-date", "error") to decide whether to show messaging after a check completes.
Release notes from the server
Each OTA release can carry release notes (CLI --release-notes, dashboard, or CI). On the client, RemotePackage.releaseNotes is populated when checkForUpdate() returns { action: "ota-update" }.
Patch does not ship a built-in update dialog. Show notes in your own UI before calling installUpdate, or after sync() returns "update-installed" for deferred install modes. Changing notes on the server does not require a new native binary; republish or patch the release metadata.
Binary version mismatch and store updates
The server targets updates with targetBinaryVersion. When the installed native binary is too old for the latest OTA on a deployment, checkForUpdate() (and therefore sync()) may return { action: "up-to-date" } even though newer store builds exist.
Every check result includes:
| Field | Meaning |
|---|---|
isStoreUpdateAvailable | Native binary on device is below the server's latest known store version |
latestBinaryVersion | Latest binary version the server knows for this app (from release metadata) |
Use these to prompt a store update instead of silently doing nothing:
const check = await checkForUpdate();
if (check.isStoreUpdateAvailable && check.latestBinaryVersion) {
// Show "Update from the App Store / Play Store", your UX
}
if (check.action === "ota-update") {
// proceed with download / install
}
Server-side targeting is covered in Production control.
Next steps
- Applying updates: install modes, staging vs production behavior, mandatory apply timing
- Manual control: button-driven check, download, and apply flows