react-native-bspatch 4.1.0 → 5.0.0

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.
Files changed (81) hide show
  1. package/README.md +83 -27
  2. package/android/build.gradle +25 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +86 -6
  5. package/android/src/main/res/values/bspatch_public_key.xml +5 -0
  6. package/cpp/bsdiff/bspatch_wrapper.c +34 -0
  7. package/cpp/bsdiff/bspatch_wrapper.h +2 -0
  8. package/cpp/bspatch_jni.cpp +18 -0
  9. package/ios/RNBsPatch.mm +142 -1
  10. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  11. package/lib/commonjs/index.js +9 -2
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/ota/OtaProvider.js +116 -20
  14. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  15. package/lib/commonjs/ota/eligibility.js +40 -0
  16. package/lib/commonjs/ota/eligibility.js.map +1 -0
  17. package/lib/commonjs/ota/entrySignature.js +23 -0
  18. package/lib/commonjs/ota/entrySignature.js.map +1 -0
  19. package/lib/commonjs/ota/installId.js +27 -0
  20. package/lib/commonjs/ota/installId.js.map +1 -0
  21. package/lib/commonjs/ota/otaCore.js +96 -21
  22. package/lib/commonjs/ota/otaCore.js.map +1 -1
  23. package/lib/commonjs/ota/storage/{GitHubRawStorage.js → R2Storage.js} +20 -15
  24. package/lib/commonjs/ota/storage/R2Storage.js.map +1 -0
  25. package/lib/commonjs/ota/types.js.map +1 -1
  26. package/lib/module/NativeBsPatch.js.map +1 -1
  27. package/lib/module/index.js +2 -1
  28. package/lib/module/index.js.map +1 -1
  29. package/lib/module/ota/OtaProvider.js +116 -20
  30. package/lib/module/ota/OtaProvider.js.map +1 -1
  31. package/lib/module/ota/eligibility.js +34 -0
  32. package/lib/module/ota/eligibility.js.map +1 -0
  33. package/lib/module/ota/entrySignature.js +17 -0
  34. package/lib/module/ota/entrySignature.js.map +1 -0
  35. package/lib/module/ota/installId.js +22 -0
  36. package/lib/module/ota/installId.js.map +1 -0
  37. package/lib/module/ota/otaCore.js +96 -21
  38. package/lib/module/ota/otaCore.js.map +1 -1
  39. package/lib/module/ota/storage/{GitHubRawStorage.js → R2Storage.js} +18 -14
  40. package/lib/module/ota/storage/R2Storage.js.map +1 -0
  41. package/lib/module/ota/types.js.map +1 -1
  42. package/lib/typescript/NativeBsPatch.d.ts +28 -2
  43. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  44. package/lib/typescript/index.d.ts +3 -1
  45. package/lib/typescript/index.d.ts.map +1 -1
  46. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  47. package/lib/typescript/ota/eligibility.d.ts +19 -0
  48. package/lib/typescript/ota/eligibility.d.ts.map +1 -0
  49. package/lib/typescript/ota/entrySignature.d.ts +9 -0
  50. package/lib/typescript/ota/entrySignature.d.ts.map +1 -0
  51. package/lib/typescript/ota/installId.d.ts +3 -0
  52. package/lib/typescript/ota/installId.d.ts.map +1 -0
  53. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  54. package/lib/typescript/ota/storage/{GitHubRawStorage.d.ts → R2Storage.d.ts} +4 -3
  55. package/lib/typescript/ota/storage/R2Storage.d.ts.map +1 -0
  56. package/lib/typescript/ota/types.d.ts +52 -5
  57. package/lib/typescript/ota/types.d.ts.map +1 -1
  58. package/package.json +3 -2
  59. package/react-native-bspatch.podspec +3 -1
  60. package/src/NativeBsPatch.ts +34 -2
  61. package/src/index.ts +5 -1
  62. package/src/ota/OtaProvider.tsx +115 -24
  63. package/src/ota/eligibility.ts +48 -0
  64. package/src/ota/entrySignature.ts +31 -0
  65. package/src/ota/installId.ts +18 -0
  66. package/src/ota/otaCore.ts +102 -21
  67. package/src/ota/storage/{GitHubRawStorage.ts → R2Storage.ts} +17 -13
  68. package/src/ota/types.ts +49 -5
  69. package/tools/build_ota.sh +343 -116
  70. package/tools/doctor.js +130 -0
  71. package/tools/ensure_signing_key.js +216 -0
  72. package/tools/fingerprint.js +111 -0
  73. package/tools/ios_fingerprint.sh +9 -0
  74. package/tools/ota_admin.js +131 -0
  75. package/tools/setup_project.js +41 -15
  76. package/tools/update_registry.js +111 -11
  77. package/tools/upload_ota.sh +85 -184
  78. package/tools/verify_publish.js +81 -0
  79. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +0 -1
  80. package/lib/module/ota/storage/GitHubRawStorage.js.map +0 -1
  81. package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"R2Storage.d.ts","sourceRoot":"","sources":["../../../../src/ota/storage/R2Storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAmBxE,uFAAuF;AACvF,eAAO,MAAM,SAAS,GAClB,KAAK,MAAM,EACX,WAAW,MAAM,EACjB,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACjC,OAAO,CAAC,GAAG,CAYb,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAKlD,CAAC;AAEF,qBAAa,iBAAkB,YAAW,kBAAkB;IAClD,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAiCrD,gBAAgB,CAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,GAClB,OAAO,CAAC,MAAM,CAAC;IAeZ,WAAW,CACb,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAClB,OAAO,CAAC,MAAM,CAAC;CAWrB"}
