Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions CodePush.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AcquisitionManager as Sdk, DownloadStatus } from "./lib/commonjs/acquisition-sdk/acquisition-sdk";
import { createCodePushProvider } from "./lib/commonjs/react/code-push-provider";
import { onceUntilRejected } from "./lib/commonjs/once-until-rejected";
import { Alert } from "./AlertAdapter";
import requestFetchAdapter from "./request-fetch-adapter";
import { AppState, NativeEventEmitter, Platform } from "react-native";
import log, { logFields, packageLogFields, redactUrl } from "./logging";
import log, { logFields, packageLogFields, redactUrl } from "./lib/commonjs/logging";
import hoistStatics from 'hoist-non-react-statics';

let NativeCodePush = require("react-native").NativeModules.CodePush;
Expand Down Expand Up @@ -317,11 +318,16 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
const notifyApplicationReady = onceUntilRejected(notifyApplicationReadyInternal);

async function notifyApplicationReadyInternal() {
await NativeCodePush.notifyApplicationReady();
const statusReport = await NativeCodePush.getNewStatusReport();
statusReport && tryReportStatus(statusReport); // Don't wait for this to complete.
try {
await NativeCodePush.notifyApplicationReady();
const statusReport = await NativeCodePush.getNewStatusReport();
Comment thread
ofalvai marked this conversation as resolved.
Resolved
statusReport && tryReportStatus(statusReport); // Don't wait for this to complete.

return statusReport;
return statusReport;
} catch (error) {
log.error("notifyAppReady failed.", error);
throw error;
}
}

async function tryReportStatus(statusReport, retryOnAppResume) {
Expand Down Expand Up @@ -512,7 +518,9 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
minimumBackgroundDuration: 0,
updateDialog: null,
...options
// A key set to undefined would replace its default (for example `mandatoryInstallMode: undefined`
// would turn IMMEDIATE into ON_NEXT_RESTART).
...Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined))
};

// The outcomes are logged below and in the API functions, whether or not a custom callback is passed.
Expand Down Expand Up @@ -780,3 +788,14 @@ if (NativeCodePush) {
}

module.exports = CodePush;

// Named exports, outside the legacy Object.assign namespace. In CommonJS they are still properties of the CodePush
// function, so they need the same native-module check (module.exports is undefined without it).
if (NativeCodePush) {
module.exports.CodePushProvider = createCodePushProvider({
sync: (options) => CodePush.sync(options),
notifyAppReady: () => CodePush.notifyAppReady(),
appState: AppState,
CheckFrequency: CodePush.CheckFrequency,
});
}
59 changes: 56 additions & 3 deletions docs/api-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,62 @@ The following sections describe the shape and behavior of these APIs in detail:

### JavaScript API Reference

#### CodePushProvider

```tsx
import { CodePushProvider } from "@bitrise/code-push-sdk";

<CodePushProvider {...props}>{children}</CodePushProvider>
```

The recommended way to integrate CodePush. Wrap your app's root in it:

```tsx
import * as Sentry from "@sentry/react-native";
import codePush, { CodePushProvider } from "@bitrise/code-push-sdk";

export default function App() {
return (
<CodePushProvider
installMode={codePush.InstallMode.ON_NEXT_RESUME}
onError={(error) => Sentry.captureException(error)}
>
<RootNavigator />
</CodePushProvider>
);
}
```

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.

*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.*

Props:

* __checkFrequency__ *(codePush.CheckFrequency)* - When to check for updates. Defaults to `codePush.CheckFrequency.ON_APP_START`.

* __ON_APP_START__ - Check once, after the first render.

* __ON_APP_RESUME__ - Check after the first render, and each time the app returns to the foreground.

* __MANUAL__ - Never check on its own. The Provider only calls `notifyAppReady`. Check for updates yourself, for example from a "Check for updates" button.

* __onError__ *(function(error: Error))* - Called once for each failure of the launch step or of a check on resume. Use it to send errors to a crash reporter. A failed update call that your own code makes rejects its promise instead, and does not reach `onError`. CodePush logs every failure to the console, with or without `onError`.

