react-native-update 10.59.2 → 10.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README-CN.md +7 -7
  2. package/README.md +7 -6
  3. package/android/build.gradle +25 -33
  4. package/android/jni/Android.mk +16 -24
  5. package/android/lib/arm64-v8a/librnpushy.so +0 -0
  6. package/android/lib/armeabi-v7a/librnpushy.so +0 -0
  7. package/android/lib/x86/librnpushy.so +0 -0
  8. package/android/lib/x86_64/librnpushy.so +0 -0
  9. package/android/proguard.pro +9 -7
  10. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
  12. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
  13. package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
  14. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
  16. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
  17. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
  18. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
  19. package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
  20. package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
  21. package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
  22. package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
  23. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
  24. package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
  25. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
  26. package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
  27. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
  28. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
  29. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
  30. package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
  31. package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
  32. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
  33. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
  34. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
  35. package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
  36. package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
  37. package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
  38. package/cpp/android_jni/archive_limits_jni.cpp +83 -0
  39. package/cpp/android_jni/install_record_jni.cpp +89 -0
  40. package/cpp/android_jni/jni_log.h +31 -0
  41. package/cpp/android_jni/jni_natives.h +40 -0
  42. package/cpp/android_jni/jni_registration.cpp +133 -0
  43. package/cpp/android_jni/jni_util.h +160 -0
  44. package/cpp/android_jni/update_core_android.cpp +450 -0
  45. package/cpp/android_jni/update_flow_jni.cpp +133 -0
  46. package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
  47. package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
  48. package/cpp/patch_core/archive_limits.cpp +194 -0
  49. package/cpp/patch_core/archive_limits.h +99 -10
  50. package/cpp/patch_core/archive_patch_core.cpp +96 -12
  51. package/cpp/patch_core/archive_patch_core.h +49 -0
  52. package/cpp/patch_core/digest.h +5 -4
  53. package/cpp/patch_core/error_codes.h +5 -3
  54. package/cpp/patch_core/install_record.cpp +430 -0
  55. package/cpp/patch_core/install_record.h +163 -9
  56. package/cpp/patch_core/patch_core.cpp +62 -16
  57. package/cpp/patch_core/patch_core.h +27 -7
  58. package/cpp/patch_core/patch_manifest.cpp +201 -0
  59. package/cpp/patch_core/patch_manifest.h +74 -0
  60. package/cpp/patch_core/state_ops.h +3 -3
  61. package/cpp/update_flow_core/flow_json.cpp +82 -8
  62. package/cpp/update_flow_core/flow_json.h +21 -0
  63. package/cpp/update_flow_core/native_config.cpp +456 -0
  64. package/cpp/update_flow_core/native_config.h +84 -0
  65. package/cpp/update_flow_core/native_round.cpp +326 -0
  66. package/cpp/update_flow_core/native_round.h +90 -0
  67. package/cpp/update_flow_core/update_flow_core.h +2 -2
  68. package/harmony/hvigor-plugin.ts +11 -19
  69. package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
  70. package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
  71. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
  72. package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
  73. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
  74. package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
  75. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
  76. package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
  77. package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
  78. package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
  79. package/harmony/pushy/src/main/ets/Logger.ts +20 -34
  80. package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
  81. package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
  82. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
  83. package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
  84. package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
  85. package/harmony/pushy/src/main/ets/Texts.ts +0 -4
  86. package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
  87. package/harmony/pushy.har +0 -0
  88. package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
  89. package/ios/RCTPushy/RCTPushy.h +12 -0
  90. package/ios/RCTPushy/RCTPushy.mm +87 -2634
  91. package/ios/RCTPushy/RCTPushyCommon.h +34 -0
  92. package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
  93. package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
  94. package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
  95. package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
  96. package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
  97. package/ios/RCTPushy/RCTPushyInternal.h +144 -0
  98. package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
  99. package/ios/RCTPushy/RCTPushyState.mm +314 -0
  100. package/package.json +25 -23
  101. package/react-native-update.podspec +73 -82
  102. package/src/client.ts +361 -451
  103. package/src/context.ts +1 -1
  104. package/src/core.ts +35 -8
  105. package/src/endpoint.ts +0 -2
  106. package/src/error.ts +13 -32
  107. package/src/i18n.ts +18 -83
  108. package/src/locales/en.ts +0 -4
  109. package/src/locales/zh.ts +0 -4
  110. package/src/provider.tsx +60 -53
  111. package/src/telemetry.ts +0 -13
  112. package/src/type.ts +0 -15
  113. package/src/updateFlowCore.ts +116 -10
  114. package/src/utils.ts +49 -42
  115. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
  116. package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
  117. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
  118. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
  119. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
  120. package/cpp/patch_core/jni_natives.h +0 -27
  121. package/cpp/patch_core/jni_registration.cpp +0 -103
  122. package/cpp/patch_core/jni_util.h +0 -60
  123. package/cpp/patch_core/patch_core_android.cpp +0 -97
  124. package/cpp/patch_core/update_core_android.cpp +0 -485
  125. package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