@@ -43,16 +43,38 @@ export type OtaPatch = {
43
43
  minAppVersion?: string;
44
44
  /** Full bundle file: used when there is no OTA bundle to patch from, or the patch fails */
45
45
  fullBundleUrl?: string;
46
+ /** Size of the full bundle in bytes: download progress when it arrives Content-Encoded */
47
+ bundleSize?: number;
46
48
  /** RSA/ECDSA SHA-256 signature (base64) of the patch file */
47
49
  signature?: string;
48
50
  /** RSA/ECDSA SHA-256 signature (base64) of the resulting bundle */
49
51
  bundleSignature?: string;
50
52
  /** URL to the asset manifest JSON for this version */
51
53
  assetManifestUrl?: string;
54
+ /** SHA-256 of the asset manifest file (covered by entrySignature) */
55
+ assetManifestHash?: string;
56
+ /** Signature (base64) of entryPayload(entry): required when a public key is set */
57
+ entrySignature?: string;
52
58
  /** Target platform for this patch */
53
59
  platform?: string;
54
60
  /** Flavor/channel for this patch */
55
61
  flavor?: string;
62
+ /** How fullBundleUrl is compressed ('gzip' | 'bz2'); absent = raw .hbc */
63
+ fullBundleCompression?: string;
64
+ /** Native runtime fingerprint the bundle was built against: installs only on a matching binary */
65
+ runtimeFingerprint?: string;
66
+ /** Hermes bytecode version of the bundle: installs only on a runtime that reads it */
67
+ hermesBytecodeVersion?: number;
68
+ /** Client features this entry needs; a client missing one skips the entry */
69
+ requires?: string[];
70
+ /** Kill switch: no client installs this version */
71
+ disabled?: boolean;
72
+ /** Staged rollout: share of installs (0-100) that get this version, by a stable per-install bucket */
73
+ rolloutPercent?: number;
74
+ /** QA: only these install IDs (useOta().installId) get this version */
75
+ allowInstallIds?: string[];
76
+ /** Apply as soon as it's ready instead of on the next launch (BACKGROUND_DOWNLOAD) */
77
+ mandatory?: boolean;
56
78
  };
57
79
  /**
58
80
  * A single content-addressed asset from the OTA asset store.
@@ -89,6 +111,8 @@ export type OtaMetrics = {
89
111
  export type OtaNetworkStrategy = {
90
112
  retry?: number;
91
113
  timeoutMs?: number;
114
+ /** Automatic downloads only on Wi-Fi/Ethernet (manual applyUpdate always downloads) */
115
+ wifiOnly?: boolean;
92
116
  };
