react-native-update 10.55.1 → 10.56.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.
Files changed (76) hide show
  1. package/README-CN.md +35 -0
  2. package/README.md +37 -0
  3. package/android/build.gradle +53 -27
  4. package/android/jni/Application.mk +6 -1
  5. package/android/jni/hpatch.c +59 -4
  6. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  7. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  8. package/android/lib/x86/librnupdate.so +0 -0
  9. package/android/lib/x86_64/librnupdate.so +0 -0
  10. package/android/proguard.pro +70 -14
  11. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
  12. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
  14. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
  18. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
  21. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
  22. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
  23. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
  24. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
  25. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
  26. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
  27. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
  28. package/cpp/patch_core/archive_limits.h +6 -0
  29. package/cpp/patch_core/jni_util.h +6 -2
  30. package/cpp/patch_core/patch_core.cpp +237 -84
  31. package/cpp/patch_core/patch_core.h +11 -0
  32. package/cpp/patch_core/patch_core_android.cpp +49 -33
  33. package/cpp/patch_core/update_core_android.cpp +134 -103
  34. package/cpp/update_flow_core/flow_json.cpp +150 -17
  35. package/cpp/update_flow_core/flow_json.h +13 -1
  36. package/cpp/update_flow_core/update_flow_core.cpp +78 -8
  37. package/cpp/update_flow_core/update_flow_core.h +9 -0
  38. package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
  39. package/harmony/hvigor-plugin.ts +168 -21
  40. package/harmony/pushy/src/README.md +74 -0
  41. package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
  42. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
  43. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
  44. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
  45. package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
  46. package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
  47. package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
  48. package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
  49. package/harmony/pushy/src/main/ets/Logger.ts +42 -16
  50. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
  51. package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
  52. package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
  53. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
  54. package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
  55. package/harmony/pushy/src/main/module.json5 +5 -1
  56. package/harmony/pushy.har +0 -0
  57. package/ios/PrivacyInfo.xcprivacy +79 -0
  58. package/ios/RCTPushy/RCTPushy.mm +399 -170
  59. package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
  60. package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
  61. package/package.json +14 -24
  62. package/react-native-update.podspec +43 -3
  63. package/src/client.ts +236 -82
  64. package/src/context.ts +40 -27
  65. package/src/core.ts +41 -16
  66. package/src/endpoint.ts +2 -1
  67. package/src/error.ts +49 -11
  68. package/src/index.ts +14 -0
  69. package/src/locales/en.ts +1 -23
  70. package/src/locales/zh.ts +1 -22
  71. package/src/provider.tsx +159 -111
  72. package/src/type.ts +0 -2
  73. package/src/updateFlowCore.ts +10 -2
  74. package/src/utils.ts +61 -4
  75. package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
  76. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
