react-native-bspatch 4.1.0 → 5.0.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/README.md +84 -27
- package/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +86 -6
- package/android/src/main/res/values/bspatch_public_key.xml +5 -0
- package/cpp/bsdiff/bspatch_wrapper.c +34 -0
- package/cpp/bsdiff/bspatch_wrapper.h +2 -0
- package/cpp/bspatch_jni.cpp +18 -0
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +9 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +165 -35
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/eligibility.js +47 -0
- package/lib/commonjs/ota/eligibility.js.map +1 -0
- package/lib/commonjs/ota/entrySignature.js +23 -0
- package/lib/commonjs/ota/entrySignature.js.map +1 -0
- package/lib/commonjs/ota/installId.js +27 -0
- package/lib/commonjs/ota/installId.js.map +1 -0
- package/lib/commonjs/ota/otaCore.js +96 -21
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/storage/{GitHubRawStorage.js → R2Storage.js} +20 -15
- package/lib/commonjs/ota/storage/R2Storage.js.map +1 -0
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +165 -35
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/eligibility.js +41 -0
- package/lib/module/ota/eligibility.js.map +1 -0
- package/lib/module/ota/entrySignature.js +17 -0
- package/lib/module/ota/entrySignature.js.map +1 -0
- package/lib/module/ota/installId.js +22 -0
- package/lib/module/ota/installId.js.map +1 -0
- package/lib/module/ota/otaCore.js +96 -21
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/storage/{GitHubRawStorage.js → R2Storage.js} +18 -14
- package/lib/module/ota/storage/R2Storage.js.map +1 -0
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +28 -2
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/eligibility.d.ts +25 -0
- package/lib/typescript/ota/eligibility.d.ts.map +1 -0
- package/lib/typescript/ota/entrySignature.d.ts +9 -0
- package/lib/typescript/ota/entrySignature.d.ts.map +1 -0
- package/lib/typescript/ota/installId.d.ts +3 -0
- package/lib/typescript/ota/installId.d.ts.map +1 -0
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/storage/{GitHubRawStorage.d.ts → R2Storage.d.ts} +4 -3
- package/lib/typescript/ota/storage/R2Storage.d.ts.map +1 -0
- package/lib/typescript/ota/types.d.ts +58 -5
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/react-native-bspatch.podspec +3 -1
- package/src/NativeBsPatch.ts +34 -2
- package/src/index.ts +5 -1
- package/src/ota/OtaProvider.tsx +167 -42
- package/src/ota/eligibility.ts +57 -0
- package/src/ota/entrySignature.ts +31 -0
- package/src/ota/installId.ts +18 -0
- package/src/ota/otaCore.ts +102 -21
- package/src/ota/storage/{GitHubRawStorage.ts → R2Storage.ts} +17 -13
- package/src/ota/types.ts +55 -5
- package/tools/build_ota.sh +343 -116
- package/tools/doctor.js +130 -0
- package/tools/ensure_signing_key.js +216 -0
- package/tools/fingerprint.js +111 -0
- package/tools/ios_fingerprint.sh +9 -0
- package/tools/ota_admin.js +131 -0
- package/tools/setup_project.js +41 -15
- package/tools/update_registry.js +111 -11
- package/tools/upload_ota.sh +85 -184
- package/tools/verify_publish.js +81 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entrySignature.d.ts","sourceRoot":"","sources":["../../../src/ota/entrySignature.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,QAAQ,KAAG,MAa3B,CAAC;AAEjB,8GAA8G;AAC9G,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,MAAM,EAQhE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installId.d.ts","sourceRoot":"","sources":["../../../src/ota/installId.ts"],"names":[],"mappings":"AAOA,6GAA6G;AAC7G,eAAO,MAAM,YAAY,QAAO,OAAO,CAAC,MAAM,CASrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otaCore.d.ts","sourceRoot":"","sources":["../../../src/ota/otaCore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"otaCore.d.ts","sourceRoot":"","sources":["../../../src/ota/otaCore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH,OAAO,EACH,SAAS,EAET,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,QAAQ,EAEhB,MAAM,SAAS,CAAC;AAUjB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,KAAG,MAAM,GAAG,IAU9D,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,OAUhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,MAAM,EACZ,eAAe,MAAM,KACtB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAoGF;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,KAAG,OAAO,CAAC,QAAQ,EAAE,CAC3B,CAAC;AA2H7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,KAAG,MACvB,CAAC;AAExC;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,QAAQ,EAAE,QAAQ,MAAM,KAAG,MACL,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACrB,WAAW,QAAQ,EAAE,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,KAChB,QAAQ,GAAG,SAMb,CAAC;AAmBF,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,EAAE,OAAO,iBAU1C,CAAC;AAEF,QAAA,MAAM,iBAAiB,GACnB,OAAO,QAAQ,EACf,QAAQ,SAAS,EACjB,UAAU,CACN,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,KACnB,IAAI,EACT,YAAY,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACrC,UAAS;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAyB,kBAkP3D,CAAC"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { OtaConfig, OtaPatch, OtaStorageProvider } from '../types';
|
|
2
2
|
/** GET + JSON.parse with a timeout (fetch has none of its own); rejects on non-2xx. */
|
|
3
|
-
export declare const fetchJson: (url: string, timeoutMs: number) => Promise<any>;
|
|
4
|
-
export declare
|
|
3
|
+
export declare const fetchJson: (url: string, timeoutMs: number, headers?: Record<string, string>) => Promise<any>;
|
|
4
|
+
export declare const normalizeChannel: (channel: string) => string;
|
|
5
|
+
export declare class R2StorageProvider implements OtaStorageProvider {
|
|
5
6
|
fetchManifest(config: OtaConfig): Promise<OtaPatch[]>;
|
|
6
7
|
getUpdateFileUrl(filename: string, config: OtaConfig): Promise<string>;
|
|
7
8
|
getAssetUrl(hash: string, ext: string, config: OtaConfig): Promise<string>;
|
|
8
9
|
}
|
|
9
|
-
//# sourceMappingURL=
|
|
10
|
+
//# sourceMappingURL=R2Storage.d.ts.map
|
|
@@ -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
|
-
*
|
|
110
|
-
*
|
|
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://
|
|
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
|
|
164
|
+
/** Storage provider instance (optional, defaults to R2StorageProvider) */
|
|
125
165
|
storage?: OtaStorageProvider;
|
|
126
166
|
};
|
|
127
167
|
export declare enum OtaStatus {
|
|
@@ -146,12 +186,25 @@ 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>;
|
|
196
|
+
/**
|
|
197
|
+
* Installs and reloads exactly this registry entry (e.g. a row of a version list): its
|
|
198
|
+
* patch when it starts from the running version, else its full bundle. Ignores the kill
|
|
199
|
+
* switch, rollout and QA list; still refuses entries for other native code or Hermes.
|
|
200
|
+
*/
|
|
201
|
+
applyEntry: (entry: OtaPatch) => Promise<void>;
|
|
152
202
|
resetToFactory: () => Promise<void>;
|
|
153
203
|
markAsHealthy: () => Promise<void>;
|
|
154
|
-
|
|
204
|
+
/** Checks now unless the last check was under minCheckIntervalMs ago (force: always) */
|
|
205
|
+
checkUpdate: (options?: {
|
|
206
|
+
force?: boolean;
|
|
207
|
+
}) => Promise<void>;
|
|
155
208
|
/** Reload the React Native JS bundle immediately to apply downloaded update */
|
|
156
209
|
reloadBundle: () => void;
|
|
157
210
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/ota/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,kBAAkB;IAC/B;;OAEG;IACH,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtD;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEvE;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9E;AAED,oBAAY,aAAa;IACrB,MAAM,WAAW;IACjB,YAAY,iBAAiB;IAC7B,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;CAC9C;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
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": "
|
|
3
|
+
"version": "5.0.1",
|
|
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
|
-
|
|
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'
|
package/src/NativeBsPatch.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type DownloadProgress = {
|
|
|
5
5
|
/** destPath passed to downloadFile */
|
|
6
6
|
path: string;
|
|
7
7
|
received: number;
|
|
8
|
-
/** -1 when
|
|
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 {
|
|
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,
|
package/src/ota/OtaProvider.tsx
CHANGED
|
@@ -17,8 +17,10 @@ import {
|
|
|
17
17
|
View,
|
|
18
18
|
} from 'react-native';
|
|
19
19
|
import NativeBsPatch from '../NativeBsPatch';
|
|
20
|
+
import { ineligibleReason, type EligibilityContext } 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,39 +109,62 @@ 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
|
-
|
|
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
|
-
|
|
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]);
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
setStatus(OtaStatus.ERROR);
|
|
117
|
-
setMessage('No upgrade path found.');
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
151
|
+
}, [config, onEvent]);
|
|
152
|
+
|
|
153
|
+
/** What decides which registry entries this install may take */
|
|
154
|
+
const eligibilityContext = useCallback(async (): Promise<EligibilityContext> => {
|
|
155
|
+
const metadata = await loadMetadata(config.baseAppVersion);
|
|
156
|
+
return {
|
|
157
|
+
platform: Platform.OS,
|
|
158
|
+
failedVersions: metadata.failedVersions ?? [],
|
|
159
|
+
runtimeFingerprint: nativeString(() => NativeBsPatch.getRuntimeFingerprint()),
|
|
160
|
+
hermesBytecodeVersion: hermesBytecodeVersion(),
|
|
161
|
+
installId: await getInstallId(),
|
|
162
|
+
};
|
|
163
|
+
}, [config.baseAppVersion]);
|
|
120
164
|
|
|
165
|
+
/** Installs one registry entry in the foreground (status + progress), then reloads */
|
|
166
|
+
const installEntry = useCallback(
|
|
167
|
+
async (directPatch: OtaPatch) => {
|
|
121
168
|
try {
|
|
122
169
|
const effectiveConfig = await getEffectiveConfig();
|
|
123
170
|
setStatus(OtaStatus.DOWNLOADING);
|
|
@@ -146,6 +193,46 @@ export function OtaProvider({
|
|
|
146
193
|
[getEffectiveConfig, setProgressIfChanged],
|
|
147
194
|
);
|
|
148
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Core apply update logic — separated so it can be mapped to button clicks
|
|
198
|
+
* OR called automatically by checkUpdates()
|
|
199
|
+
*/
|
|
200
|
+
const executeApplyUpdate = useCallback(
|
|
201
|
+
async (patchList: OtaPatch[], currentVer: number, targetVer: number) => {
|
|
202
|
+
const directPatch = selectUpdate(patchList, currentVer, targetVer);
|
|
203
|
+
if (!directPatch) {
|
|
204
|
+
if (__DEV__) {
|
|
205
|
+
console.warn('[OtaProvider] No update entry found for', currentVer, '->', targetVer);
|
|
206
|
+
}
|
|
207
|
+
setStatus(OtaStatus.ERROR);
|
|
208
|
+
setMessage('No upgrade path found.');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await installEntry(directPatch);
|
|
212
|
+
},
|
|
213
|
+
[installEntry],
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Installs exactly this registry entry (a version list in a debug/QA screen): the patch
|
|
218
|
+
* when it starts from the running version, else its full bundle. Ignores the kill switch,
|
|
219
|
+
* rollout and QA list; still refuses what would break the app (native code, Hermes, …).
|
|
220
|
+
*/
|
|
221
|
+
const applyEntry = useCallback(async (entry: OtaPatch) => {
|
|
222
|
+
if (!isDebugUpdateEnabled) {
|
|
223
|
+
setStatus(OtaStatus.UP_TO_DATE);
|
|
224
|
+
setMessage('OTA updates are disabled while running a debug build.');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const reason = ineligibleReason(entry, await eligibilityContext(), { ignoreControls: true });
|
|
228
|
+
if (reason) {
|
|
229
|
+
setStatus(OtaStatus.ERROR);
|
|
230
|
+
setMessage(`Can't install V${entry.toVersion}: ${reason}`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await installEntry(entry);
|
|
234
|
+
}, [eligibilityContext, installEntry, isDebugUpdateEnabled]);
|
|
235
|
+
|
|
149
236
|
/**
|
|
150
237
|
* Exposed method for manual trigger
|
|
151
238
|
*/
|
|
@@ -184,6 +271,12 @@ export function OtaProvider({
|
|
|
184
271
|
setStatus(OtaStatus.UP_TO_DATE);
|
|
185
272
|
setMessage(`Update V${targetV} ready for next launch`);
|
|
186
273
|
if (m) setMetrics(m);
|
|
274
|
+
if (directPatch.mandatory) {
|
|
275
|
+
setIsMandatory(true);
|
|
276
|
+
// Only in the foreground: a reload in the background is rolled back (see the
|
|
277
|
+
// AppState effect); the effect reloads once the app comes back
|
|
278
|
+
if (AppState.currentState === 'active') NativeBsPatch.reloadBundle();
|
|
279
|
+
}
|
|
187
280
|
}
|
|
188
281
|
},
|
|
189
282
|
() => {}, // silent: background doesn't spam progress
|
|
@@ -201,8 +294,9 @@ export function OtaProvider({
|
|
|
201
294
|
/**
|
|
202
295
|
* Check for updates from the manifest.
|
|
203
296
|
* Uses a ref to guard against concurrent calls (avoids stale closure).
|
|
297
|
+
* Automatic calls are throttled by minCheckIntervalMs; { force: true } always checks.
|
|
204
298
|
*/
|
|
205
|
-
const checkUpdates = useCallback(async () => {
|
|
299
|
+
const checkUpdates = useCallback(async (options?: { force?: boolean }) => {
|
|
206
300
|
if (isCheckingRef.current || configError) return;
|
|
207
301
|
if (!isDebugUpdateEnabled) {
|
|
208
302
|
setPendingCount(0);
|
|
@@ -211,6 +305,9 @@ export function OtaProvider({
|
|
|
211
305
|
setMessage('OTA updates are disabled while running a debug build.');
|
|
212
306
|
return;
|
|
213
307
|
}
|
|
308
|
+
const minInterval = config.minCheckIntervalMs ?? DEFAULT_MIN_CHECK_INTERVAL_MS;
|
|
309
|
+
if (!options?.force && Date.now() - lastCheckRef.current < minInterval) return;
|
|
310
|
+
lastCheckRef.current = Date.now();
|
|
214
311
|
isCheckingRef.current = true;
|
|
215
312
|
setIsChecking(true);
|
|
216
313
|
setStatus(OtaStatus.CHECKING);
|
|
@@ -223,14 +320,16 @@ export function OtaProvider({
|
|
|
223
320
|
const metadata = await loadMetadata(config.baseAppVersion);
|
|
224
321
|
const localV = metadata.currentVersion;
|
|
225
322
|
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
const
|
|
229
|
-
const platformPatches = remotePatches.filter(
|
|
230
|
-
p
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
323
|
+
// Storage providers return every platform: keep what this install may take (platform,
|
|
324
|
+
// native fingerprint, Hermes, rollout/QA lists, kill switch, versions that crashed here)
|
|
325
|
+
const context = await eligibilityContext();
|
|
326
|
+
const platformPatches = remotePatches.filter(p => {
|
|
327
|
+
const reason = ineligibleReason(p, context);
|
|
328
|
+
if (reason && __DEV__ && p.toVersion > localV) {
|
|
329
|
+
console.log(`[OtaProvider] Skipping V${p.toVersion} (${p.id}): ${reason}`);
|
|
330
|
+
}
|
|
331
|
+
return !reason;
|
|
332
|
+
});
|
|
234
333
|
|
|
235
334
|
const maxV =
|
|
236
335
|
platformPatches.length > 0
|
|
@@ -247,7 +346,12 @@ export function OtaProvider({
|
|
|
247
346
|
setStatus(OtaStatus.UPDATE_AVAILABLE);
|
|
248
347
|
|
|
249
348
|
// Trigger behaviors based on mode
|
|
250
|
-
|
|
349
|
+
const network = config.network?.wifiOnly
|
|
350
|
+
? await NativeBsPatch.getNetworkType().catch(() => 'unknown')
|
|
351
|
+
: 'wifi';
|
|
352
|
+
if (network !== 'wifi' && network !== 'ethernet') {
|
|
353
|
+
setMessage(`Update available: V${localV} → V${maxV} (waiting for Wi-Fi)`);
|
|
354
|
+
} else if (config.updateMode === OtaUpdateMode.BACKGROUND_DOWNLOAD) {
|
|
251
355
|
downloadBackgroundUpdate(platformPatches, localV, maxV);
|
|
252
356
|
} else if (
|
|
253
357
|
config.updateMode === OtaUpdateMode.ON_APP_START ||
|
|
@@ -268,7 +372,7 @@ export function OtaProvider({
|
|
|
268
372
|
isCheckingRef.current = false;
|
|
269
373
|
setIsChecking(false);
|
|
270
374
|
}
|
|
271
|
-
}, [config, configError, executeApplyUpdate, isDebugUpdateEnabled, downloadBackgroundUpdate, getEffectiveConfig]);
|
|
375
|
+
}, [config, configError, eligibilityContext, executeApplyUpdate, isDebugUpdateEnabled, downloadBackgroundUpdate, getEffectiveConfig]);
|
|
272
376
|
|
|
273
377
|
/**
|
|
274
378
|
* Bootstrap: runs once on mount.
|
|
@@ -283,6 +387,11 @@ export function OtaProvider({
|
|
|
283
387
|
*/
|
|
284
388
|
useEffect(() => {
|
|
285
389
|
const bootstrap = async () => {
|
|
390
|
+
getInstallId().then(id => {
|
|
391
|
+
installIdRef.current = id;
|
|
392
|
+
setInstallId(id);
|
|
393
|
+
}).catch(() => {});
|
|
394
|
+
|
|
286
395
|
// Clean up any pending trash directories from previous resets (non-blocking)
|
|
287
396
|
NativeBsPatch.readDir(DOCUMENT_DIR).then(dirs => {
|
|
288
397
|
for (const dir of dirs) {
|
|
@@ -320,20 +429,26 @@ export function OtaProvider({
|
|
|
320
429
|
}
|
|
321
430
|
};
|
|
322
431
|
|
|
323
|
-
bootstrap
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
432
|
+
// OTA must never take the app down: if bootstrap fails, render the app with OTA idle
|
|
433
|
+
bootstrap()
|
|
434
|
+
.catch(error => {
|
|
435
|
+
console.warn('[OtaProvider] Bootstrap failed, OTA disabled for this session:', error);
|
|
436
|
+
setMessage(`OTA disabled: ${(error as Error).message}`);
|
|
437
|
+
setStatus(OtaStatus.ERROR);
|
|
438
|
+
})
|
|
439
|
+
.finally(() => setIsInitialized(true));
|
|
328
440
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
329
441
|
}, []);
|
|
330
442
|
|
|
331
443
|
/**
|
|
332
|
-
* AppState listener for ON_APP_FOREGROUND mode
|
|
444
|
+
* AppState listener for ON_APP_FOREGROUND mode, and for a mandatory update that got
|
|
445
|
+
* ready while in the background.
|
|
333
446
|
* A downloaded update is not reloaded on background: iOS suspends JS before the
|
|
334
447
|
* first render, so the native resolver counts the launch as failed and rolls it back.
|
|
335
448
|
* metadata already points at the new version, so the next cold start applies it
|
|
336
449
|
*/
|
|
450
|
+
const isMandatoryRef = useRef(false);
|
|
451
|
+
isMandatoryRef.current = isMandatory && isUpdateReady;
|
|
337
452
|
useEffect(() => {
|
|
338
453
|
const subscription = AppState.addEventListener(
|
|
339
454
|
'change',
|
|
@@ -343,6 +458,10 @@ export function OtaProvider({
|
|
|
343
458
|
nextAppState === 'active' &&
|
|
344
459
|
bootstrapDone.current
|
|
345
460
|
) {
|
|
461
|
+
if (isMandatoryRef.current) {
|
|
462
|
+
NativeBsPatch.reloadBundle();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
346
465
|
const mode = config.updateMode ?? OtaUpdateMode.MANUAL;
|
|
347
466
|
if (mode === OtaUpdateMode.ON_APP_FOREGROUND) {
|
|
348
467
|
checkUpdates();
|
|
@@ -358,14 +477,14 @@ export function OtaProvider({
|
|
|
358
477
|
}, [config.updateMode, checkUpdates]);
|
|
359
478
|
|
|
360
479
|
const markAsHealthy = useCallback(async () => {
|
|
361
|
-
const log = createOtaLogger(
|
|
480
|
+
const log = createOtaLogger(onEvent);
|
|
362
481
|
log.emit(OtaEvent.MARK_HEALTHY);
|
|
363
482
|
const appVersion = await NativeBsPatch.getAppVersion().catch(() => config.appVersion);
|
|
364
483
|
await markHealthy(config.baseAppVersion, runningVersionRef.current, appVersion);
|
|
365
|
-
}, [config]);
|
|
484
|
+
}, [config, onEvent]);
|
|
366
485
|
|
|
367
486
|
const resetToFactory = useCallback(async () => {
|
|
368
|
-
const log = createOtaLogger(
|
|
487
|
+
const log = createOtaLogger(onEvent);
|
|
369
488
|
try {
|
|
370
489
|
setStatus(OtaStatus.APPLYING);
|
|
371
490
|
setMessage('Executing system wipe...');
|
|
@@ -381,7 +500,7 @@ export function OtaProvider({
|
|
|
381
500
|
setStatus(OtaStatus.ERROR);
|
|
382
501
|
setMessage(`Reset failed: ${(error as Error).message}`);
|
|
383
502
|
}
|
|
384
|
-
}, [
|
|
503
|
+
}, [onEvent]);
|
|
385
504
|
|
|
386
505
|
// Memoized: consumers re-render only when something they can see changes
|
|
387
506
|
const value = useMemo<OtaState>(() => ({
|
|
@@ -394,8 +513,11 @@ export function OtaProvider({
|
|
|
394
513
|
isChecking,
|
|
395
514
|
metrics,
|
|
396
515
|
isUpdateReady,
|
|
516
|
+
isMandatory,
|
|
517
|
+
installId,
|
|
397
518
|
applyUpdate,
|
|
398
519
|
applyVersion,
|
|
520
|
+
applyEntry,
|
|
399
521
|
resetToFactory,
|
|
400
522
|
markAsHealthy,
|
|
401
523
|
checkUpdate: checkUpdates,
|
|
@@ -410,8 +532,11 @@ export function OtaProvider({
|
|
|
410
532
|
isChecking,
|
|
411
533
|
metrics,
|
|
412
534
|
isUpdateReady,
|
|
535
|
+
isMandatory,
|
|
536
|
+
installId,
|
|
413
537
|
applyUpdate,
|
|
414
538
|
applyVersion,
|
|
539
|
+
applyEntry,
|
|
415
540
|
resetToFactory,
|
|
416
541
|
markAsHealthy,
|
|
417
542
|
checkUpdates,
|