* __deploymentKey__, __installMode__, __mandatoryInstallMode__, __minimumBackgroundDuration__, __updateDialog__, __rollbackRetryOptions__ - The [update options](#codepushoptions), with the same defaults.

* __children__ - Your app.

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.

#### codePush

Integrate CodePush by calling [`sync`](#codepushsync) from a `useEffect` in your root component. Below are some examples of ways you can set this up (you can pick one or even use a combination):
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):

1. **Silent sync on app start** *(the simplest, default behavior)*. Your app will automatically download available updates, and apply them the next time the app restarts (like the OS or end user killed it, or the device was restarted). This way, the entire update experience is "silent" to the end user, since they don't see any update prompt and/or "synthetic" app restarts.

Expand Down Expand Up @@ -168,7 +221,7 @@ The default export, `codePush` above, is a single object. Every API described be

It provides the following methods:

* [sync](#codepushsync): Allows checking for an update, downloading it and installing it, all with a single call. Unless you need custom UI and/or behavior, we recommend most developers to call this method from a `useEffect` in their root component when integrating CodePush into their apps. See the [setup examples](#codepush) above.
* [sync](#codepushsync): Allows checking for an update, downloading it and installing it, all with a single call. [`CodePushProvider`](#codepushprovider) calls it for you. You can also call it from a `useEffect` in your root component (see the [setup examples](#codepush) above), or from a button.

* [allowRestart](#codepushallowrestart): Re-allows programmatic restarts to occur as a result of an update being installed, and optionally, immediately restarts the app if a pending update had attempted to restart the app while restarts were disallowed. This is an advanced API and is only necessary if your app explicitly disallowed restarts via the `disallowRestart` method.

Expand Down Expand Up @@ -375,7 +428,7 @@ codePush.notifyAppReady(): Promise<void>;

Notifies the CodePush runtime that a freshly installed update should be considered successful, and therefore, an automatic client-side rollback isn't necessary. It is mandatory to call this function somewhere in the code of the updated bundle. Otherwise, when the app next restarts, the CodePush runtime will assume that the installed update has failed and roll back to the previous version. This behavior exists to help ensure that your end users aren't blocked by a broken update.

If you are using the `sync` function, and doing your update check on app start, then you don't need to manually call `notifyAppReady` since `sync` will call it for you. This behavior exists due to the assumption that the point at which `sync` is called in your app represents a good approximation of a successful startup.
If you are using [`CodePushProvider`](#codepushprovider), or the `sync` function with your update check on app start, then you don't need to manually call `notifyAppReady` since they call it for you. This behavior exists due to the assumption that the point at which `sync` is called in your app represents a good approximation of a successful startup.

*NOTE: This method is also aliased as `notifyApplicationReady` (for backwards compatibility).*

Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import bitrise from '@bitrise/eslint-plugin';

const files = ['src/**/*.ts', 'test/**/*.mts'];
const files = ['src/**/*.ts', 'src/**/*.tsx', 'test/**/*.mts'];

export default [
{ ...bitrise.react, files },
Expand Down
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"test:unit:ios": "npm run test:unit:ios:codepush && npm run test:unit:ios:core",
"test:unit:ios:codepush": "xcodebuild test -project ios/CodePush.xcodeproj -scheme CodePushTests -destination \"platform=iOS Simulator,id=$(xcrun simctl list devices available | grep -m1 iPhone | grep -oE '[0-9A-F-]{36}')\"",
"test:unit:ios:core": "cd ios/CodePushCore && xcodebuild test -scheme CodePushCore -destination \"platform=iOS Simulator,id=$(xcrun simctl list devices available | grep -m1 iPhone | grep -oE '[0-9A-F-]{36}')\"",
"lint": "eslint --max-warnings 0 'src/**/*.ts' 'test/**/*.mts'",
"lint": "eslint --max-warnings 0 'src/**/*.ts' 'src/**/*.tsx' 'test/**/*.mts'",
"lint:fix": "npm run lint -- --fix"
},
"repository": {
Expand All @@ -70,13 +70,17 @@
"dependencies": {
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"react": ">=18.2.0"
},
"devDependencies": {
"@babel/register": "^7.29.7",
"@bitrise/eslint-plugin": "^4.1.0",
"@react-native/babel-preset": "^0.88.0",
"@types/mocha": "^10.0.10",
"@types/node": "^24.19.0",
"@types/q": "^1.5.4",
"@types/react": "^19.1.1",
"archiver": "latest",
"code-push-plugin-testing-framework": "file:./code-push-plugin-testing-framework",
"eslint": "^9.39.5",
Expand Down
13 changes: 6 additions & 7 deletions src/acquisition-sdk/acquisition-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Vendored from https://github.com/microsoft/code-push/blob/master/src/script/acquisition-sdk.ts (archived, MIT licensed)

import log from '../logging';
import { CodePushDeployStatusError, CodePushHttpError, CodePushPackageError } from './code-push-error';
import {
DeploymentStatusReport,
Expand Down Expand Up @@ -126,17 +127,15 @@ export class AcquisitionManager {
private isRecoverable = (statusCode: number): boolean =>
statusCode >= 500 || statusCode === 408 || statusCode === 429;

// Logs with console directly: this file is compiled to lib/ separately, and cannot import the root logging.js.
// TODO: simplify this once we adopt TypeScript properly in the entire repo.
private handleRequestFailure() {
if (
this._serverUrl.includes(this.BASE_URL_PART) &&
!this.isRecoverable(this._statusCode) &&
!AcquisitionManager._apiCallsDisabled
) {
AcquisitionManager._apiCallsDisabled = true;
console.warn(
`[CodePush] The server returned HTTP ${this._statusCode}, so all later API calls are disabled for the rest of this app session.`,
log.warn(
`The server returned HTTP ${this._statusCode}, so all later API calls are disabled for the rest of this app session.`,
);
}
}
Expand All @@ -146,7 +145,7 @@ export class AcquisitionManager {
callback?: Callback<RemotePackage | NativeUpdateNotification>,
): void {
if (AcquisitionManager._apiCallsDisabled) {
console.log(`[CodePush] API calls are disabled, skipping the update_check request.`);
log.info(`API calls are disabled, skipping the update_check request.`);
callback(/*error=*/ null, /*remotePackage=*/ null);
return;
}
Expand Down Expand Up @@ -229,7 +228,7 @@ export class AcquisitionManager {
callback?: Callback<void>,
): void {
if (AcquisitionManager._apiCallsDisabled) {
console.log(`[CodePush] API calls are disabled, skipping the report_status/deploy request.`);
log.info(`API calls are disabled, skipping the report_status/deploy request.`);
callback(/*error*/ null, /*not used*/ null);
return;
}
Expand Down Expand Up @@ -308,7 +307,7 @@ export class AcquisitionManager {

public reportStatusDownload(downloadedPackage: DownloadedPackage, callback?: Callback<void>): void {
if (AcquisitionManager._apiCallsDisabled) {
console.log(`[CodePush] API calls are disabled, skipping the report_status/download request.`);
log.info(`API calls are disabled, skipping the report_status/download request.`);
callback(/*error*/ null, /*not used*/ null);
return;
}
Expand Down
Loading
Loading