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
package/src/provider.tsx CHANGED
@@ -7,14 +7,7 @@ import React, {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
- import {
11
- Alert,
12
- AppState,
13
- Linking,
14
- type NativeEventSubscription,
15
- Platform,
16
- } from 'react-native';
17
- import { URL } from 'react-native-url-polyfill';
10
+ import { Alert, AppState, Linking, Platform } from 'react-native';
18
11
  import { type Cresc, type Pushy, sharedState } from './client';
19
12
  import { ProgressContext, UpdateContext } from './context';
20
13
  import {
@@ -27,7 +20,7 @@ import {
27
20
  } from './core';
28
21
  import type { CheckResult, ProgressData, UpdateTestPayload } from './type';
29
22
  import { decideDownload, resolveCheckResult } from './updateFlowCore';
30
- import { assertWeb, log, noop } from './utils';
23
+ import { assertWeb, isWeb, log, noop, parseQueryParams } from './utils';
31
24
 
32
25
  export const UpdateProvider = ({
33
26
  client,
@@ -42,24 +35,22 @@ export const UpdateProvider = ({
42
35
  // A second concurrently mounted provider is a hard integration error (the
43
36
  // client is a process-level singleton); the client throws on the second
44
37
  // claim. Also releases the claim on unmount.
45
- useEffect(() => client.claimProviderMount?.(), [client]);
38
+ useEffect(() => client.claimProviderMount(), [client]);
46
39
 
47
40
  // options is mutated in place by client.setOptions (its identity never
48
41
  // changes), so effects keyed on it would never re-run. The client bumps a
49
42
  // version on every setOptions; mirroring it into state re-renders this
50
- // provider and re-runs the effects that list it as a dependency.
51
- const [optionsVersion, setOptionsVersion] = useState(
52
- client.optionsVersion ?? 0
53
- );
43
+ // provider, which re-reads the option fields its effects key on (see the
44
+ // lifecycle effects below).
45
+ const [, setOptionsVersion] = useState(client.optionsVersion);
54
46
  useEffect(
55
47
  () =>
56
- client.onOptionsChange?.(() => {
57
- setOptionsVersion((version) => version + 1);
48
+ client.onOptionsChange(() => {
49
+ setOptionsVersion(client.optionsVersion);
58
50
  }),
59
51
  [client]
60
52
  );
61
53
 
62
- const stateListener = useRef<NativeEventSubscription>(undefined);
63
54
  const [updateInfo, setUpdateInfo] = useState<CheckResult>();
64
55
  const updateInfoRef = useRef(updateInfo);
65
56
  const [progress, setProgress] = useState<ProgressData>();
@@ -198,6 +189,27 @@ export const UpdateProvider = ({
198
189
  [client]
199
190
  );
200
191
 
192
+ // The action behind an expired package: install the APK in place on
193
+ // Android, hand every other URL to the system. The URL is server-controlled,
194
+ // so a scheme without a handler (or a malformed value) rejects — from an
195
+ // alert button or the silent branch that must not become an unhandled
196
+ // rejection.
197
+ const openExpiredDownload = useCallback(
198
+ (downloadUrl: string) => {
199
+ if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
200
+ downloadAndInstallApk(downloadUrl).catch(noop);
201
+ return;
202
+ }
203
+ Promise.resolve()
204
+ .then(() => Linking.openURL(downloadUrl))
205
+ .catch((e: any) => {
206
+ log('openURL failed:', downloadUrl, e?.message || e);
207
+ setLastError(e instanceof Error ? e : new Error(String(e)));
208
+ });
209
+ },
210
+ [downloadAndInstallApk]
211
+ );
212
+
201
213
  const checkUpdate = useCallback(
202
214
  async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => {
203
215
  // No throttle here: the client already dedupes checks via its 5s
@@ -234,16 +246,27 @@ export const UpdateProvider = ({
234
246
  log
235
247
  );
236
248
  if (
237
- !info.expired &&
238
249
  info.update &&
239
250
  (typeof info.hash !== 'string' || info.hash.length === 0)
240
251
  ) {
241
- // A malformed rollout/root entry must not produce an alert whose
242
- // confirm button can never download anything. Surface it to the
243
- // developer telemetry/logger and present it to the app as no update.
244
- client.reportInvalidUpdateOnce('missingHash');
245
- info = { upToDate: true };
252
+ if (info.expired) {
253
+ // An expired package carries its own action (downloadUrl); a stray
254
+ // `update: true` without a hash is nothing the app could act on, so
255
+ // present the response as expired-only instead of also advertising
256
+ // an update.
257
+ info = { ...info, update: false };
258
+ } else {
259
+ // A malformed rollout/root entry must not produce an alert whose
260
+ // confirm button can never download anything. Surface it to the
261
+ // developer telemetry/logger and present it to the app as no update.
262
+ client.reportInvalidUpdateOnce('missingHash');
263
+ info = { upToDate: true };
264
+ }
246
265
  }
266
+ const silentStrategy =
267
+ options.updateStrategy === 'silentAndNow' ||
268
+ options.updateStrategy === 'silentAndLater';
269
+ let unusableRelease: CheckResult | undefined;
247
270
  if (info.update && !info.expired) {
248
271
  const decision = decideDownload(
249
272
  info,
@@ -252,9 +275,17 @@ export const UpdateProvider = ({
252
275
  );
253
276
  if (decision.action === 'none') {
254
277
  if (decision.reason === 'noArtifact') {
255
- // Invalid server data is worth reporting; local rollout guards are
256
- // expected no-ops and stay silent.
257
- client.reportInvalidUpdateOnce('noArtifact', info.hash || '');
278
+ // Invalid server data is worth reporting (local rollout guards are
279
+ // expected no-ops and stay silent) — once per bad release. The
280
+ // client reports it from the download attempt, which the silent
281
+ // strategies make below; the alert strategies never get there
282
+ // (no confirm button is shown for an update that cannot
283
+ // download), so only they report from here.
284
+ if (silentStrategy) {
285
+ unusableRelease = info;
286
+ } else {
287
+ client.reportInvalidUpdateOnce('noArtifact', info.hash || '');
288
+ }
258
289
  }
259
290
  info = { upToDate: true };
260
291
  }
@@ -264,6 +295,11 @@ export const UpdateProvider = ({
264
295
  }
265
296
  updateInfoRef.current = info;
266
297
  setUpdateInfo(info);
298
+ if (unusableRelease) {
299
+ // Resolves false once the client has reported the bad release.
300
+ downloadUpdate(unusableRelease).catch(noop);
301
+ return info;
302
+ }
267
303
  if (info.expired) {
268
304
  if (
269
305
  options.onPackageExpired &&
@@ -275,39 +311,30 @@ export const UpdateProvider = ({
275
311
  const { downloadUrl } = info;
276
312
  if (downloadUrl && sharedState.apkStatus === null) {
277
313
  if (options.updateStrategy === 'silentAndNow') {
278
- if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
279
- downloadAndInstallApk(downloadUrl).catch(noop);
280
- } else {
281
- Linking.openURL(downloadUrl);
282
- }
314
+ openExpiredDownload(downloadUrl);
283
315
  return info;
284
316
  }
285
317
  alertUpdate(client.t('alert_title'), client.t('alert_app_updated'), [
286
318
  {
287
319
  text: client.t('alert_update_button'),
288
320
  onPress: () => {
289
- if (Platform.OS === 'android' && downloadUrl.endsWith('.apk')) {
290
- downloadAndInstallApk(downloadUrl).catch(noop);
291
- } else {
292
- Linking.openURL(downloadUrl);
293
- }
321
+ openExpiredDownload(downloadUrl);
294
322
  },
295
323
  },
296
324
  ]);
297
325
  }
298
326
  } else if (info.update) {
299
- if (
300
- options.updateStrategy === 'silentAndNow' ||
301
- options.updateStrategy === 'silentAndLater'
302
- ) {
327
+ if (silentStrategy) {
303
328
  downloadUpdate(info).catch(noop);
304
329
  return info;
305
330
  }
306
331
  alertUpdate(
307
332
  client.t('alert_title'),
308
333
  client.t('alert_new_version_found', {
309
- name: info.name!,
310
- description: info.description!,
334
+ // A server that omits the name must not leave a literal
335
+ // `{{name}}` in the alert.
336
+ name: info.name || '',
337
+ description: info.description || '',
311
338
  }),
312
339
  [
313
340
  { text: client.t('alert_cancel'), style: 'cancel' },
@@ -328,78 +355,105 @@ export const UpdateProvider = ({
328
355
  options,
329
356
  alertUpdate,
330
357
  alertError,
331
- downloadAndInstallApk,
358
+ openExpiredDownload,
332
359
  downloadUpdate,
333
360
  ]
334
361
  );
335
362
 
336
363
  const markSuccess = client.markSuccess;
337
364
 
338
- // biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
365
+ // The lifecycle effects below key on the primitive option values they read
366
+ // (re-read on every render, i.e. after every setOptions), so a setOptions
367
+ // that touches something else — a logger, disableErrorReporting — no longer
368
+ // re-runs a start check, resets the auto-mark timer or re-subscribes
369
+ // AppState. checkUpdate's identity follows the strategy options; the
370
+ // listener and the start check reach it through this ref instead of
371
+ // depending on it.
372
+ const { checkStrategy, autoMarkSuccess, autoMarkSuccessDelayMs } = options;
373
+ const latestCheckUpdate = useRef(checkUpdate);
339
374
  useEffect(() => {
340
- if (!client.assertDebug('checkUpdate()')) {
375
+ latestCheckUpdate.current = checkUpdate;
376
+ }, [checkUpdate]);
377
+
378
+ // (a) Foreground checks: one AppState subscription for as long as the
379
+ // strategy asks for it.
380
+ useEffect(() => {
381
+ if (
382
+ isWeb ||
383
+ (checkStrategy !== 'both' && checkStrategy !== 'onAppResume') ||
384
+ !client.assertDebug('checkUpdate()')
385
+ ) {
341
386
  return;
342
387
  }
343
- if (!assertWeb()) {
388
+ const subscription = AppState.addEventListener('change', (nextAppState) => {
389
+ if (nextAppState === 'active') {
390
+ latestCheckUpdate.current().catch(noop);
391
+ }
392
+ });
393
+ return () => {
394
+ subscription.remove();
395
+ };
396
+ }, [client, checkStrategy]);
397
+
398
+ // (b) Automatic markSuccess: armed once per (autoMarkSuccess, delay); an
399
+ // unrelated setOptions must not push the health confirmation out again.
400
+ useEffect(() => {
401
+ if (isWeb || !autoMarkSuccess || !client.assertDebug('markSuccess()')) {
344
402
  return;
345
403
  }
346
- const { checkStrategy, autoMarkSuccess, autoMarkSuccessDelayMs } = options;
347
- let markSuccessTimer: ReturnType<typeof setTimeout> | undefined;
348
- if (autoMarkSuccess) {
349
- const delay =
350
- typeof autoMarkSuccessDelayMs === 'number' &&
351
- autoMarkSuccessDelayMs >= 0
352
- ? autoMarkSuccessDelayMs
353
- : 1000;
354
- markSuccessTimer = setTimeout(() => {
355
- // The health check is read when the timer fires, not when it was
356
- // armed, so a late setOptions still applies.
357
- const { healthCheck } = options;
358
- (async () => {
359
- if (healthCheck) {
360
- let healthy = false;
361
- try {
362
- healthy = (await healthCheck()) !== false;
363
- } catch (e: any) {
364
- log('healthCheck threw, not marking success:', e?.message || e);
365
- return;
366
- }
367
- if (!healthy) {
368
- log('healthCheck returned false, not marking success');
369
- return;
370
- }
404
+ const delay =
405
+ typeof autoMarkSuccessDelayMs === 'number' && autoMarkSuccessDelayMs >= 0
406
+ ? autoMarkSuccessDelayMs
407
+ : 1000;
408
+ const markSuccessTimer = setTimeout(() => {
409
+ // The health check is read when the timer fires, not when it was
410
+ // armed, so a late setOptions still applies.
411
+ const { healthCheck } = client.options;
412
+ (async () => {
413
+ if (healthCheck) {
414
+ let healthy = false;
415
+ try {
416
+ healthy = (await healthCheck()) !== false;
417
+ } catch (e: any) {
418
+ log('healthCheck threw, not marking success:', e?.message || e);
419
+ return;
371
420
  }
372
- // Failures are reported and surfaced via the onError subscription.
373
- await markSuccess();
374
- })().catch(noop);
375
- }, delay);
376
- }
377
- if (checkStrategy === 'both' || checkStrategy === 'onAppResume') {
378
- stateListener.current = AppState.addEventListener(
379
- 'change',
380
- (nextAppState) => {
381
- if (nextAppState === 'active') {
382
- checkUpdate().catch(noop);
421
+ if (!healthy) {
422
+ log('healthCheck returned false, not marking success');
423
+ return;
383
424
  }
384
425
  }
385
- );
386
- }
387
- if (checkStrategy === 'both' || checkStrategy === 'onAppStart') {
388
- checkUpdate().catch(noop);
389
- }
426
+ // Failures are reported and surfaced via the onError subscription.
427
+ await client.markSuccess();
428
+ })().catch(noop);
429
+ }, delay);
390
430
  return () => {
391
- if (markSuccessTimer) {
392
- clearTimeout(markSuccessTimer);
393
- }
394
- stateListener.current?.remove();
431
+ clearTimeout(markSuccessTimer);
395
432
  };
396
- // optionsVersion: checkStrategy/autoMarkSuccess are read from the
397
- // in-place-mutated options object, so option changes must re-run this.
398
- }, [checkUpdate, options, markSuccess, client, optionsVersion]);
433
+ }, [client, autoMarkSuccess, autoMarkSuccessDelayMs]);
434
+
435
+ // (c) The start-of-life check runs once per mount, never again because of a
436
+ // setOptions. It still respects the strategy in force at mount: an app that
437
+ // mounts without automatic checks and enables them later gets its one
438
+ // start check at that point.
439
+ const startCheckDone = useRef(false);
440
+ useEffect(() => {
441
+ if (
442
+ startCheckDone.current ||
443
+ (checkStrategy !== 'both' && checkStrategy !== 'onAppStart') ||
444
+ !client.assertDebug('checkUpdate()') ||
445
+ !assertWeb()
446
+ ) {
447
+ return;
448
+ }
449
+ startCheckDone.current = true;
450
+ latestCheckUpdate.current().catch(noop);
451
+ }, [client, checkStrategy]);
399
452
 
400
- // biome-ignore lint/correctness/useExhaustiveDependencies: optionsVersion re-runs this when setOptions mutates options in place
453
+ // A dismissErrorAfter change reschedules a running timer, not only the next
454
+ // error's.
455
+ const { dismissErrorAfter } = options;
401
456
  useEffect(() => {
402
- const { dismissErrorAfter } = options;
403
457
  if (
404
458
  lastError &&
405
459
  typeof dismissErrorAfter === 'number' &&
@@ -412,9 +466,7 @@ export const UpdateProvider = ({
412
466
  clearTimeout(dismissErrorTimer);
413
467
  };
414
468
  }
415
- // optionsVersion: dismissErrorAfter changes must reschedule a running
416
- // timer, not only apply to the next error.
417
- }, [lastError, options, dismissError, optionsVersion]);
469
+ }, [lastError, dismissErrorAfter, dismissError]);
418
470
 
419
471
  const parseTestPayload = useCallback(
420
472
  (payload: UpdateTestPayload) => {
@@ -496,18 +548,14 @@ export const UpdateProvider = ({
496
548
  if (!url) {
497
549
  return;
498
550
  }
499
- try {
500
- const params = new URL(url).searchParams;
501
- const payload = {
502
- type: params.get('type'),
503
- data: params.get('data'),
504
- };
505
- parseTestPayload(payload);
506
- } catch (e: any) {
507
- // A malformed deep link (new URL throws) must not become an
508
- // unhandled rejection / a throw inside the 'url' event handler.
509
- log('parseLinking: invalid url', e?.message || e);
510
- }
551
+ // A plain query-string parse (never throws, see parseQueryParams): a
552
+ // malformed deep link must not become an unhandled rejection / a throw
553
+ // inside the 'url' event handler.
554
+ const params = parseQueryParams(url);
555
+ parseTestPayload({
556
+ type: params.type ?? null,
557
+ data: params.data ?? null,
558
+ });
511
559
  };
512
560
 
513
561
  Linking.getInitialURL().then(parseLinking).catch(noop);
package/src/type.ts CHANGED
@@ -105,8 +105,6 @@ export type EventType =
105
105
  | 'errorRestart'
106
106
  | 'errorSwitchVersion'
107
107
  | 'downloadingApk'
108
- | 'rejectStoragePermission'
109
- | 'errorStoragePermission'
110
108
  | 'errorDownloadAndInstallApk'
111
109
  | 'errorInstallApk';
112
110
 
@@ -355,7 +355,11 @@ export interface DownloadPlan {
355
355
  hash: string;
356
356
  /** Ordered attempts: incremental first, full as the last resort. */
357
357
  attempts: DownloadAttempt[];
358
- /** Dev-only: nothing to fetch — treat as an immediate no-op success. */
358
+ /**
359
+ * Dev-only: nothing to fetch. The executor logs why and delivers nothing
360
+ * (resolves undefined) — not a fake success whose hash could never be
361
+ * switched to.
362
+ */
359
363
  devNoop: boolean;
360
364
  }
361
365
 
@@ -371,7 +375,11 @@ export function decideDownload(
371
375
  identity: { currentVersion?: string; rolledBackVersion?: string },
372
376
  isDev = false
373
377
  ): DownloadDecision {
374
- const { hash, diff, pdiff, full, paths = [] } = info;
378
+ const { hash, diff, pdiff, full } = info;
379
+ // A server `paths: null` behaves like an absent field (a destructuring
380
+ // default only covers undefined, and `null.map` would throw). The C++ port
381
+ // treats null and undefined alike here.
382
+ const paths = info.paths ?? [];
375
383
  if (!info.update || !hash) {
376
384
  return { action: 'none', reason: 'noUpdate' };
377
385
  }
package/src/utils.ts CHANGED
@@ -1,12 +1,28 @@
1
1
  import { Platform } from 'react-native';
2
2
  import i18n from './i18n';
3
3
 
4
+ // log/info are developer diagnostics (they include full check request bodies
5
+ // and responses); release builds only print them when the client's `debug`
6
+ // option is on. warn/error always print.
7
+ let debugLogging = false;
8
+ export const setDebugLogging = (enabled: boolean) => {
9
+ debugLogging = !!enabled;
10
+ };
11
+ const isVerbose = () =>
12
+ // Metro always defines __DEV__; guard anyway so a bundler that does not
13
+ // cannot break the whole module at import time.
14
+ (typeof __DEV__ === 'boolean' && __DEV__) || debugLogging;
15
+
4
16
  export function log(...args: any[]) {
5
- console.log(i18n.t('dev_log_prefix'), ...args);
17
+ if (isVerbose()) {
18
+ console.log(i18n.t('dev_log_prefix'), ...args);
19
+ }
6
20
  }
7
21
 
8
22
  export function info(...args: any[]) {
9
- console.info(i18n.t('dev_log_prefix'), ...args);
23
+ if (isVerbose()) {
24
+ console.info(i18n.t('dev_log_prefix'), ...args);
25
+ }
10
26
  }
11
27
 
12
28
  export function warn(...args: any[]) {
@@ -41,7 +57,6 @@ export function promiseAny<T>(promises: Promise<T>[]) {
41
57
  });
42
58
  }
43
59
 
44
- export const emptyObj = {};
45
60
  export const noop = () => {};
46
61
  const emptyModuleTarget: Record<string, typeof noop> = {};
47
62
  export const emptyModule = new Proxy(emptyModuleTarget, {
@@ -147,10 +162,11 @@ export const fetchWithTimeout = (
147
162
  // caller-provided signal (e.g. the hedged endpoint race cancelling losers)
148
163
  // must be chained onto it manually.
149
164
  const externalSignal = (params as any)?.signal as AbortSignal | undefined;
165
+ const onExternalAbort = () => controller.abort();
150
166
  if (externalSignal?.aborted) {
151
167
  controller.abort();
152
168
  } else if (externalSignal) {
153
- externalSignal.addEventListener('abort', () => controller.abort());
169
+ externalSignal.addEventListener('abort', onExternalAbort);
154
170
  }
155
171
  const timeoutId = setTimeout(() => {
156
172
  log('fetch timeout', url);
@@ -170,9 +186,50 @@ export const fetchWithTimeout = (
170
186
  })
171
187
  .finally(() => {
172
188
  clearTimeout(timeoutId);
189
+ // A long-lived caller signal (one per endpoint race) would otherwise
190
+ // keep every finished request's listener — and its controller — alive.
191
+ externalSignal?.removeEventListener('abort', onExternalAbort);
173
192
  });
174
193
  };
175
194
 
195
+ /**
196
+ * Query parameters of a URL as a plain object, without a URL polyfill: the
197
+ * only consumer is the test-channel deep link (`?type=...&data=...`). Handles
198
+ * a missing `?`, a `#fragment`, `+` as space and percent-encoding; a value
199
+ * that fails to decode is kept verbatim rather than throwing. Later
200
+ * duplicates win.
201
+ */
202
+ export const parseQueryParams = (url: string): Record<string, string> => {
203
+ const params: Record<string, string> = {};
204
+ const hashStart = url.indexOf('#');
205
+ const withoutFragment = hashStart < 0 ? url : url.slice(0, hashStart);
206
+ const queryStart = withoutFragment.indexOf('?');
207
+ if (queryStart < 0) {
208
+ return params;
209
+ }
210
+ const query = withoutFragment.slice(queryStart + 1);
211
+ const decode = (value: string) => {
212
+ const spaced = value.replace(/\+/g, ' ');
213
+ try {
214
+ return decodeURIComponent(spaced);
215
+ } catch {
216
+ return spaced;
217
+ }
218
+ };
219
+ for (const pair of query.split('&')) {
220
+ if (!pair) {
221
+ continue;
222
+ }
223
+ const separator = pair.indexOf('=');
224
+ const key = decode(separator < 0 ? pair : pair.slice(0, separator));
225
+ if (!key) {
226
+ continue;
227
+ }
228
+ params[key] = separator < 0 ? '' : decode(pair.slice(separator + 1));
229
+ }
230
+ return params;
231
+ };
232
+
176
233
  /**
177
234
  * True when `finalUrl` (after redirects) is plaintext http while the request
178
235
  * started on https. An https endpoint or artifact URL must never be served
@@ -1,13 +0,0 @@
1
- package cn.reactnative.modules.update;
2
-
3
- import androidx.annotation.Nullable;
4
-
5
- public interface ReactNativeHostHandler {
6
- @Nullable
7
- String getJSBundleFile(boolean useDeveloperSupport);
8
-
9
- @Nullable
10
- String getBundleAssetName(boolean useDeveloperSupport);
11
-
12
- void onWillCreateReactInstance(boolean useDeveloperSupport);
13
- }
@@ -1,45 +0,0 @@
1
- package cn.reactnative.modules.update;
2
-
3
- import android.util.Log;
4
- import java.util.HashMap;
5
- import java.util.Map;
6
-
7
- final class UpdateModuleSupport {
8
- private UpdateModuleSupport() {
9
- }
10
-
11
- static Map<String, Object> getConstants(UpdateContext updateContext) {
12
- final Map<String, Object> constants = new HashMap<String, Object>();
13
- constants.put("downloadRootDir", updateContext.getRootDir());
14
- constants.put("packageVersion", updateContext.getPackageVersion());
15
-
16
- String currentVersion = updateContext.getCurrentVersion();
17
- constants.put("currentVersion", currentVersion);
18
- constants.put("currentVersionInfo", updateContext.getKv("hash_" + currentVersion));
19
- constants.put("currentBundleSha256", updateContext.currentBundleSha256(currentVersion));
20
- constants.put("buildTime", updateContext.getBuildTime());
21
- constants.put("isUsingBundleUrl", updateContext.getIsUsingBundleUrl());
22
-
23
- boolean isFirstTime = updateContext.consumeFirstLoadMarker();
24
- constants.put("isFirstTime", isFirstTime);
25
-
26
- String rolledBackVersion = updateContext.rolledBackVersion();
27
- constants.put("rolledBackVersion", rolledBackVersion);
28
- if (rolledBackVersion != null) {
29
- updateContext.clearRollbackMark();
30
- }
31
-
32
- constants.put("uuid", updateContext.getKv("uuid"));
33
- int supportedDiffVersion = 0;
34
- try {
35
- supportedDiffVersion = NativeUpdateCore.supportedDiffVersion();
36
- } catch (UnsatisfiedLinkError e) {
37
- // A mismatched librnupdate.so (stale manual copy / build cache)
38
- // must not crash startup via getConstants; 0 simply means "no v2
39
- // diff track" and the server degrades gracefully.
40
- Log.e("pushy", "supportedDiffVersion missing from librnupdate.so", e);
41
- }
42
- constants.put("supportedDiffVersion", supportedDiffVersion);
43
- return constants;
44
- }
45
- }