@novacraft-engineering/pulsar-code-push 1.5.5 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CodePush.js CHANGED
@@ -9,6 +9,12 @@ import { setTargetingInfo, getTargetingInfo } from "./targeting";
9
9
  let NativeCodePush = require("react-native").NativeModules.CodePush;
10
10
  const PackageMixins = require("./package-mixins")(NativeCodePush);
11
11
 
12
+ // Apps that choose their deployment key in JS (per environment) leave the native one empty, so the
13
+ // launch-time status report has no key. Fall back to the first key the app checks with (its primary
14
+ // backend) and send a report that was held for want of one as soon as that key is known.
15
+ let fallbackDeployment = null;
16
+ let reportAwaitingKey = null;
17
+
12
18
  async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null, serverUrl = null) {
13
19
  /*
14
20
  * Before we ask the server if an update exists, we
@@ -39,6 +45,14 @@ async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchC
39
45
  ...(deploymentKey ? { deploymentKey } : {}),
40
46
  ...(serverUrl ? { serverUrl } : {}),
41
47
  };
48
+ if (config.deploymentKey && !fallbackDeployment) {
49
+ fallbackDeployment = { deploymentKey: config.deploymentKey, serverUrl: config.serverUrl };
50
+ if (reportAwaitingKey) {
51
+ const heldReport = reportAwaitingKey;
52
+ reportAwaitingKey = null;
53
+ tryReportStatus(heldReport);
54
+ }
55
+ }
42
56
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);
43
57
 
44
58
  // Use dynamically overridden getCurrentPackage() during tests.
@@ -193,7 +207,8 @@ async function notifyApplicationReadyInternal() {
193
207
  }
194
208
 
195
209
  async function tryReportStatus(statusReport, retryOnAppResume) {
196
- const config = await getConfiguration();
210
+ const nativeConfig = await getConfiguration();
211
+ const config = nativeConfig.deploymentKey || !fallbackDeployment ? nativeConfig : { ...nativeConfig, ...fallbackDeployment };
197
212
  const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;
198
213
  const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;
199
214
  try {
@@ -201,6 +216,7 @@ async function tryReportStatus(statusReport, retryOnAppResume) {
201
216
  log(`Reporting binary update (${statusReport.appVersion})`);
202
217
 
203
218
  if (!config.deploymentKey) {
219
+ reportAwaitingKey = statusReport;
204
220
  throw new Error("Deployment key is missed");
205
221
  }
206
222
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @novacraft-engineering/pulsar-code-push
2
2
 
3
- The React Native client for [Pulsar](https://pulsar.novacraft.africa) — over-the-air JavaScript updates for your app, no app-store review.
3
+ The React Native client for [Pulsar](https://usepulsar.dev) — **over-the-air JavaScript updates** for your app, no app-store review. A free, self-hostable **CodePush alternative** for React Native.
4
4
 
5
5
  This is a full CodePush client (native iOS + Android modules), not a wrapper. It is a fork of
6
6
  [`@revopush/react-native-code-push`](https://github.com/revopush/react-native-code-push) — itself the
@@ -8,6 +8,13 @@ New-Architecture-maintained continuation of Microsoft's original
8
8
  [`react-native-code-push`](https://github.com/microsoft/react-native-code-push) (archived March 2025).
9
9
  It speaks the CodePush protocol, so it works against any CodePush-compatible server; it defaults to Pulsar.
10
10
 
11
+ ## Why
12
+
13
+ Microsoft App Center CodePush shut down in March 2025 and Expo's EAS Update only covers Expo-managed
14
+ projects. Pulsar keeps the CodePush protocol every bare React Native app already knows, adds staged
15
+ rollouts, instant rollback, differential updates and per-release health metrics, and can be self-hosted.
16
+ Read the full comparison at [usepulsar.dev/codepush-alternative](https://usepulsar.dev/codepush-alternative).
17
+
11
18
  ## Install
12
19
 
13
20
  ```bash
@@ -25,7 +32,7 @@ Autolinking registers the native module on both platforms (RN 0.70+, New Archite
25
32
  <key>CodePushDeploymentKey</key>
26
33
  <string>YOUR_DEPLOYMENT_KEY</string>
27
34
  <key>CodePushServerURL</key>
28
- <string>https://api.pulsar.novacraft.africa</string>
35
+ <string>https://api.usepulsar.dev</string>
29
36
  ```
30
37
 
31
38
  **Android** — `android/app/src/main/res/values/strings.xml`:
@@ -33,7 +40,7 @@ Autolinking registers the native module on both platforms (RN 0.70+, New Archite
33
40
  ```xml
34
41
  <resources>
35
42
  <string moduleConfig="true" name="CodePushDeploymentKey">YOUR_DEPLOYMENT_KEY</string>
36
- <string moduleConfig="true" name="CodePushServerURL">https://api.pulsar.novacraft.africa</string>
43
+ <string moduleConfig="true" name="CodePushServerURL">https://api.usepulsar.dev</string>
37
44
  </resources>
38
45
  ```
39
46
 
@@ -56,14 +63,31 @@ The full CodePush API is available — `codePush.sync()`, `codePush.checkForUpda
56
63
  `InstallMode`, `CheckFrequency`, `notifyAppReady()`, rollout %, mandatory updates, binary-version
57
64
  targeting, differential updates, automatic rollback, and download/install telemetry (reported to Pulsar).
58
65
 
66
+ ## Migrating from CodePush or RevoPush
67
+
68
+ Swap the import and point `CodePushServerURL` at Pulsar — the native modules, the JS API and the
69
+ package-hash algorithm are unchanged, so devices already running your current bundle correctly
70
+ report "up to date" instead of re-downloading it.
71
+
72
+ ```diff
73
+ -import codePush from 'react-native-code-push';
74
+ +import codePush from '@novacraft-engineering/pulsar-code-push';
75
+ ```
76
+
59
77
  ## Releasing
60
78
 
61
79
  Use the Pulsar CLI ([`@novacraft-engineering/pulsar-cli`](https://github.com/Novacraft-Africa/pulsar-cli)):
62
80
 
63
81
  ```bash
64
- npx pulsar release MyApp Staging --platform ios --target 1.0.0
82
+ npx @novacraft-engineering/pulsar-cli release MyApp Staging --platform ios --target 1.0.0
65
83
  ```
66
84
 
85
+ ## Related
86
+
87
+ - [Pulsar vs CodePush vs RevoPush](https://usepulsar.dev/blog/codepush-vs-revopush-vs-pulsar)
88
+ - [Microsoft CodePush shutdown: what to move to](https://usepulsar.dev/blog/microsoft-codepush-shutdown-alternatives)
89
+ - [How React Native OTA updates actually work](https://usepulsar.dev/blog/how-react-native-ota-updates-work)
90
+
67
91
  ## License
68
92
 
69
93
  MIT. Original CodePush © Microsoft; RevoPush fork © Revopush; this Pulsar distribution © Novacraft Engineering.
@@ -570,19 +570,11 @@ static NSString *const LatestRollbackCountKey = @"count";
570
570
  } else {
571
571
  CPLog(@"Attempted to perform a rollback when there is no current update");
572
572
  }
573
- // Nothing to roll back to. This happens when a pending update was
574
- // recorded before a binary update: bundleURL clears the packages
575
- // because the app version changed, but CODE_PUSH_PENDING_UPDATE lives
576
- // in NSUserDefaults and survives. Reloading here re-enters this same
577
- // path on the new bridge, so the app reload-loops on a blank screen
578
- // until it is killed. Drop the stale flag and keep the loaded bundle.
579
- [CodePush removePendingUpdate];
580
- return;
573
+ } else {
574
+ // Write the current package's metadata to the "failed list"
575
+ [self saveFailedUpdate:failedPackage];
581
576
  }
582
577
 
583
- // Write the current package's metadata to the "failed list"
584
- [self saveFailedUpdate:failedPackage];
585
-
586
578
  // Rollback to the previous version and de-register the new update
587
579
  [CodePushPackage rollbackPackage];
588
580
  [CodePush removePendingUpdate];
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "@novacraft-engineering/pulsar-code-push",
3
- "version": "1.5.5",
3
+ "version": "1.6.1",
4
4
  "description": "Pulsar OTA client for React Native — over-the-air JavaScript updates. A CodePush-compatible client (RevoPush lineage) pointed at Pulsar.",
5
5
  "main": "CodePush.js",
6
6
  "typings": "typings/react-native-code-push.d.ts",
7
- "homepage": "https://pulsar.novacraft.africa",
7
+ "homepage": "https://usepulsar.dev",
8
8
  "keywords": [
9
- "react-native",
10
9
  "code-push",
11
10
  "codepush",
12
- "pulsar",
11
+ "codepush-alternative",
12
+ "hot-update",
13
13
  "ota",
14
+ "ota-updates",
14
15
  "over-the-air",
15
- "hot-update"
16
+ "pulsar",
17
+ "react-native",
18
+ "react-native-ota"
16
19
  ],
17
20
  "author": "Novacraft Engineering",
18
21
  "license": "MIT",
@@ -1,6 +1,17 @@
1
+ const { Platform } = require("react-native");
1
2
  const packageJson = require("./package.json");
2
3
  const { getTargetingInfo } = require("./targeting");
3
4
 
5
+ // React Native's default agent carries no usable platform detail — iOS sends a Darwin/CFNetwork
6
+ // string with no "iPhone OS x_y", and Android goes out through OkHttp as bare "okhttp/4.x". Both
7
+ // left the server guessing (Android was filed as "unknown" entirely, OS version was always empty),
8
+ // so state it explicitly instead.
9
+ function userAgent() {
10
+ const os = Platform.OS === "ios" ? "iOS" : Platform.OS === "android" ? "Android" : Platform.OS;
11
+ const version = Platform.Version === undefined || Platform.Version === null ? "" : String(Platform.Version);
12
+ return `Pulsar/${packageJson.version} (${os}${version ? " " + version : ""})`;
13
+ }
14
+
4
15
  module.exports = {
5
16
  async request(verb, url, requestBody, callback) {
6
17
  if (typeof requestBody === "function") {
@@ -9,18 +20,32 @@ module.exports = {
9
20
  }
10
21
 
11
22
  // Scope the update-check to a specific device model / user id when the host app has set them.
12
- // Only the GET update_check request is augmented; report_status is untouched.
23
+ const { userId, deviceModel } = getTargetingInfo();
13
24
  if (verb === 0 && url.indexOf("update_check") !== -1) {
14
- const { userId, deviceModel } = getTargetingInfo();
15
25
  const extra = [];
16
26
  if (deviceModel) extra.push("device_model=" + encodeURIComponent(deviceModel));
17
27
  if (userId) extra.push("user_id=" + encodeURIComponent(userId));
18
28
  if (extra.length) url += (url.indexOf("?") === -1 ? "?" : "&") + extra.join("&");
19
29
  }
20
30
 
31
+ // Status reports carry the same identity. update_check only records a device when an update is
32
+ // actually served, so without this the device/user targeting lists never fill on a quiet app.
33
+ // The acquisition SDK hands us an already-stringified body here, so parse before augmenting.
34
+ if (url.indexOf("report_status") !== -1 && requestBody && (deviceModel || userId)) {
35
+ try {
36
+ const parsed = typeof requestBody === "string" ? JSON.parse(requestBody) : requestBody;
37
+ if (deviceModel && !parsed.device_model) parsed.device_model = deviceModel;
38
+ if (userId && !parsed.user_id) parsed.user_id = userId;
39
+ requestBody = parsed;
40
+ } catch (err) {
41
+ // A body we can't parse is left exactly as-is — reporting must never fail over telemetry.
42
+ }
43
+ }
44
+
21
45
  const headers = {
22
46
  "Accept": "application/json",
23
47
  "Content-Type": "application/json",
48
+ "User-Agent": userAgent(),
24
49
  "X-CodePush-Plugin-Name": packageJson.name,
25
50
  "X-CodePush-Plugin-Version": packageJson.version,
26
51
  "X-CodePush-SDK-Version": packageJson.dependencies["code-push"]