@@ -12,6 +12,20 @@ final class UpdateFileUtils {
12
12
  private UpdateFileUtils() {
13
13
  }
14
14
 
15
+ // Server-provided identifiers (hash/originHash/fileName) become child
16
+ // names under the update root; anything that could resolve outside of it
17
+ // (path separators, "..", ".") must be rejected before touching the
18
+ // filesystem.
19
+ static boolean isSafePathComponent(String name) {
20
+ return name != null
21
+ && !name.isEmpty()
22
+ && !name.equals(".")
23
+ && !name.equals("..")
24
+ && !name.contains("/")
25
+ && !name.contains("\\")
26
+ && name.indexOf('\0') < 0;
27
+ }
28
+
15
29
  static void ensureDirectory(File directory) throws IOException {
16
30
  if (!directory.exists() && !directory.mkdirs() && !directory.exists()) {
17
31
  throw new IOException("Failed to create directory: " + directory);
@@ -1,18 +1,107 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
+ import android.util.Log;
3
4
  import androidx.annotation.Nullable;
4
5
  import com.facebook.react.bridge.Promise;
5
6
  import com.facebook.react.bridge.ReactApplicationContext;
6
7
  import com.facebook.react.bridge.ReadableMap;
8
+ import java.util.HashMap;
9
+ import java.util.Map;
7
10
  import org.json.JSONException;
8
11
  import org.json.JSONObject;
9
12
  import org.json.JSONTokener;
10
13
 
14
+ /**
15
+ * Everything the "Pushy" native module does, independent of the bridge it is
16
+ * registered with. The old-architecture and new-architecture UpdateModule
17
+ * classes are thin wrappers that only carry their annotations / spec
18
+ * overrides and forward here, so the two cannot drift apart.
19
+ */
11
20
  public class UpdateModuleImpl {
12
21
 
13
22
  public static final String NAME = "Pushy";
14
23
 
15
- private UpdateModuleImpl() {
24
+ private final ReactApplicationContext reactContext;
25
+ private final UpdateContext updateContext;
26
+
27
+ public UpdateModuleImpl(ReactApplicationContext reactContext) {
28
+ this(reactContext, UpdateContext.getInstance(reactContext));
29
+ }
30
+
31
+ public UpdateModuleImpl(ReactApplicationContext reactContext, UpdateContext updateContext) {
32
+ this.reactContext = reactContext;
33
+ this.updateContext = updateContext;
34
+ UpdateEventEmitter.register(reactContext);
35
+ }
36
+
37
+ public Map<String, Object> getConstants() {
38
+ final Map<String, Object> constants = new HashMap<String, Object>();
39
+ constants.put("downloadRootDir", updateContext.getRootDir());
40
+ constants.put("packageVersion", updateContext.getPackageVersion());
41
+
42
+ String currentVersion = updateContext.getCurrentVersion();
43
+ constants.put("currentVersion", currentVersion);
44
+ constants.put("currentVersionInfo", updateContext.getKv("hash_" + currentVersion));
45
+ constants.put("currentBundleSha256", updateContext.currentBundleSha256(currentVersion));
46
+ constants.put("buildTime", updateContext.getBuildTime());
47
+ constants.put("isUsingBundleUrl", updateContext.getIsUsingBundleUrl());
48
+
49
+ // Both one-shot markers are consumed in a single commit: under
50
+ // TurboModules this runs synchronously on the JS thread.
51
+ UpdateContext.LaunchMarkers markers = updateContext.consumeLaunchMarkers();
52
+ constants.put("isFirstTime", markers.isFirstTime);
53
+ constants.put("rolledBackVersion", markers.rolledBackVersion);
54
+
55
+ constants.put("uuid", updateContext.getKv("uuid"));
56
+ int supportedDiffVersion = 0;
57
+ try {
58
+ supportedDiffVersion = NativeUpdateCore.supportedDiffVersion();
59
+ } catch (UnsatisfiedLinkError e) {
60
+ // A mismatched librnupdate.so (stale manual copy / build cache)
61
+ // must not crash startup via getConstants; 0 simply means "no v2
62
+ // diff track" and the server degrades gracefully.
63
+ Log.e("pushy", "supportedDiffVersion missing from librnupdate.so", e);
64
+ }
65
+ constants.put("supportedDiffVersion", supportedDiffVersion);
66
+ return constants;
67
+ }
68
+
69
+ /**
70
+ * Reads a required string option. A module call runs on the native
71
+ * modules thread, where anything thrown (a missing key, a wrong type)
72
+ * reaches the bridge's exception handler and kills the app, so a bad
73
+ * argument has to come back as an INVALID_OPTIONS rejection instead
74
+ * (CODE_AUDIT 2.11). Returns null after rejecting.
75
+ */
76
+ @Nullable
77
+ private static String readRequiredString(
78
+ @Nullable ReadableMap options,
79
+ String key,
80
+ Promise promise
81
+ ) {
82
+ String value = null;
83
+ try {
84
+ if (options != null && options.hasKey(key) && !options.isNull(key)) {
85
+ value = options.getString(key);
86
+ }
87
+ } catch (RuntimeException e) {
88
+ // Wrong type: treated like a missing key below.
89
+ }
90
+ return requireNonEmpty(value, "options." + key, promise);
91
+ }
92
+
93
+ /** Same contract for a plain string argument. */
94
+ @Nullable
95
+ private static String requireNonEmpty(
96
+ @Nullable String value,
97
+ String name,
98
+ Promise promise
99
+ ) {
100
+ if (value == null || value.isEmpty()) {
101
+ promise.reject(ErrorCodes.INVALID_OPTIONS, name + " must be a non-empty string");
102
+ return null;
103
+ }
104
+ return value;
16
105
  }
17
106
 
18
107
  private static boolean isValidHashInfo(@Nullable String json) {
@@ -26,14 +115,8 @@ public class UpdateModuleImpl {
26
115
  }
27
116
  }
28
117
 
29
- public static void downloadFullUpdate(
30
- UpdateContext updateContext,
31
- final ReadableMap options,
32
- final Promise promise
33
- ) {
34
- String url = options.getString("updateUrl");
35
- String hash = options.getString("hash");
36
- updateContext.downloadFullUpdate(url, hash, new UpdateContext.DownloadFileListener() {
118
+ private static UpdateContext.DownloadFileListener downloadListener(final Promise promise) {
119
+ return new UpdateContext.DownloadFileListener() {
37
120
  @Override
38
121
  public void onDownloadCompleted(DownloadTaskParams params) {
39
122
  promise.resolve(null);
@@ -43,32 +126,61 @@ public class UpdateModuleImpl {
43
126
  public void onDownloadFailed(Throwable error) {
44
127
  promise.reject(downloadErrorCode(error), error);
45
128
  }
46
- });
129
+ };
47
130
  }
48
131
 
49
132
  // Post-download failures (unzip / hdiff / resource copy, incl. copiesCrc
50
133
  // verification) reject as PATCH_FAILED so JS-side telemetry can separate
51
- // patch health from network health. DownloadTask wraps them.
134
+ // patch health from network health; a local refusal (reinstalling the
135
+ // running version in place) as FILE_OPERATION_FAILED. DownloadTask
136
+ // classifies them.
52
137
  private static String downloadErrorCode(Throwable error) {
53
- return error instanceof PatchFailedException
54
- ? ErrorCodes.PATCH_FAILED
55
- : ErrorCodes.DOWNLOAD_FAILED;
138
+ if (error instanceof PatchFailedException) {
139
+ return ErrorCodes.PATCH_FAILED;
140
+ }
141
+ if (error instanceof FileOperationException) {
142
+ return ErrorCodes.FILE_OPERATION_FAILED;
143
+ }
144
+ return ErrorCodes.DOWNLOAD_FAILED;
56
145
  }
57
146
 
58
- public static void downloadAndInstallApk(
59
- final ReactApplicationContext reactContext,
60
- UpdateContext updateContext,
61
- final ReadableMap options,
62
- final Promise promise
63
- ) {
64
- // A module call runs on the native modules thread: anything thrown here
65
- // reaches the bridge's exception handler and kills the app. Every
66
- // failure of this path — a missing REQUEST_INSTALL_PACKAGES declaration
67
- // above all — has to come back as a promise rejection instead.
147
+ public void downloadFullUpdate(final ReadableMap options, final Promise promise) {
148
+ String url = readRequiredString(options, "updateUrl", promise);
149
+ if (url == null) {
150
+ return;
151
+ }
152
+ String hash = readRequiredString(options, "hash", promise);
153
+ if (hash == null) {
154
+ return;
155
+ }
156
+ updateContext.downloadFullUpdate(url, hash, downloadListener(promise));
157
+ }
158
+
159
+ public void downloadAndInstallApk(final ReadableMap options, final Promise promise) {
160
+ // Every failure of this path — a missing REQUEST_INSTALL_PACKAGES
161
+ // declaration above all — has to come back as a promise rejection,
162
+ // never as an exception on the native modules thread.
68
163
  try {
69
- final String url = options.getString("url");
70
- String hash = options.getString("hash");
71
- String target = options.getString("target");
164
+ final String url = readRequiredString(options, "url", promise);
165
+ if (url == null) {
166
+ return;
167
+ }
168
+ String hash = readRequiredString(options, "hash", promise);
169
+ if (hash == null) {
170
+ return;
171
+ }
172
+ String target = readRequiredString(options, "target", promise);
173
+ if (target == null) {
174
+ return;
175
+ }
176
+ if (!HttpUtils.isHttpsUrl(url)) {
177
+ // Installing an APK is code execution and nothing verifies
178
+ // its bytes yet (CODE_AUDIT 2.6): TLS is the only thing
179
+ // standing between the server and the device, so a plaintext
180
+ // URL is refused outright.
181
+ promise.reject(ErrorCodes.INVALID_OPTIONS, "APK url must use https: " + url);
182
+ return;
183
+ }
72
184
  if (!ApkInstaller.ensureInstallPermission(reactContext, promise)) {
73
185
  return;
74
186
  }
@@ -88,129 +200,87 @@ public class UpdateModuleImpl {
88
200
  }
89
201
  }
90
202
 
91
- public static void downloadPatchFromPackage(
92
- UpdateContext updateContext,
93
- final ReadableMap options,
94
- final Promise promise
95
- ) {
96
- String url = options.getString("updateUrl");
97
- String hash = options.getString("hash");
98
- updateContext.downloadPatchFromApk(url, hash, new UpdateContext.DownloadFileListener() {
99
- @Override
100
- public void onDownloadCompleted(DownloadTaskParams params) {
101
- promise.resolve(null);
102
- }
103
-
104
- @Override
105
- public void onDownloadFailed(Throwable error) {
106
- promise.reject(downloadErrorCode(error), error);
107
- }
108
- });
203
+ public void downloadPatchFromPackage(final ReadableMap options, final Promise promise) {
204
+ String url = readRequiredString(options, "updateUrl", promise);
205
+ if (url == null) {
206
+ return;
207
+ }
208
+ String hash = readRequiredString(options, "hash", promise);
209
+ if (hash == null) {
210
+ return;
211
+ }
212
+ updateContext.downloadPatchFromApk(url, hash, downloadListener(promise));
109
213
  }
110
214
 
111
- public static void downloadPatchFromPpk(
112
- UpdateContext updateContext,
113
- final ReadableMap options,
114
- final Promise promise
115
- ) {
116
- try {
117
- String url = options.getString("updateUrl");
118
- String hash = options.getString("hash");
119
- String originHash = options.getString("originHash");
120
-
121
- updateContext.downloadPatchFromPpk(url, hash, originHash, new UpdateContext.DownloadFileListener() {
122
- @Override
123
- public void onDownloadCompleted(DownloadTaskParams params) {
124
- promise.resolve(null);
125
- }
126
-
127
- @Override
128
- public void onDownloadFailed(Throwable error) {
129
- promise.reject(downloadErrorCode(error), error);
130
- }
131
- });
132
- } catch (Exception e) {
133
- promise.reject(ErrorCodes.INVALID_OPTIONS, "downloadPatchFromPpk failed: " + e.getMessage(), e);
215
+ public void downloadPatchFromPpk(final ReadableMap options, final Promise promise) {
216
+ String url = readRequiredString(options, "updateUrl", promise);
217
+ if (url == null) {
218
+ return;
219
+ }
220
+ String hash = readRequiredString(options, "hash", promise);
221
+ if (hash == null) {
222
+ return;
134
223
  }
224
+ String originHash = readRequiredString(options, "originHash", promise);
225
+ if (originHash == null) {
226
+ return;
227
+ }
228
+ updateContext.downloadPatchFromPpk(url, hash, originHash, downloadListener(promise));
135
229
  }
136
230
 
137
- public static void reloadUpdate(
138
- final UpdateContext updateContext,
139
- final ReactApplicationContext reactContext,
140
- final ReadableMap options,
141
- final Promise promise
142
- ) {
143
- restartApp(updateContext, reactContext, options.getString("hash"), promise);
231
+ public void reloadUpdate(final ReadableMap options, final Promise promise) {
232
+ String hash = readRequiredString(options, "hash", promise);
233
+ if (hash == null) {
234
+ return;
235
+ }
236
+ restartApp(hash, promise);
144
237
  }
145
238
 
146
- public static void restartApp(
147
- final UpdateContext updateContext,
148
- final ReactApplicationContext reactContext,
149
- @Nullable final String hash,
150
- final Promise promise
151
- ) {
152
- UiThreadRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new UiThreadRunner.Operation() {
239
+ public void restartApp(@Nullable final String hash, final Promise promise) {
240
+ // Two hops (CODE_AUDIT 2.2): the switch (bundle digest + commit) and
241
+ // the launch resolution run on the state serial thread like every
242
+ // other state write; only the React reload itself is posted to the
243
+ // UI thread. A failure at either hop rejects with RESTART_FAILED.
244
+ StateSerialRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new StateSerialRunner.Operation() {
153
245
  @Override
154
246
  public void run() throws Throwable {
155
- ReactReloadManager.restartApp(updateContext, reactContext, hash);
156
- promise.resolve(true);
247
+ final String bundlePath = ReactReloadManager.prepareRestart(updateContext, hash);
248
+ UiThreadRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new UiThreadRunner.Operation() {
249
+ @Override
250
+ public void run() throws Throwable {
251
+ ReactReloadManager.reload(updateContext, reactContext, bundlePath);
252
+ promise.resolve(true);
253
+ }
254
+ });
157
255
  }
158
256
  });
159
257
  }
160
258
 
161
- private static void setNeedUpdateInternal(UpdateContext updateContext, String hash) {
162
- updateContext.switchVersion(hash);
163
- }
164
-
165
- public static void setNeedUpdate(
166
- final UpdateContext updateContext,
167
- final ReadableMap options,
168
- final Promise promise
169
- ) {
170
- final String hash = options.getString("hash");
259
+ public void setNeedUpdate(final ReadableMap options, final Promise promise) {
260
+ final String hash = readRequiredString(options, "hash", promise);
261
+ if (hash == null) {
262
+ return;
263
+ }
171
264
  StateSerialRunner.run(promise, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
172
265
  @Override
173
266
  public void run() {
174
- setNeedUpdateInternal(updateContext, hash);
267
+ updateContext.switchVersion(hash);
175
268
  promise.resolve(true);
176
269
  }
177
270
  });
178
271
  }
179
272
 
180
- public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
181
- final String hash = options.getString("hash");
182
- StateSerialRunner.run(null, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
183
- @Override
184
- public void run() {
185
- setNeedUpdateInternal(updateContext, hash);
186
- }
187
- });
188
- }
189
-
190
- private static void markSuccessInternal(UpdateContext updateContext) {
191
- updateContext.markSuccess();
192
- }
193
-
194
- public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
273
+ public void markSuccess(final Promise promise) {
195
274
  StateSerialRunner.run(promise, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
196
275
  @Override
197
276
  public void run() {
198
- markSuccessInternal(updateContext);
277
+ updateContext.markSuccess();
199
278
  promise.resolve(true);
200
279
  }
201
280
  });
202
281
  }
203
282
 
204
- public static void markSuccess(final UpdateContext updateContext) {
205
- StateSerialRunner.run(null, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
206
- @Override
207
- public void run() {
208
- markSuccessInternal(updateContext);
209
- }
210
- });
211
- }
212
-
213
- public static void getBundleHash(final UpdateContext updateContext, final Promise promise) {
283
+ public void getBundleHash(final Promise promise) {
214
284
  // Threading lives in UpdateContext (download executor); resolve is
215
285
  // thread-safe. Never rejects — empty string means "unknown".
216
286
  updateContext.getBundleHash(new UpdateContext.BundleHashListener() {
@@ -221,10 +291,7 @@ public class UpdateModuleImpl {
221
291
  });
222
292
  }
223
293
 
224
- public static void resetToPackagedBundle(
225
- final UpdateContext updateContext,
226
- final Promise promise
227
- ) {
294
+ public void resetToPackagedBundle(final Promise promise) {
228
295
  StateSerialRunner.run(promise, ErrorCodes.RESET_FAILED, "resetToPackagedBundle", new StateSerialRunner.Operation() {
229
296
  @Override
230
297
  public void run() {
@@ -239,18 +306,11 @@ public class UpdateModuleImpl {
239
306
  * reuse instead of re-checking (§10.3). Empty string when absent; never
240
307
  * rejects.
241
308
  */
242
- public static void getNativeCheckCache(
243
- final UpdateContext updateContext,
244
- final Promise promise
245
- ) {
309
+ public void getNativeCheckCache(final Promise promise) {
246
310
  String cached = updateContext.getKv(NativeCheckOrchestrator.KEY_RESP_CACHE);
247
311
  promise.resolve(cached == null ? "" : cached);
248
312
  }
249
313
 
250
- private static void setUuidInternal(UpdateContext updateContext, String uuid) {
251
- updateContext.setKv("uuid", uuid);
252
- }
253
-
254
314
  /**
255
315
  * Provisioning for the native cold-start update check
256
316
  * (NATIVE_CHECKUPDATE_DESIGN §10.1): the raw JSON persists as-is and is
@@ -258,26 +318,16 @@ public class UpdateModuleImpl {
258
318
  * Validated at write time — a corrupt config would otherwise silently
259
319
  * disable the native check forever with no signal.
260
320
  */
261
- public static void markJsCheckCompleted(
262
- final UpdateContext updateContext,
263
- final String config,
264
- final Promise promise
265
- ) {
266
- if (config == null || config.isEmpty()) {
267
- promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a non-empty string");
321
+ public void markJsCheckCompleted(final String config, final Promise promise) {
322
+ if (requireNonEmpty(config, "config", promise) == null) {
268
323
  return;
269
324
  }
270
325
  NativeCheckOrchestrator.markJsCheckCompleted(config);
271
326
  promise.resolve(true);
272
327
  }
273
328
 
274
- public static void syncNativeConfig(
275
- final UpdateContext updateContext,
276
- final String config,
277
- final Promise promise
278
- ) {
279
- if (config == null || config.isEmpty()) {
280
- promise.reject(ErrorCodes.INVALID_OPTIONS, "config must be a JSON object string");
329
+ public void syncNativeConfig(final String config, final Promise promise) {
330
+ if (requireNonEmpty(config, "config", promise) == null) {
281
331
  return;
282
332
  }
283
333
  try {
@@ -295,92 +345,49 @@ public class UpdateModuleImpl {
295
345
  });
296
346
  }
297
347
 
298
- public static void setUuid(
299
- final UpdateContext updateContext,
300
- final String uuid,
301
- final Promise promise
302
- ) {
348
+ public void setUuid(final String uuid, final Promise promise) {
349
+ if (requireNonEmpty(uuid, "uuid", promise) == null) {
350
+ return;
351
+ }
303
352
  StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
304
353
  @Override
305
354
  public void run() {
306
- setUuidInternal(updateContext, uuid);
355
+ updateContext.setKv("uuid", uuid);
307
356
  promise.resolve(true);
308
357
  }
309
358
  });
310
359
  }
311
360
 
312
- public static void setUuid(final UpdateContext updateContext, final String uuid) {
313
- StateSerialRunner.run(null, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
314
- @Override
315
- public void run() {
316
- setUuidInternal(updateContext, uuid);
317
- }
318
- });
319
- }
320
-
321
- private static void setLocalHashInfoInternal(
322
- UpdateContext updateContext,
323
- String hash,
324
- String info
325
- ) {
326
- if (!isValidHashInfo(info)) {
327
- throw new IllegalArgumentException("invalid json string");
328
- }
329
- try {
330
- updateContext.setKv("hash_" + hash, info);
331
- } catch (IllegalStateException e) {
332
- // Distinguish "could not persist" from "malformed input": JS
333
- // classifies the former as FILE_OPERATION_FAILED.
334
- throw new PersistenceException(e);
361
+ public void setLocalHashInfo(final String hash, final String info, final Promise promise) {
362
+ if (requireNonEmpty(hash, "hash", promise) == null) {
363
+ return;
335
364
  }
336
- }
337
-
338
- /** A state write that did not reach disk (SharedPreferences commit false). */
339
- static final class PersistenceException extends RuntimeException {
340
- PersistenceException(Throwable cause) {
341
- super(String.valueOf(cause.getMessage()), cause);
365
+ if (!isValidHashInfo(info)) {
366
+ promise.reject(ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo failed: invalid json string");
367
+ return;
342
368
  }
343
- }
344
-
345
- public static void setLocalHashInfo(
346
- final UpdateContext updateContext,
347
- final String hash,
348
- final String info,
349
- final Promise promise
350
- ) {
351
- StateSerialRunner.run(promise, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
369
+ // A state write that did not reach disk (SharedPreferences commit
370
+ // false) is FILE_OPERATION_FAILED, distinct from malformed input.
371
+ StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "setLocalHashInfo", new StateSerialRunner.Operation() {
352
372
  @Override
353
373
  public void run() {
354
- try {
355
- setLocalHashInfoInternal(updateContext, hash, info);
356
- } catch (PersistenceException e) {
357
- promise.reject(ErrorCodes.FILE_OPERATION_FAILED, "setLocalHashInfo failed", e);
358
- return;
359
- }
374
+ updateContext.setKv("hash_" + hash, info);
360
375
  promise.resolve(true);
361
376
  }
362
377
  });
363
378
  }
364
379
 
365
- public static void setLocalHashInfo(
366
- final UpdateContext updateContext,
367
- final String hash,
368
- final String info
369
- ) {
370
- StateSerialRunner.run(null, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
371
- @Override
372
- public void run() {
373
- setLocalHashInfoInternal(updateContext, hash, info);
374
- }
375
- });
376
- }
377
-
378
- public static void getLocalHashInfo(
379
- UpdateContext updateContext,
380
- final String hash,
381
- final Promise promise
382
- ) {
380
+ public void getLocalHashInfo(final String hash, final Promise promise) {
381
+ if (requireNonEmpty(hash, "hash", promise) == null) {
382
+ return;
383
+ }
383
384
  String value = updateContext.getKv("hash_" + hash);
385
+ if (value == null || value.isEmpty()) {
386
+ // No record (iOS resolves nil the same way): JS treats it as {}.
387
+ // Only a record that exists and is not a JSON object is invalid.
388
+ promise.resolve(null);
389
+ return;
390
+ }
384
391
  if (!isValidHashInfo(value)) {
385
392
  promise.reject(ErrorCodes.INVALID_HASH_INFO, "getLocalHashInfo failed: invalid json string");
386
393
  return;
@@ -9,6 +9,11 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
9
9
  import java.util.HashMap;
10
10
  import java.util.Map;
11
11
 
12
+ // TurboReactPackage and the 7-argument ReactModuleInfo are deprecated in
13
+ // current React Native but still shipped; their replacements
14
+ // (BaseReactPackage, the 6-argument constructor) only exist from RN 0.74,
15
+ // and this module's peer floor is RN 0.59.
16
+ @SuppressWarnings("deprecation")
12
17
  public class UpdatePackage extends TurboReactPackage {
13
18
  @Nullable
14
19
  @Override