93
117
  export type OtaConfig = {
94
118
  /** Current app versionName (e.g. "1.0.0"). If omitted, auto-detected from native app. */
@@ -106,8 +130,22 @@ export type OtaConfig = {
106
130
  /** Telemetry: structured event callback for analytics */
107
131
  onEvent?: OtaEventCallback;
108
132
  /**
109
- * SPKI public key (PEM or base64) for RSA/ECDSA signature verification.
110
- * When set, patches and bundles without a valid signature are rejected.
133
+ * POSTs every OTA event as JSON to `url` (fire-and-forget), with installId, platform,
134
+ * appVersion, appBuild, runtimeFingerprint, flavor and the running OTA version attached.
135
+ */
136
+ telemetry?: {
137
+ url: string;
138
+ headers?: Record<string, string>;
139
+ };
140
+ /**
141
+ * Minimum time between automatic update checks (app start / foreground / checkUpdate()).
142
+ * checkUpdate({ force: true }) ignores it. Default 10 minutes.
143
+ */
144
+ minCheckIntervalMs?: number;
145
+ /**
146
+ * SPKI public key(s), PEM or base64 (comma-separated for several), for RSA/ECDSA
147
+ * signature verification. Defaults to the keys `patch` embedded in the app.
148
+ * When there is a key, unsigned or wrongly signed updates are rejected.
111
149
  */
112
150
  publicKey?: string;
113
151
  /**
@@ -116,12 +154,14 @@ export type OtaConfig = {
116
154
  * Read-only and public: never put upload credentials here, they ship inside the app.
117
155
  */
118
156
  customServer?: {
119
- /** e.g. https://raw.githubusercontent.com/<owner>/<repo>/<branch> */
157
+ /** R2 public URL, e.g. https://pub-<id>.r2.dev or a custom domain */
120
158
  baseUrl: string;
121
159
  channel: string;
122
160
  baseFolder?: string;
161
+ /** Extra headers on every request (registry, bundles, assets), e.g. auth for a private server */
162
+ headers?: Record<string, string>;
123
163
  };
124
- /** Storage provider instance (optional, defaults to GitHubRawStorageProvider) */
164
+ /** Storage provider instance (optional, defaults to R2StorageProvider) */
125
165
  storage?: OtaStorageProvider;
126
166
  };
127
167
  export declare enum OtaStatus {
@@ -146,12 +186,19 @@ export type OtaState = {
146
186
  metrics: OtaMetrics | null;
147
187
  /** True if a background update has been downloaded and prepared silently */
148
188
  isUpdateReady: boolean;
189
+ /** The ready update is marked mandatory: it reloads as soon as the app is in the foreground */
190
+ isMandatory: boolean;
191
+ /** Stable random ID of this install (rollout bucket, QA allow-lists, telemetry) */
192
+ installId: string;
149
193
  applyUpdate: () => Promise<void>;
150
194
  /** Installs and reloads `version` from the last checked manifest (full bundle, or a patch from the current version) */
151
195
  applyVersion: (version: number) => Promise<void>;
152
196
  resetToFactory: () => Promise<void>;
153
197
  markAsHealthy: () => Promise<void>;
154
- checkUpdate: () => Promise<void>;
198
+ /** Checks now unless the last check was under minCheckIntervalMs ago (force: always) */
199
+ checkUpdate: (options?: {
200
+ force?: boolean;
201
+ }) => Promise<void>;
155
202
  /** Reload the React Native JS bundle immediately to apply downloaded update */
156
203
  reloadBundle: () => void;
157
204
  };
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/ota/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/ota/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-bspatch",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Premium OTA Update Solution for React Native using TurboModules and bsdiff",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -62,7 +62,8 @@
62
62
  "bump-version": "node ./tools/bump_version.js",
63
63
  "generate-keys": "node ./tools/generate_keys.js",
64
64
  "typecheck": "tsc --noEmit",
65
- "prepare": "bob build"
65
+ "prepare": "bob build",
66
+ "test": "node tools/__tests__/entry_signature.test.js && node tools/__tests__/signing_key.test.js && node tools/__tests__/eligibility.test.js"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@types/react": "^19.2.0",
@@ -18,7 +18,9 @@ Pod::Spec.new do |s|
18
18
  s.exclude_files = "cpp/bspatch_jni.cpp", "cpp/bzip2/**"
19
19
  s.public_header_files = 'ios/**/*.h'
20
20
 
21
- s.library = 'bz2'
21
+ # bz2: bspatch + decompressFile, z: decompressFile gzip, Network: getNetworkType
22
+ s.libraries = 'bz2', 'z'
23
+ s.frameworks = 'Network'
22
24
 
23
25
  s.pod_target_xcconfig = {
24
26
  'OTHER_CFLAGS' => '$(inherited) -O3 -flto'
@@ -5,7 +5,7 @@ export type DownloadProgress = {
5
5
  /** destPath passed to downloadFile */
6
6
  path: string;
7
7
  received: number;
8
- /** -1 when the server sends no Content-Length */
8
+ /** -1 when unknown: no Content-Length, or a Content-Encoding (received counts decoded bytes) */
9
9
  total: number;
10
10
  };
11
11
 
@@ -60,13 +60,45 @@ export interface Spec extends TurboModule {
60
60
  /** Directory the native resolver reads `ota/` from (filesDir on Android, Documents on iOS) */
61
61
  getDocumentDir(): string;
62
62
 
63
+ /**
64
+ * Public keys the CLI embedded in the app, comma-separated base64 SPKI (primary first,
65
+ * then the offline backup): Info.plist `BsPatchPublicKey` / Android string
66
+ * `bspatch_public_key`. '' when none.
67
+ */
68
+ getEmbeddedPublicKeys(): string;
69
+
70
+ /** Build number: versionCode (Android) / CFBundleVersion (iOS) */
71
+ getAppBuild(): string;
72
+
73
+ /**
74
+ * Native runtime fingerprint stamped into this binary at build time (Info.plist
75
+ * `BsPatchRuntimeFingerprint` / Android resValue `bspatch_runtime_fingerprint`);
76
+ * '' when the build hook didn't run. An OTA only installs on a matching binary.
77
+ */
78
+ getRuntimeFingerprint(): string;
79
+
80
+ /** Free bytes on the volume holding getDocumentDir() */
81
+ getFreeDiskSpace(): Promise<number>;
82
+
83
+ /** Current connection: 'wifi' | 'cellular' | 'ethernet' | 'none' | 'unknown' */
84
+ getNetworkType(): Promise<string>;
85
+
86
+ /**
87
+ * Decompresses srcPath into destPath (replacing it). format: 'gzip' | 'bz2'.
88
+ * Rejects on unknown format or corrupt input; destPath is removed on failure.
89
+ */
90
+ decompressFile(srcPath: string, destPath: string, format: string): Promise<void>;
91
+
63
92
  /**
64
93
  * Streams `url` to `destPath` (via `destPath.part`, renamed only on a 2xx response,
65
94
  * replacing any existing file). Rejects on network error or non-2xx status.
95
+ * A failed download of the same destPath is resumed (HTTP Range) on the next call
96
+ * when the server supports it; otherwise it restarts from zero.
66
97
  * @param timeoutMs Idle timeout: no bytes for this long fails the download
98
+ * @param headersJson Extra request headers as a JSON object string ('{}' for none)
67
99
  * @returns Size of the downloaded file in bytes
68
100
  */
69
- downloadFile(url: string, destPath: string, timeoutMs: number): Promise<number>;
101
+ downloadFile(url: string, destPath: string, timeoutMs: number, headersJson: string): Promise<number>;
70
102
 
71
103
  /** Lowercase hex SHA-256 of a file */
72
104
  hashFile(path: string): Promise<string>;
package/src/index.ts CHANGED
@@ -20,7 +20,11 @@ export * from './ota/otaLogger';
20
20
  export * from './ota/OtaProvider';
21
21
  export * from './ota/types';
22
22
 
23
- export { GitHubRawStorageProvider } from './ota/storage/GitHubRawStorage';
23
+ export {
24
+ R2StorageProvider,
25
+ /** @deprecated Renamed to R2StorageProvider */
26
+ R2StorageProvider as GitHubRawStorageProvider,
27
+ } from './ota/storage/R2Storage';
24
28
 
25
29
  export default {
26
30
  applyPatch,
@@ -17,8 +17,10 @@ import {
17
17
  View,
18
18
  } from 'react-native';
19
19
  import NativeBsPatch from '../NativeBsPatch';
20
+ import { ineligibleReason } from './eligibility';
21
+ import { getInstallId } from './installId';
20
22
  import { applyOtaPatchInternal, fetchOtaManifest, selectUpdate, validateOtaConfig } from './otaCore';
21
- import { createOtaLogger, OtaEvent } from './otaLogger';
23
+ import { createOtaLogger, OtaEvent, type OtaLogEntry } from './otaLogger';
22
24
  import { DOCUMENT_DIR, loadMetadata, markHealthy, OTA_DIR } from './otaMetadata';
23
25
  import {
24
26
  OtaConfig,
@@ -31,6 +33,23 @@ import {
31
33
 
32
34
  const OtaContext = createContext<OtaState | undefined>(undefined);
33
35
 
36
+ const DEFAULT_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1000;
37
+
38
+ /** Sync native getter, '' if it throws */
39
+ const nativeString = (get: () => string): string => {
40
+ try {
41
+ return get() ?? '';
42
+ } catch {
43
+ return '';
44
+ }
45
+ };
46
+
47
+ /** Bytecode version the running Hermes reads (undefined off Hermes) */
48
+ const hermesBytecodeVersion = (): number | undefined => {
49
+ const v = (globalThis as any).HermesInternal?.getRuntimeProperties?.()?.['Bytecode Version'];
50
+ return v ? Number(v) : undefined;
51
+ };
52
+
34
53
  export function OtaProvider({
35
54
  children,
36
55
  config,
@@ -58,6 +77,8 @@ export function OtaProvider({
58
77
  const [patches, setPatches] = useState<OtaPatch[]>([]);
59
78
  const [isInitialized, setIsInitialized] = useState<boolean>(false);
60
79
  const [isUpdateReady, setIsUpdateReady] = useState<boolean>(false);
80
+ const [isMandatory, setIsMandatory] = useState<boolean>(false);
81
+ const [installId, setInstallId] = useState<string>('');
61
82
 
62
83
  const appState = useRef(AppState.currentState);
63
84
  const isCheckingRef = useRef(false);
@@ -65,6 +86,9 @@ export function OtaProvider({
65
86
  /** Version the native resolver loaded for this JS session */
66
87
  const runningVersionRef = useRef(config.baseAppVersion);
67
88
  const progressRef = useRef(0);
89
+ const lastCheckRef = useRef(0);
90
+ const appVersionRef = useRef(config.appVersion ?? '');
91
+ const installIdRef = useRef('');
68
92
  const isDebugUpdateEnabled = !__DEV__ || config.allowDebugUpdates === true;
69
93
 
70
94
  // No fallbacks: a missing env value disables OTA instead of silently pointing elsewhere.
@@ -85,21 +109,46 @@ export function OtaProvider({
85
109
  setProgress(nextProgress);
86
110
  }, []);
87
111
 
112
+ /** config.onEvent, plus a fire-and-forget POST to config.telemetry.url */
113
+ const onEvent = useCallback((entry: OtaLogEntry) => {
114
+ config.onEvent?.(entry);
115
+ const telemetry = config.telemetry;
116
+ if (!telemetry) return;
117
+ fetch(telemetry.url, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json', ...telemetry.headers },
120
+ body: JSON.stringify({
121
+ ...entry,
122
+ installId: installIdRef.current,
123
+ platform: Platform.OS,
124
+ appVersion: appVersionRef.current,
125
+ appBuild: nativeString(() => NativeBsPatch.getAppBuild()),
126
+ runtimeFingerprint: nativeString(() => NativeBsPatch.getRuntimeFingerprint()),
127
+ flavor: config.customServer?.channel,
128
+ otaVersion: runningVersionRef.current,
129
+ }),
130
+ }).catch(() => {});
131
+ }, [config]);
132
+
88
133
  const getEffectiveConfig = useCallback(async (): Promise<OtaConfig> => {
89
- if (config.appVersion) return config;
134
+ // The keys `patch` embedded in the app (primary + backup), unless the app passes its own
135
+ const publicKey = config.publicKey || nativeString(() => NativeBsPatch.getEmbeddedPublicKeys()) || undefined;
136
+ const base = { ...config, publicKey, onEvent };
137
+ if (config.appVersion) return base;
90
138
  try {
91
139
  const nativeVer = await NativeBsPatch.getAppVersion();
92
140
  if (!nativeVer) {
93
141
  throw new Error('Native appVersion is empty');
94
142
  }
95
- return { ...config, appVersion: nativeVer };
143
+ appVersionRef.current = nativeVer;
144
+ return { ...base, appVersion: nativeVer };
96
145
  } catch (e) {
97
146
  throw new Error(
98
147
  `[OtaProvider] Failed to auto-detect appVersion from native: ${(e as Error).message}. ` +
99
148
  `Please set versionName (Android) / CFBundleShortVersionString (iOS), or provide config.appVersion explicitly.`
100
149
  );
101
150
  }
102
- }, [config]);
151
+ }, [config, onEvent]);
103
152
 
104
153
  /**
105
154
  * Core apply update logic — separated so it can be mapped to button clicks
@@ -184,6 +233,12 @@ export function OtaProvider({
184
233
  setStatus(OtaStatus.UP_TO_DATE);
185
234
  setMessage(`Update V${targetV} ready for next launch`);
186
235
  if (m) setMetrics(m);
236
+ if (directPatch.mandatory) {
237
+ setIsMandatory(true);
238
+ // Only in the foreground: a reload in the background is rolled back (see the
239
+ // AppState effect); the effect reloads once the app comes back
240
+ if (AppState.currentState === 'active') NativeBsPatch.reloadBundle();
241
+ }
187
242
  }
188
243
  },
189
244
  () => {}, // silent: background doesn't spam progress
@@ -201,8 +256,9 @@ export function OtaProvider({
201
256
  /**
202
257
  * Check for updates from the manifest.
203
258
  * Uses a ref to guard against concurrent calls (avoids stale closure).
259
+ * Automatic calls are throttled by minCheckIntervalMs; { force: true } always checks.
204
260
  */
205
- const checkUpdates = useCallback(async () => {
261
+ const checkUpdates = useCallback(async (options?: { force?: boolean }) => {
206
262
  if (isCheckingRef.current || configError) return;
207
263
  if (!isDebugUpdateEnabled) {
208
264
  setPendingCount(0);
@@ -211,6 +267,9 @@ export function OtaProvider({
211
267
  setMessage('OTA updates are disabled while running a debug build.');
212
268
  return;
213
269
  }
270
+ const minInterval = config.minCheckIntervalMs ?? DEFAULT_MIN_CHECK_INTERVAL_MS;
271
+ if (!options?.force && Date.now() - lastCheckRef.current < minInterval) return;
272
+ lastCheckRef.current = Date.now();
214
273
  isCheckingRef.current = true;
215
274
  setIsChecking(true);
216
275
  setStatus(OtaStatus.CHECKING);
@@ -223,14 +282,22 @@ export function OtaProvider({
223
282
  const metadata = await loadMetadata(config.baseAppVersion);
224
283
  const localV = metadata.currentVersion;
225
284
 
226
- // The one platform filter (storage providers return every platform).
227
- // Also skips versions that crashed on boot and were rolled back natively.
228
- const failed = metadata.failedVersions ?? [];
229
- const platformPatches = remotePatches.filter(
230
- p =>
231
- (!p.platform || p.platform.toLowerCase() === Platform.OS) &&
232
- !failed.includes(p.toVersion),
233
- );
285
+ // Storage providers return every platform: keep what this install may take (platform,
286
+ // native fingerprint, Hermes, rollout/QA lists, kill switch, versions that crashed here)
287
+ const context = {
288
+ platform: Platform.OS,
289
+ failedVersions: metadata.failedVersions ?? [],
290
+ runtimeFingerprint: nativeString(() => NativeBsPatch.getRuntimeFingerprint()),
291
+ hermesBytecodeVersion: hermesBytecodeVersion(),
292
+ installId: await getInstallId(),
293
+ };
294
+ const platformPatches = remotePatches.filter(p => {
295
+ const reason = ineligibleReason(p, context);
296
+ if (reason && __DEV__ && p.toVersion > localV) {
297
+ console.log(`[OtaProvider] Skipping V${p.toVersion} (${p.id}): ${reason}`);
298
+ }
299
+ return !reason;
300
+ });
234
301
 
235
302
  const maxV =
236
303
  platformPatches.length > 0
@@ -247,7 +314,12 @@ export function OtaProvider({
247
314
  setStatus(OtaStatus.UPDATE_AVAILABLE);
248
315
 
249
316
  // Trigger behaviors based on mode
250
- if (config.updateMode === OtaUpdateMode.BACKGROUND_DOWNLOAD) {
317
+ const network = config.network?.wifiOnly
318
+ ? await NativeBsPatch.getNetworkType().catch(() => 'unknown')
319
+ : 'wifi';
320
+ if (network !== 'wifi' && network !== 'ethernet') {
321
+ setMessage(`Update available: V${localV} → V${maxV} (waiting for Wi-Fi)`);
322
+ } else if (config.updateMode === OtaUpdateMode.BACKGROUND_DOWNLOAD) {
251
323
  downloadBackgroundUpdate(platformPatches, localV, maxV);
252
324
  } else if (
253
325
  config.updateMode === OtaUpdateMode.ON_APP_START ||
@@ -283,6 +355,11 @@ export function OtaProvider({
283
355
  */
284
356
  useEffect(() => {
285
357
  const bootstrap = async () => {
358
+ getInstallId().then(id => {
359
+ installIdRef.current = id;
360
+ setInstallId(id);
361
+ }).catch(() => {});
362
+
286
363
  // Clean up any pending trash directories from previous resets (non-blocking)
287
364
  NativeBsPatch.readDir(DOCUMENT_DIR).then(dirs => {
288
365
  for (const dir of dirs) {
@@ -320,20 +397,26 @@ export function OtaProvider({
320
397
  }
321
398
  };
322
399
 
323
- bootstrap().then(() => setIsInitialized(true)).catch(() => {
324
- // Bootstrap failed: keep UI blocked so user sees the error state.
325
- // setIsInitialized stays false → isBlocking remains true → the error
326
- // message is shown via loadingView instead of a silent blank screen.
327
- });
400
+ // OTA must never take the app down: if bootstrap fails, render the app with OTA idle
401
+ bootstrap()
402
+ .catch(error => {
403
+ console.warn('[OtaProvider] Bootstrap failed, OTA disabled for this session:', error);
404
+ setMessage(`OTA disabled: ${(error as Error).message}`);
405
+ setStatus(OtaStatus.ERROR);
406
+ })
407
+ .finally(() => setIsInitialized(true));
328
408
  // eslint-disable-next-line react-hooks/exhaustive-deps
329
409
  }, []);
330
410
 
331
411
  /**
332
- * AppState listener for ON_APP_FOREGROUND mode.
412
+ * AppState listener for ON_APP_FOREGROUND mode, and for a mandatory update that got
413
+ * ready while in the background.
333
414
  * A downloaded update is not reloaded on background: iOS suspends JS before the
334
415
  * first render, so the native resolver counts the launch as failed and rolls it back.
335
416
  * metadata already points at the new version, so the next cold start applies it
336
417
  */
418
+ const isMandatoryRef = useRef(false);
419
+ isMandatoryRef.current = isMandatory && isUpdateReady;
337
420
  useEffect(() => {
338
421
  const subscription = AppState.addEventListener(
339
422
  'change',
@@ -343,6 +426,10 @@ export function OtaProvider({
343
426
  nextAppState === 'active' &&
344
427
  bootstrapDone.current
345
428
  ) {
429
+ if (isMandatoryRef.current) {
430
+ NativeBsPatch.reloadBundle();
431
+ return;
432
+ }
346
433
  const mode = config.updateMode ?? OtaUpdateMode.MANUAL;
347
434
  if (mode === OtaUpdateMode.ON_APP_FOREGROUND) {
348
435
  checkUpdates();
@@ -358,14 +445,14 @@ export function OtaProvider({
358
445
  }, [config.updateMode, checkUpdates]);
359
446
 
360
447
  const markAsHealthy = useCallback(async () => {
361
- const log = createOtaLogger(config.onEvent);
448
+ const log = createOtaLogger(onEvent);
362
449
  log.emit(OtaEvent.MARK_HEALTHY);
363
450
  const appVersion = await NativeBsPatch.getAppVersion().catch(() => config.appVersion);
364
451
  await markHealthy(config.baseAppVersion, runningVersionRef.current, appVersion);
365
- }, [config]);
452
+ }, [config, onEvent]);
366
453
 
367
454
  const resetToFactory = useCallback(async () => {
368
- const log = createOtaLogger(config.onEvent);
455
+ const log = createOtaLogger(onEvent);
369
456
  try {
370
457
  setStatus(OtaStatus.APPLYING);
371
458
  setMessage('Executing system wipe...');
@@ -381,7 +468,7 @@ export function OtaProvider({
381
468
  setStatus(OtaStatus.ERROR);
382
469
  setMessage(`Reset failed: ${(error as Error).message}`);
383
470
  }
384
- }, [config]);
471
+ }, [onEvent]);
385
472
 
386
473
  // Memoized: consumers re-render only when something they can see changes
387
474
  const value = useMemo<OtaState>(() => ({
@@ -394,6 +481,8 @@ export function OtaProvider({
394
481
  isChecking,
395
482
  metrics,
396
483
  isUpdateReady,
484
+ isMandatory,
485
+ installId,
397
486
  applyUpdate,
398
487
  applyVersion,
399
488
  resetToFactory,
@@ -410,6 +499,8 @@ export function OtaProvider({
410
499
  isChecking,
411
500
  metrics,
412
501
  isUpdateReady,
502
+ isMandatory,
503
+ installId,
413
504
  applyUpdate,
414
505
  applyVersion,
415
506
  resetToFactory,
@@ -0,0 +1,48 @@
1
+ import type { OtaPatch } from './types';
2
+
3
+ /** Entry `requires` values this client understands */
4
+ export const SUPPORTED_FEATURES = ['gzip', 'bz2'];
5
+
6
+ export type EligibilityContext = {
7
+ /** Platform.OS */
8
+ platform: string;
9
+ /** Versions that crashed on boot here and were rolled back */
10
+ failedVersions: number[];
11
+ /** Fingerprint stamped into this binary ('' when the build hook didn't run) */
12
+ runtimeFingerprint: string;
13
+ /** Bytecode version of the running Hermes (undefined off Hermes) */
14
+ hermesBytecodeVersion?: number;
15
+ installId: string;
16
+ };
17
+
18
+ /** FNV-1a: stable 0-99 bucket for an install and version (a different slice of installs per version) */
19
+ export const rolloutBucket = (installId: string, version: number): number => {
20
+ let h = 0x811c9dc5;
21
+ for (const c of `${installId}:${version}`) {
22
+ h ^= c.charCodeAt(0);
23
+ h = Math.imul(h, 0x01000193) >>> 0;
24
+ }
25
+ return h % 100;
26
+ };
27
+
28
+ /** Why this client must not install the entry, or null when it may */
29
+ export const ineligibleReason = (e: OtaPatch, ctx: EligibilityContext): string | null => {
30
+ if (e.platform && e.platform.toLowerCase() !== ctx.platform) return 'other platform';
31
+ if (ctx.failedVersions.includes(e.toVersion)) return 'crashed on boot here before';
32
+ if (e.disabled) return 'disabled';
33
+ const missing = (e.requires ?? []).filter(f => !SUPPORTED_FEATURES.includes(f));
34
+ if (missing.length) return `needs a newer client (${missing.join(', ')})`;
35
+ if (e.runtimeFingerprint && e.runtimeFingerprint !== ctx.runtimeFingerprint) {
36
+ return ctx.runtimeFingerprint
37
+ ? 'built for different native code (needs a store build)'
38
+ : 'this binary has no runtime fingerprint (run `react-native-bspatch setup` and rebuild)';
39
+ }
40
+ if (e.hermesBytecodeVersion && ctx.hermesBytecodeVersion && e.hermesBytecodeVersion !== ctx.hermesBytecodeVersion) {
41
+ return `Hermes bytecode ${e.hermesBytecodeVersion}, runtime reads ${ctx.hermesBytecodeVersion}`;
42
+ }
43
+ if (e.allowInstallIds?.length && !e.allowInstallIds.includes(ctx.installId)) return 'not in the QA list';
44
+ if (e.rolloutPercent !== undefined && rolloutBucket(ctx.installId, e.toVersion) >= e.rolloutPercent) {
45
+ return `outside the ${e.rolloutPercent}% rollout`;
46
+ }
47
+ return null;
48
+ };
@@ -0,0 +1,31 @@
1
+ import type { OtaPatch } from './types';
2
+
3
+ /**
4
+ * What `entrySignature` signs: the fields that decide which bundle a client installs.
5
+ * Must stay byte-for-byte equal to entryPayload in tools/update_registry.js.
6
+ */
7
+ export const entryPayload = (p: OtaPatch): string =>
8
+ [
9
+ 'bspatch-entry-v2',
10
+ p.appVersion,
11
+ p.flavor ?? '',
12
+ p.platform ?? '',
13
+ p.fromVersion,
14
+ p.toVersion,
15
+ p.bundleHash ?? '',
16
+ p.assetManifestHash ?? '',
17
+ p.minAppVersion ?? '',
18
+ p.runtimeFingerprint ?? '',
19
+ p.hermesBytecodeVersion ?? '',
20
+ ].join('\n');
21
+
22
+ /** Public keys from config or the embedded list: PEM blocks, or base64 SPKI separated by commas/whitespace */
23
+ export const parsePublicKeys = (keys: string | undefined): string[] => {
24
+ if (!keys) return [];
25
+ if (keys.includes('-----BEGIN')) {
26
+ return (keys.match(/-----BEGIN PUBLIC KEY-----[\s\S]*?-----END PUBLIC KEY-----/g) ?? []).map(block =>
27
+ block.replace(/-----[A-Z ]+-----|\s+/g, ''),
28
+ );
29
+ }
30
+ return keys.split(/[\s,]+/).filter(Boolean);
31
+ };
@@ -0,0 +1,18 @@
1
+ import NativeBsPatch from '../NativeBsPatch';
2
+ import { DOCUMENT_DIR } from './otaMetadata';
3
+
4
+ // Outside ota/: resetToFactory and native rollbacks keep it
5
+ const INSTALL_ID_PATH = `${DOCUMENT_DIR}/bspatch-install-id`;
6
+ let installId: Promise<string> | undefined;
7
+
8
+ /** Random ID made on first use and kept for the life of the install (rollout bucket, QA lists, telemetry) */
9
+ export const getInstallId = (): Promise<string> =>
10
+ (installId ??= (async () => {
11
+ try {
12
+ const saved = (await NativeBsPatch.readFile(INSTALL_ID_PATH)).trim();
13
+ if (/^[0-9a-f]{32}$/.test(saved)) return saved;
14
+ } catch {}
15
+ const id = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
16
+ await NativeBsPatch.writeFile(INSTALL_ID_PATH, id).catch(() => {});
17
+ return id;
18
+ })());