package/src/context.ts CHANGED
@@ -46,7 +46,7 @@ export interface UpdateContextValue {
46
46
  lastError?: Error;
47
47
  }
48
48
 
49
- export const defaultContext: UpdateContextValue = {
49
+ const defaultContext: UpdateContextValue = {
50
50
  checkUpdate: asyncNoop,
51
51
  switchVersion: asyncNoop,
52
52
  switchVersionLater: asyncNoop,
package/src/core.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
2
  import { UpdateError } from './error';
3
3
  import i18n from './i18n';
4
- import { emptyModule, error, log } from './utils';
4
+ import { createNonSecureId, emptyModule, error, log } from './utils';
5
5
 
6
6
  // RN's JS global object. Declared locally because RN 0.87's own typings no
7
7
  // longer define `global` (only @types/node did), and this source is
@@ -35,6 +35,34 @@ if (!PushyModule) {
35
35
  );
36
36
  }
37
37
 
38
+ /**
39
+ * `module[name]` as an always-async function (a synchronous bridge throw
40
+ * becomes a rejection), or undefined when the module lacks it: the JS layer
41
+ * can arrive via hot update onto an older binary whose native module predates
42
+ * the method. Always undefined on web, where the module is a noop Proxy that
43
+ * would pass any typeof check.
44
+ */
45
+ export const bindNativeMethod = (
46
+ module: Record<string, any>,
47
+ name: string
48
+ ): ((...args: any[]) => Promise<any>) | undefined => {
49
+ const method = module === emptyModule ? undefined : module[name];
50
+ if (typeof method !== 'function') {
51
+ return undefined;
52
+ }
53
+ return (...args) => {
54
+ try {
55
+ return Promise.resolve(method.apply(module, args));
56
+ } catch (e) {
57
+ return Promise.reject(e);
58
+ }
59
+ };
60
+ };
61
+
62
+ /** An optional PushyModule method, see bindNativeMethod. */
63
+ export const nativeMethod = (name: string) =>
64
+ bindNativeMethod(PushyModule, name);
65
+
38
66
  // On web PushyModule is a Proxy whose every property is a noop function, so
39
67
  // reading the constants off it would make each of them a function:
40
68
  // `isRolledBack` true (a fake rollback report on every page load), a uuid of
@@ -99,7 +127,6 @@ export const currentBundleSha256: string =
99
127
  : '';
100
128
 
101
129
  export const isFirstTime: boolean = PushyConstants.isFirstTime;
102
- export const isFirstTimeDebug: boolean = isFirstTime && isDebugChannel;
103
130
  export const rolledBackVersion: string = PushyConstants.rolledBackVersion;
104
131
  export const isRolledBack: boolean = !!rolledBackVersion;
105
132
 
@@ -121,9 +148,10 @@ let uuid = PushyConstants.uuid;
121
148
  // between its dedup window and the fetch, and a hash is never worth delaying
122
149
  // a check for.
123
150
  let bundleHash = '';
124
- // The JS layer can arrive via hot update onto an older binary whose native
125
- // module predates the method, hence the feature detect (on web the noop Proxy
126
- // would pass it, so web is excluded explicitly).
151
+ // Feature-detected like nativeMethod (older natives lack it, web has no native
152
+ // module), but called directly rather than through it: a synchronous throw
153
+ // means a broken native integration and must propagate at module load, not be
154
+ // turned into a logged rejection. Only an async failure is tolerated.
127
155
  if (!isWebPlatform && typeof PushyModule.getBundleHash === 'function') {
128
156
  Promise.resolve(PushyModule.getBundleHash())
129
157
  .then((hash: unknown) => {
@@ -156,7 +184,7 @@ export async function getCurrentVersionInfo(): Promise<{
156
184
  export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule);
157
185
 
158
186
  if (!uuid) {
159
- uuid = require('nanoid/non-secure').nanoid();
187
+ uuid = createNonSecureId();
160
188
  // If persisting fails the uuid drifts on every launch, which skews gray
161
189
  // release bucketing and inflates stats — log it instead of failing silently.
162
190
  // Web has nowhere to persist it (nothing runs there anyway).
@@ -171,7 +199,7 @@ export const cInfo = {
171
199
  rnu: require('../package.json').version,
172
200
  rn: RNVersion,
173
201
  // tvOS reports Platform.OS 'ios'; label it apart so the server can bucket
174
- // tvOS devices separately. Must match PushyOsName in RCTPushy.mm.
202
+ // tvOS devices separately. Must match PushyOsName in RCTPushyOrchestrator.mm.
175
203
  os: `${Platform.OS === 'ios' && Platform.isTV ? 'tvos' : Platform.OS} ${Platform.Version}`,
176
204
  uuid,
177
205
  };
@@ -181,7 +209,6 @@ log('bootup status', {
181
209
  currentVersion,
182
210
  currentVersionInfo,
183
211
  isFirstTime,
184
- isFirstTimeDebug,
185
212
  isDebugChannel,
186
213
  cInfo,
187
214
  });
package/src/endpoint.ts CHANGED
@@ -2,8 +2,6 @@ import { UpdateError } from './error';
2
2
  import i18n from './i18n';
3
3
  import { dedupeEndpoints, orderEndpointCandidates } from './updateFlowCore';
4
4
 
5
- export { dedupeEndpoints };
6
-
7
5
  export interface EndpointAttemptSuccess<T> {
8
6
  endpoint: string;
9
7
  value: T;
package/src/error.ts CHANGED
@@ -7,42 +7,13 @@
7
7
  * Android/iOS/Harmony modules — and flow through promise rejections onto the
8
8
  * `code` property, which toUpdateError() preserves.
9
9
  */
10
- export type UpdateErrorCode =
10
+ const UPDATE_ERROR_CODES = [
11
11
  // JS-layer codes
12
- | 'MODULE_NOT_LOADED'
13
- | 'APPKEY_REQUIRED'
14
- | 'NO_ENDPOINTS'
15
- | 'HTTP_STATUS'
16
- // A 2xx check response that is not a check verdict (schema gate).
17
- | 'INVALID_RESPONSE'
18
- | 'CHECK_FAILED'
19
- | 'DOWNLOAD_FAILED'
20
- | 'SWITCH_VERSION_FAILED'
21
- | 'MARK_SUCCESS_FAILED'
22
- | 'APK_INSTALL_PENDING'
23
- | 'APK_DOWNLOAD_FAILED'
24
- // A throw from a user-provided hook (e.g. beforeReload) — not an update
25
- // pipeline failure, and excluded from server-side patch-health telemetry.
26
- | 'USER_HOOK_ERROR'
27
- // A second client/provider in the same process — a hard integration error;
28
- // the SDK is a process-level singleton.
29
- | 'SINGLETON_VIOLATION'
30
- // Native codes (see cpp/patch_core/error_codes.h)
31
- | 'INVALID_OPTIONS'
32
- | 'PATCH_FAILED'
33
- | 'FILE_OPERATION_FAILED'
34
- | 'RESTART_FAILED'
35
- | 'RESET_FAILED'
36
- | 'INVALID_HASH_INFO'
37
- | 'UNSUPPORTED_PLATFORM'
38
- | 'APK_INSTALL_PERMISSION_REQUIRED'
39
- | 'APK_INSTALL_FAILED';
40
-
41
- const KNOWN_CODES = new Set<string>([
42
12
  'MODULE_NOT_LOADED',
43
13
  'APPKEY_REQUIRED',
44
14
  'NO_ENDPOINTS',
45
15
  'HTTP_STATUS',
16
+ // A 2xx check response that is not a check verdict (schema gate).
46
17
  'INVALID_RESPONSE',
47
18
  'CHECK_FAILED',
48
19
  'DOWNLOAD_FAILED',
@@ -50,8 +21,13 @@ const KNOWN_CODES = new Set<string>([
50
21
  'MARK_SUCCESS_FAILED',
51
22
  'APK_INSTALL_PENDING',
52
23
  'APK_DOWNLOAD_FAILED',
24
+ // A throw from a user-provided hook (e.g. beforeReload) — not an update
25
+ // pipeline failure, and excluded from server-side patch-health telemetry.
53
26
  'USER_HOOK_ERROR',
27
+ // A second client/provider in the same process — a hard integration error;
28
+ // the SDK is a process-level singleton.
54
29
  'SINGLETON_VIOLATION',
30
+ // Native codes (see cpp/patch_core/error_codes.h)
55
31
  'INVALID_OPTIONS',
56
32
  'PATCH_FAILED',
57
33
  'FILE_OPERATION_FAILED',
@@ -61,7 +37,12 @@ const KNOWN_CODES = new Set<string>([
61
37
  'UNSUPPORTED_PLATFORM',
62
38
  'APK_INSTALL_PERMISSION_REQUIRED',
63
39
  'APK_INSTALL_FAILED',
64
- ]);
40
+ ] as const;
41
+
42
+ /** A stable error code (see UPDATE_ERROR_CODES). */
43
+ export type UpdateErrorCode = (typeof UPDATE_ERROR_CODES)[number];
44
+
45
+ const KNOWN_CODES = new Set<string>(UPDATE_ERROR_CODES);
65
46
 
66
47
  /**
67
48
  * Narrow an arbitrary `code` property (axios' ERR_NETWORK, Node's
package/src/i18n.ts CHANGED
@@ -1,108 +1,43 @@
1
1
  import enTranslations from './locales/en';
2
2
  import zhTranslations from './locales/zh';
3
3
 
4
+ type Locale = 'zh' | 'en';
4
5
  type TranslationKey = keyof typeof zhTranslations | keyof typeof enTranslations;
5
6
  type TranslationValues = Record<string, string | number>;
6
7
 
8
+ const translations: Record<Locale, Record<string, string>> = {
9
+ zh: zhTranslations,
10
+ en: enTranslations,
11
+ };
12
+
7
13
  class I18n {
8
- private currentLocale: 'zh' | 'en' = 'en';
9
- private translations = {
10
- zh: zhTranslations,
11
- en: enTranslations,
12
- };
14
+ private currentLocale: Locale = 'en';
13
15
 
14
- /**
15
- * Set locale directly
16
- * @param locale - 'zh' or 'en'
17
- */
18
- setLocale(locale: 'zh' | 'en') {
16
+ setLocale(locale: Locale) {
19
17
  this.currentLocale = locale;
20
18
  }
21
19
 
22
20
  /**
23
- * Get current locale
24
- */
25
- getLocale(): 'zh' | 'en' {
26
- return this.currentLocale;
27
- }
28
-
29
- /**
30
- * Translate a key with optional interpolation
31
- * @param key - Translation key
32
- * @param values - Values for interpolation (optional)
33
- * @returns Translated string with interpolated values
21
+ * Translate a key, filling `{{name}}` placeholders from `values` (unknown
22
+ * placeholders are kept). A key missing from the current locale falls back
23
+ * to the other one, then to the key itself.
34
24
  */
35
25
  t(key: TranslationKey, values?: TranslationValues): string {
36
- const translation =
37
- this.translations[this.currentLocale][
38
- key as keyof (typeof this.translations)[typeof this.currentLocale]
39
- ];
40
-
41
- if (!translation) {
42
- // Fallback to the other locale if key not found
43
- const fallbackLocale = this.currentLocale === 'zh' ? 'en' : 'zh';
44
- const fallbackTranslation =
45
- this.translations[fallbackLocale][
46
- key as keyof (typeof this.translations)[typeof fallbackLocale]
47
- ];
48
-
49
- if (!fallbackTranslation) {
50
- // If still not found, return the key itself
51
- return String(key);
52
- }
53
-
54
- return this.interpolate(fallbackTranslation, values);
55
- }
56
-
57
- return this.interpolate(translation, values);
58
- }
59
-
60
- /**
61
- * Interpolate values into a string template
62
- * Supports {{key}} syntax
63
- * @param template - String template with {{key}} placeholders
64
- * @param values - Values to interpolate
65
- * @returns Interpolated string
66
- */
67
- private interpolate(template: string, values?: TranslationValues): string {
26
+ const fallbackLocale = this.currentLocale === 'zh' ? 'en' : 'zh';
27
+ const template =
28
+ translations[this.currentLocale][key] ||
29
+ translations[fallbackLocale][key] ||
30
+ String(key);
68
31
  if (!values) {
69
32
  return template;
70
33
  }
71
-
72
- return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
73
- const value = values[key];
34
+ return template.replace(/\{\{(\w+)\}\}/g, (match, name) => {
35
+ const value = values[name];
74
36
  return value !== undefined ? String(value) : match;
75
37
  });
76
38
  }
77
-
78
- /**
79
- * Add or update translations for a specific locale
80
- * @param locale - Target locale
81
- * @param translations - Translation object to merge
82
- */
83
- addTranslations(locale: 'zh' | 'en', translations: Record<string, string>) {
84
- this.translations[locale] = {
85
- ...this.translations[locale],
86
- ...translations,
87
- };
88
- }
89
39
  }
90
40
 
91
- // Create singleton instance
92
41
  const i18n = new I18n();
93
42
 
94
- // Export both the instance and the class for flexibility
95
- export { I18n, i18n };
96
43
  export default i18n;
97
-
98
- /**
99
- * Usage examples:
100
- *
101
- * // Direct locale setting (new preferred method)
102
- * i18n.setLocale('zh'); // Chinese
103
- * i18n.setLocale('en'); // English
104
- *
105
- * // Get translations
106
- * i18n.t('checking_update'); // Based on current locale
107
- * i18n.t('download_progress', { progress: 50 }); // With interpolation
108
- */
package/src/locales/en.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  export default {
2
2
  // Common messages
3
- checking_update: 'Checking for updates...',
4
3
  update_failed: 'Update failed',
5
4
 
6
- // Progress messages with interpolation
7
- download_progress: 'Download progress: {{progress}}%',
8
-
9
5
  // Alert messages
10
6
  alert_title: 'Notice',
11
7
  alert_update_ready: 'Download completed. Update now?',
package/src/locales/zh.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  export default {
2
2
  // Common messages
3
- checking_update: '正在检查更新...',
4
3
  update_failed: '更新失败',
5
4
 
6
- // Progress messages with interpolation
7
- download_progress: '下载进度: {{progress}}%',
8
-
9
5
  // Alert messages
10
6
  alert_title: '提示',
11
7
  alert_update_ready: '下载完毕,是否立即更新?',
package/src/provider.tsx CHANGED
@@ -22,6 +22,16 @@ import type { CheckResult, ProgressData, UpdateTestPayload } from './type';
22
22
  import { decideDownload, resolveCheckResult } from './updateFlowCore';
23
23
  import { assertWeb, isWeb, log, noop, parseQueryParams } from './utils';
24
24
 
25
+ // A context action applied to the hash of the given (default: the last
26
+ // checked) update info; a no-op without one.
27
+ const forUpdateHash =
28
+ <T,>(
29
+ updateInfoRef: { current?: CheckResult },
30
+ apply: (hash: string) => Promise<T>
31
+ ) =>
32
+ async (info: CheckResult | undefined = updateInfoRef.current) =>
33
+ info?.hash ? apply(info.hash) : undefined;
34
+
25
35
  export const UpdateProvider = ({
26
36
  client,
27
37
  children,
@@ -100,22 +110,32 @@ export const UpdateProvider = ({
100
110
  [client, alertError]
101
111
  );
102
112
 
103
- const switchVersion = useCallback(
104
- async (info: CheckResult | undefined = updateInfoRef.current) => {
105
- if (info?.hash) {
106
- return client.switchVersion(info.hash);
107
- }
108
- },
113
+ const switchVersion = useMemo(
114
+ () => forUpdateHash(updateInfoRef, client.switchVersion),
109
115
  [client]
110
116
  );
111
117
 
112
- const switchVersionLater = useCallback(
113
- async (info: CheckResult | undefined = updateInfoRef.current) => {
114
- if (info?.hash) {
115
- return client.switchVersionLater(info.hash);
118
+ const switchVersionLater = useMemo(
119
+ () => forUpdateHash(updateInfoRef, client.switchVersionLater),
120
+ [client]
121
+ );
122
+
123
+ // Client pipeline errors were already surfaced via the onError
124
+ // subscription; errors thrown by user hooks (beforeCheckUpdate,
125
+ // afterDownloadUpdate) bypass the pipeline and are surfaced here. Asking
126
+ // the client instead of checking `e.code` matters: axios/system errors
127
+ // carry their own code without ever entering the pipeline.
128
+ const surfaceCaughtError = useCallback(
129
+ (e: any, title: string) => {
130
+ if (!client.wasEmitted(e)) {
131
+ setLastError(e);
132
+ alertError(title, e.message);
133
+ }
134
+ if (options.throwError) {
135
+ throw e;
116
136
  }
117
137
  },
118
- [client]
138
+ [client, options, alertError]
119
139
  );
120
140
 
121
141
  const downloadUpdate = useCallback(
@@ -162,27 +182,17 @@ export const UpdateProvider = ({
162
182
  ]);
163
183
  return true;
164
184
  } catch (e: any) {
165
- // Client pipeline errors were already surfaced via the onError
166
- // subscription; errors thrown by user hooks (afterDownloadUpdate)
167
- // bypass the pipeline and are surfaced here. Asking the client
168
- // instead of checking `e.code` matters: axios/system errors carry
169
- // their own code without ever entering the pipeline.
170
- if (!client.wasEmitted(e)) {
171
- setLastError(e);
172
- alertError(client.t('update_failed'), e.message);
173
- }
174
- if (options.throwError) {
175
- throw e;
176
- }
185
+ surfaceCaughtError(e, client.t('update_failed'));
177
186
  return false;
178
187
  }
179
188
  },
180
- [client, options, alertUpdate, alertError]
189
+ [client, options, alertUpdate, surfaceCaughtError]
181
190
  );
182
191
 
183
192
  const downloadAndInstallApk = useCallback(
184
193
  async (downloadUrl: string) => {
185
- if (Platform.OS === 'android' && downloadUrl) {
194
+ // The client itself is a no-op off Android.
195
+ if (downloadUrl) {
186
196
  await client.downloadAndInstallApk(downloadUrl, setProgress);
187
197
  }
188
198
  },
@@ -219,16 +229,7 @@ export const UpdateProvider = ({
219
229
  try {
220
230
  rootInfo = await client.checkUpdate(extra);
221
231
  } catch (e: any) {
222
- // Client pipeline errors were already surfaced via the onError
223
- // subscription; errors thrown by user hooks (beforeCheckUpdate)
224
- // bypass the pipeline and are surfaced here (see wasEmitted).
225
- if (!client.wasEmitted(e)) {
226
- setLastError(e);
227
- alertError(client.t('error_update_check_failed'), e.message);
228
- }
229
- if (options.throwError) {
230
- throw e;
231
- }
232
+ surfaceCaughtError(e, client.t('error_update_check_failed'));
232
233
  return;
233
234
  }
234
235
  if (!rootInfo) {
@@ -354,13 +355,14 @@ export const UpdateProvider = ({
354
355
  client,
355
356
  options,
356
357
  alertUpdate,
357
- alertError,
358
+ surfaceCaughtError,
358
359
  openExpiredDownload,
359
360
  downloadUpdate,
360
361
  ]
361
362
  );
362
363
 
363
- const markSuccess = client.markSuccess;
364
+ // Stable arrow-function properties of the (pinned) client.
365
+ const { markSuccess, restartApp, resetToPackagedBundle } = client;
364
366
 
365
367
  // The lifecycle effects below key on the primitive option values they read
366
368
  // (re-read on every render, i.e. after every setOptions), so a setOptions
@@ -528,16 +530,15 @@ export const UpdateProvider = ({
528
530
  [parseTestPayload]
529
531
  );
530
532
 
531
- const restartApp = useCallback(async () => {
532
- return client.restartApp();
533
- }, [client]);
534
-
535
- const resetToPackagedBundle = useCallback(
536
- async (resetOptions?: { restart?: boolean }) => {
537
- return client.resetToPackagedBundle(resetOptions);
538
- },
539
- [client]
540
- );
533
+ // parseTestPayload's identity follows checkUpdate (and through it the
534
+ // update strategy callbacks); the Linking subscription reads it through a
535
+ // ref so it is set up once instead of re-running — and replaying the
536
+ // initial URL — on every such change.
537
+ const latestParseTestPayload = useRef(parseTestPayload);
538
+ useEffect(() => {
539
+ latestParseTestPayload.current = parseTestPayload;
540
+ }, [parseTestPayload]);
541
+ const initialUrlHandled = useRef(false);
541
542
 
542
543
  useEffect(() => {
543
544
  if (!assertWeb()) {
@@ -552,25 +553,31 @@ export const UpdateProvider = ({
552
553
  // malformed deep link must not become an unhandled rejection / a throw
553
554
  // inside the 'url' event handler.
554
555
  const params = parseQueryParams(url);
555
- parseTestPayload({
556
+ latestParseTestPayload.current({
556
557
  type: params.type ?? null,
557
558
  data: params.data ?? null,
558
559
  });
559
560
  };
560
561
 
561
- Linking.getInitialURL().then(parseLinking).catch(noop);
562
+ // The launch URL is one event: a remount (StrictMode included) must not
563
+ // replay it.
564
+ if (!initialUrlHandled.current) {
565
+ initialUrlHandled.current = true;
566
+ Linking.getInitialURL().then(parseLinking).catch(noop);
567
+ }
562
568
  const linkingHandler = ({ url }: { url: string }) => {
563
569
  parseLinking(url);
564
570
  };
565
571
  const linkingListener = Linking.addEventListener('url', linkingHandler);
566
572
  return () => {
567
- if ('removeEventListener' in Linking) {
568
- (Linking as any).removeEventListener('url', linkingHandler);
569
- } else {
573
+ // RN < 0.65 returns no subscription, only removeEventListener.
574
+ if (typeof linkingListener?.remove === 'function') {
570
575
  linkingListener.remove();
576
+ } else {
577
+ (Linking as any).removeEventListener('url', linkingHandler);
571
578
  }
572
579
  };
573
- }, [parseTestPayload]);
580
+ }, []);
574
581
 
575
582
  // progress lives in its own context (see context.ts), so this value only
576
583
  // changes when the update state itself changes, not on every progress tick.
package/src/telemetry.ts CHANGED
@@ -14,19 +14,6 @@ export type ServerEventType =
14
14
  // Failure detail is stored server-side in a VARCHAR(512); stay under it.
15
15
  export const MAX_DETAIL_LENGTH = 500;
16
16
 
17
- export interface ServerReportPayload {
18
- type: ServerEventType;
19
- hash: string;
20
- packageVersion?: string;
21
- cInfo?: {
22
- rnu?: string;
23
- rn?: string;
24
- os?: string;
25
- uuid?: string;
26
- };
27
- detail?: string;
28
- }
29
-
30
17
  /**
31
18
  * Maps a local report event to the server-side aggregate type, or undefined
32
19
  * for events the server does not collect. A DOWNLOAD_FAILED whose underlying
package/src/type.ts CHANGED
@@ -41,21 +41,6 @@ interface RootResult {
41
41
  bundleStatus?: 'matched' | 'rebuiltSameJs' | 'unknownBundle';
42
42
  }
43
43
 
44
- export type UpToDateCheckResult = RootResult & {
45
- upToDate: true;
46
- };
47
-
48
- export type UpdateAvailableCheckResult = RootResult &
49
- Partial<VersionInfo> & {
50
- update: true;
51
- expVersion?: VersionInfo;
52
- };
53
-
54
- export type ExpiredCheckResult = RootResult & {
55
- expired: true;
56
- downloadUrl: string;
57
- };
58
-
59
44
  export type CheckResult = RootResult &
60
45
  Partial<VersionInfo> & {
61
46
  expVersion?: VersionInfo;