@hot-updater/react-native 0.15.1 → 0.16.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.
@@ -50,6 +50,10 @@ public abstract class NativeHotUpdaterSpec extends ReactContextBaseJavaModule im
50
50
  @DoNotStrip
51
51
  public abstract void getAppVersion(Promise promise);
52
52
 
53
+ @ReactMethod
54
+ @DoNotStrip
55
+ public abstract void setChannel(String channel, Promise promise);
56
+
53
57
  @ReactMethod
54
58
  @DoNotStrip
55
59
  public abstract void addListener(String eventName);
@@ -68,7 +72,10 @@ public abstract class NativeHotUpdaterSpec extends ReactContextBaseJavaModule im
68
72
  Set<String> obligatoryFlowConstants = new HashSet<>(Arrays.asList(
69
73
  "MIN_BUNDLE_ID"
70
74
  ));
71
- Set<String> optionalFlowConstants = new HashSet<>();
75
+ Set<String> optionalFlowConstants = new HashSet<>(Arrays.asList(
76
+ "APP_VERSION",
77
+ "CHANNEL"
78
+ ));
72
79
  Set<String> undeclaredConstants = new HashSet<>(constants.keySet());
73
80
  undeclaredConstants.removeAll(obligatoryFlowConstants);
74
81
  undeclaredConstants.removeAll(optionalFlowConstants);
@@ -27,6 +27,11 @@ static facebook::jsi::Value __hostFunction_NativeHotUpdaterSpecJSI_getAppVersion
27
27
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "getAppVersion", "(Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
28
28
  }
29
29
 
30
+ static facebook::jsi::Value __hostFunction_NativeHotUpdaterSpecJSI_setChannel(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
31
+ static jmethodID cachedMethodId = nullptr;
32
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "setChannel", "(Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
33
+ }
34
+
30
35
  static facebook::jsi::Value __hostFunction_NativeHotUpdaterSpecJSI_addListener(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
31
36
  static jmethodID cachedMethodId = nullptr;
32
37
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "addListener", "(Ljava/lang/String;)V", args, count, cachedMethodId);
@@ -47,6 +52,7 @@ NativeHotUpdaterSpecJSI::NativeHotUpdaterSpecJSI(const JavaTurboModule::InitPara
47
52
  methodMap_["reload"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterSpecJSI_reload};
48
53
  methodMap_["updateBundle"] = MethodMetadata {2, __hostFunction_NativeHotUpdaterSpecJSI_updateBundle};
49
54
  methodMap_["getAppVersion"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterSpecJSI_getAppVersion};
55
+ methodMap_["setChannel"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterSpecJSI_setChannel};
50
56
  methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterSpecJSI_addListener};
51
57
  methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterSpecJSI_removeListeners};
52
58
  methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterSpecJSI_getConstants};
@@ -29,6 +29,12 @@ static jsi::Value __hostFunction_NativeHotUpdaterCxxSpecJSI_getAppVersion(jsi::R
29
29
  rt
30
30
  );
31
31
  }
32
+ static jsi::Value __hostFunction_NativeHotUpdaterCxxSpecJSI_setChannel(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
33
+ return static_cast<NativeHotUpdaterCxxSpecJSI *>(&turboModule)->setChannel(
34
+ rt,
35
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
36
+ );
37
+ }
32
38
  static jsi::Value __hostFunction_NativeHotUpdaterCxxSpecJSI_addListener(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
33
39
  static_cast<NativeHotUpdaterCxxSpecJSI *>(&turboModule)->addListener(
34
40
  rt,
@@ -54,6 +60,7 @@ NativeHotUpdaterCxxSpecJSI::NativeHotUpdaterCxxSpecJSI(std::shared_ptr<CallInvok
54
60
  methodMap_["reload"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterCxxSpecJSI_reload};
55
61
  methodMap_["updateBundle"] = MethodMetadata {2, __hostFunction_NativeHotUpdaterCxxSpecJSI_updateBundle};
56
62
  methodMap_["getAppVersion"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterCxxSpecJSI_getAppVersion};
63
+ methodMap_["setChannel"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterCxxSpecJSI_setChannel};
57
64
  methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterCxxSpecJSI_addListener};
58
65
  methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterCxxSpecJSI_removeListeners};
59
66
  methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterCxxSpecJSI_getConstants};
@@ -23,6 +23,7 @@ public:
23
23
  virtual void reload(jsi::Runtime &rt) = 0;
24
24
  virtual jsi::Value updateBundle(jsi::Runtime &rt, jsi::String bundleId, jsi::String zipUrl) = 0;
25
25
  virtual jsi::Value getAppVersion(jsi::Runtime &rt) = 0;
26
+ virtual jsi::Value setChannel(jsi::Runtime &rt, jsi::String channel) = 0;
26
27
  virtual void addListener(jsi::Runtime &rt, jsi::String eventName) = 0;
27
28
  virtual void removeListeners(jsi::Runtime &rt, double count) = 0;
28
29
  virtual jsi::Object getConstants(jsi::Runtime &rt) = 0;
@@ -76,6 +77,14 @@ private:
76
77
  return bridging::callFromJs<jsi::Value>(
77
78
  rt, &T::getAppVersion, jsInvoker_, instance_);
78
79
  }
80
+ jsi::Value setChannel(jsi::Runtime &rt, jsi::String channel) override {
81
+ static_assert(
82
+ bridging::getParameterCount(&T::setChannel) == 2,
83
+ "Expected setChannel(...) to have 2 parameters");
84
+
85
+ return bridging::callFromJs<jsi::Value>(
86
+ rt, &T::setChannel, jsInvoker_, instance_, std::move(channel));
87
+ }
79
88
  void addListener(jsi::Runtime &rt, jsi::String eventName) override {
80
89
  static_assert(
81
90
  bridging::getParameterCount(&T::addListener) == 2,
@@ -31,16 +31,30 @@ class HotUpdater : ReactPackage {
31
31
  return packageInfo.versionName
32
32
  }
33
33
 
34
+ @Volatile
35
+ private var prefsInstance: HotUpdaterPrefs? = null
36
+
37
+ @Volatile
38
+ private var cachedAppVersion: String? = null
39
+
40
+ private fun getPrefs(context: Context): HotUpdaterPrefs {
41
+ val appContext = context.applicationContext
42
+ val currentAppVersion = getAppVersion(appContext) ?: "unknown"
43
+ synchronized(this) {
44
+ if (prefsInstance == null || cachedAppVersion != currentAppVersion) {
45
+ prefsInstance = HotUpdaterPrefs(appContext, currentAppVersion)
46
+ cachedAppVersion = currentAppVersion
47
+ }
48
+ return prefsInstance!!
49
+ }
50
+ }
51
+
34
52
  private fun setBundleURL(
35
53
  context: Context,
36
54
  bundleURL: String?,
37
55
  ) {
38
- val sharedPreferences =
39
- context.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
40
- with(sharedPreferences.edit()) {
41
- putString("HotUpdaterBundleURL", bundleURL)
42
- apply()
43
- }
56
+ val updaterPrefs = getPrefs(context)
57
+ updaterPrefs.setItem("HotUpdaterBundleURL", bundleURL)
44
58
 
45
59
  if (bundleURL == null) {
46
60
  return
@@ -98,22 +112,33 @@ class HotUpdater : ReactPackage {
98
112
  }
99
113
 
100
114
  fun getJSBundleFile(context: Context): String {
101
- val sharedPreferences =
102
- context.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
103
- val urlString = sharedPreferences.getString("HotUpdaterBundleURL", null)
115
+ val updaterPrefs = getPrefs(context)
116
+ val urlString = updaterPrefs.getItem("HotUpdaterBundleURL")
104
117
  if (urlString.isNullOrEmpty()) {
105
118
  return "assets://index.android.bundle"
106
119
  }
107
120
 
108
121
  val file = File(urlString)
109
122
  if (!file.exists()) {
110
- setBundleURL(context, null)
123
+ updaterPrefs.setItem("HotUpdaterBundleURL", null)
111
124
  return "assets://index.android.bundle"
112
125
  }
113
-
114
126
  return urlString
115
127
  }
116
128
 
129
+ fun setChannel(
130
+ context: Context,
131
+ channel: String,
132
+ ) {
133
+ val updaterPrefs = getPrefs(context)
134
+ updaterPrefs.setItem("HotUpdaterChannel", channel)
135
+ }
136
+
137
+ fun getChannel(context: Context): String? {
138
+ val updaterPrefs = getPrefs(context)
139
+ return updaterPrefs.getItem("HotUpdaterChannel")
140
+ }
141
+
117
142
  suspend fun updateBundle(
118
143
  context: Context,
119
144
  bundleId: String,
@@ -137,7 +162,6 @@ class HotUpdater : ReactPackage {
137
162
  Log.d("HotUpdater", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
138
163
  val existingIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
139
164
  if (existingIndexFile != null) {
140
- // Update directory modification time to current time after update
141
165
  finalBundleDir.setLastModified(System.currentTimeMillis())
142
166
  setBundleURL(context, existingIndexFile.absolutePath)
143
167
  cleanupOldBundles(bundleStoreDir)
@@ -225,7 +249,6 @@ class HotUpdater : ReactPackage {
225
249
  return false
226
250
  }
227
251
 
228
- // Move (or copy) contents from temp folder to finalBundleDir
229
252
  if (finalBundleDir.exists()) {
230
253
  finalBundleDir.deleteRecursively()
231
254
  }
@@ -241,41 +264,29 @@ class HotUpdater : ReactPackage {
241
264
  return false
242
265
  }
243
266
 
244
- // Update final bundle directory modification time to current time after bundle update
245
267
  finalBundleDir.setLastModified(System.currentTimeMillis())
246
-
247
268
  val bundlePath = finalIndexFile.absolutePath
248
269
  Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
249
270
  setBundleURL(context, bundlePath)
250
-
251
- // Clean up old bundles in the bundle store to keep only up to 2 bundles
252
271
  cleanupOldBundles(bundleStoreDir)
253
-
254
- // Clean up temp directory
255
272
  tempDir.deleteRecursively()
256
273
 
257
274
  Log.d("HotUpdater", "Downloaded and extracted file successfully.")
258
275
  return true
259
276
  }
260
277
 
261
- // Helper function to delete old bundles, keeping only up to 2 bundles in the bundle-store folder
262
278
  private fun cleanupOldBundles(bundleStoreDir: File) {
263
- // Get list of all directories in bundle-store folder
264
279
  val bundles = bundleStoreDir.listFiles { file -> file.isDirectory }?.toList() ?: return
265
-
266
- // Sort by last modified time in descending order to keep most recently updated bundles at the top
267
280
  val sortedBundles = bundles.sortedByDescending { it.lastModified() }
268
-
269
- // Delete all bundles except the top 2
270
- if (sortedBundles.size > 2) {
271
- sortedBundles.drop(2).forEach { oldBundle ->
281
+ if (sortedBundles.size > 1) {
282
+ sortedBundles.drop(1).forEach { oldBundle ->
272
283
  Log.d("HotUpdater", "Removing old bundle: ${oldBundle.name}")
273
284
  oldBundle.deleteRecursively()
274
285
  }
275
286
  }
276
287
  }
277
288
 
278
- fun getMinBundleId(context: Context): String =
289
+ fun getMinBundleId(): String =
279
290
  try {
280
291
  val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
281
292
  val bytes =
@@ -0,0 +1,42 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import java.io.File
6
+
7
+ /**
8
+ * A class that manages SharedPreferences based on the app version.
9
+ * Externally, only getItem(key) and setItem(key, value) can be used.
10
+ * It constructs the prefs filename based on the app version passed in the constructor,
11
+ * and deletes previous files that don't match the current version during initialization.
12
+ */
13
+ class HotUpdaterPrefs(
14
+ private val context: Context,
15
+ private val appVersion: String,
16
+ ) {
17
+ private val prefs: SharedPreferences
18
+
19
+ init {
20
+ val prefsName = "HotUpdaterPrefs_$appVersion"
21
+
22
+ val sharedPrefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
23
+ if (sharedPrefsDir.exists() && sharedPrefsDir.isDirectory) {
24
+ sharedPrefsDir.listFiles()?.forEach { file ->
25
+ if (file.name.startsWith("HotUpdaterPrefs_") && file.name != "$prefsName.xml") {
26
+ file.delete()
27
+ }
28
+ }
29
+ }
30
+
31
+ prefs = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
32
+ }
33
+
34
+ fun getItem(key: String): String? = prefs.getString(key, null)
35
+
36
+ fun setItem(
37
+ key: String,
38
+ value: String?,
39
+ ) {
40
+ prefs.edit().putString(key, value).apply()
41
+ }
42
+ }
@@ -26,6 +26,15 @@ class HotUpdaterModule internal constructor(
26
26
  promise.resolve(HotUpdater.getAppVersion(mReactApplicationContext))
27
27
  }
28
28
 
29
+ @ReactMethod
30
+ override fun setChannel(
31
+ channel: String,
32
+ promise: Promise,
33
+ ) {
34
+ HotUpdater.setChannel(mReactApplicationContext, channel)
35
+ promise.resolve(null)
36
+ }
37
+
29
38
  @ReactMethod
30
39
  override fun updateBundle(
31
40
  bundleId: String,
@@ -56,15 +65,21 @@ class HotUpdaterModule internal constructor(
56
65
 
57
66
  override fun getTypedExportedConstants(): Map<String, Any?> {
58
67
  val constants: MutableMap<String, Any?> = HashMap()
59
- constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId(mReactApplicationContext)
68
+ constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId()
69
+ constants["APP_VERSION"] = HotUpdater.getAppVersion(mReactApplicationContext)
70
+ constants["CHANNEL"] = HotUpdater.getChannel(mReactApplicationContext)
60
71
  return constants
61
72
  }
62
73
 
63
- override fun addListener(eventName: String?) {
74
+ override fun addListener(
75
+ @Suppress("UNUSED_PARAMETER") eventName: String?,
76
+ ) {
64
77
  // No-op
65
78
  }
66
79
 
67
- override fun removeListeners(count: Double) {
80
+ override fun removeListeners(
81
+ @Suppress("UNUSED_PARAMETER") count: Double,
82
+ ) {
68
83
  // No-op
69
84
  }
70
85
 
@@ -26,6 +26,15 @@ class HotUpdaterModule internal constructor(
26
26
  promise.resolve(HotUpdater.getAppVersion(mReactApplicationContext))
27
27
  }
28
28
 
29
+ @ReactMethod
30
+ override fun setChannel(
31
+ channel: String,
32
+ promise: Promise,
33
+ ) {
34
+ HotUpdater.setChannel(mReactApplicationContext, channel)
35
+ promise.resolve(null)
36
+ }
37
+
29
38
  @ReactMethod
30
39
  override fun updateBundle(
31
40
  bundleId: String,
@@ -55,18 +64,24 @@ class HotUpdaterModule internal constructor(
55
64
  }
56
65
 
57
66
  @ReactMethod
58
- fun addListener(eventName: String?) {
67
+ fun addListener(
68
+ @Suppress("UNUSED_PARAMETER") eventName: String?,
69
+ ) {
59
70
  // No-op
60
71
  }
61
72
 
62
73
  @ReactMethod
63
- fun removeListeners(count: Double) {
74
+ fun removeListeners(
75
+ @Suppress("UNUSED_PARAMETER") count: Double,
76
+ ) {
64
77
  // No-op
65
78
  }
66
79
 
67
80
  override fun getConstants(): Map<String, Any?> {
68
81
  val constants: MutableMap<String, Any?> = HashMap()
69
- constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId(mReactApplicationContext)
82
+ constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId()
83
+ constants["APP_VERSION"] = HotUpdater.getAppVersion(mReactApplicationContext)
84
+ constants["CHANNEL"] = HotUpdater.getChannel(mReactApplicationContext)
70
85
  return constants
71
86
  }
72
87
 
@@ -16,4 +16,9 @@ abstract class HotUpdaterSpec internal constructor(
16
16
  abstract fun reload()
17
17
 
18
18
  abstract fun getAppVersion(promise: Promise)
19
+
20
+ abstract fun setChannel(
21
+ channel: String,
22
+ promise: Promise,
23
+ )
19
24
  }
@@ -2,5 +2,10 @@ export interface CheckForUpdateOptions {
2
2
  source: string;
3
3
  requestHeaders?: Record<string, string>;
4
4
  onError?: (error: Error) => void;
5
+ /**
6
+ * The timeout duration for the request.
7
+ * @default 5000
8
+ */
9
+ requestTimeout?: number;
5
10
  }
6
11
  export declare function checkForUpdate(options: CheckForUpdateOptions): Promise<import("@hot-updater/core").AppUpdateInfo | null>;
@@ -1,2 +1,2 @@
1
1
  import type { AppUpdateInfo, GetBundlesArgs } from "@hot-updater/core";
2
- export declare const fetchUpdateInfo: (source: string, { appVersion, bundleId, platform, minBundleId, channel }: GetBundlesArgs, requestHeaders?: Record<string, string>, onError?: (error: Error) => void) => Promise<AppUpdateInfo | null>;
2
+ export declare const fetchUpdateInfo: (source: string, { appVersion, bundleId, platform, minBundleId, channel }: GetBundlesArgs, requestHeaders?: Record<string, string>, onError?: (error: Error) => void, requestTimeout?: number) => Promise<AppUpdateInfo | null>;
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export declare const HotUpdater: {
34
34
  /**
35
35
  * Fetches the current app version.
36
36
  */
37
- getAppVersion: () => Promise<string | null>;
37
+ getAppVersion: () => string | null;
38
38
  /**
39
39
  * Fetches the current bundle ID of the app.
40
40
  */
@@ -58,6 +58,10 @@ export declare const HotUpdater: {
58
58
  * ```
59
59
  */
60
60
  getChannel: () => string | null;
61
+ /**
62
+ * Sets the channel for the app.
63
+ */
64
+ setChannel: (channel: string) => Promise<any>;
61
65
  /**
62
66
  * Adds a listener to HotUpdater events.
63
67
  *
package/dist/index.js CHANGED
@@ -66,9 +66,14 @@ var __webpack_exports__ = {};
66
66
  this.name = "HotUpdaterError";
67
67
  }
68
68
  }
69
- const fetchUpdateInfo = async (source, { appVersion, bundleId, platform, minBundleId, channel }, requestHeaders, onError)=>{
69
+ const fetchUpdateInfo = async (source, { appVersion, bundleId, platform, minBundleId, channel }, requestHeaders, onError, requestTimeout = 5000)=>{
70
+ const controller = new AbortController();
71
+ const timeoutId = setTimeout(()=>{
72
+ controller.abort();
73
+ }, requestTimeout);
70
74
  try {
71
75
  const response = await fetch(source, {
76
+ signal: controller.signal,
72
77
  headers: {
73
78
  "Content-Type": "application/json",
74
79
  "x-app-platform": platform,
@@ -83,9 +88,14 @@ var __webpack_exports__ = {};
83
88
  ...requestHeaders
84
89
  }
85
90
  });
91
+ clearTimeout(timeoutId);
86
92
  if (200 !== response.status) throw new Error(response.statusText);
87
93
  return response.json();
88
94
  } catch (error) {
95
+ if ("AbortError" === error.name) {
96
+ onError?.(new Error("Request timed out"));
97
+ return null;
98
+ }
89
99
  onError?.(error);
90
100
  return null;
91
101
  }
@@ -114,7 +124,10 @@ var __webpack_exports__ = {};
114
124
  };
115
125
  };
116
126
  const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
117
- const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
127
+ const getAppVersion = ()=>{
128
+ const constants = HotUpdaterNative.getConstants();
129
+ return constants?.APP_VERSION ?? null;
130
+ };
118
131
  const reload = ()=>{
119
132
  requestAnimationFrame(()=>{
120
133
  HotUpdaterNative.reload();
@@ -125,7 +138,11 @@ var __webpack_exports__ = {};
125
138
  return constants.MIN_BUNDLE_ID;
126
139
  };
127
140
  const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID === NIL_UUID ? getMinBundleId() : HotUpdater.HOT_UPDATER_BUNDLE_ID;
128
- const getChannel = ()=>HotUpdater.CHANNEL;
141
+ const setChannel = async (channel)=>HotUpdaterNative.setChannel(channel);
142
+ const getChannel = ()=>{
143
+ const constants = HotUpdaterNative.getConstants();
144
+ return constants?.CHANNEL ?? HotUpdater.CHANNEL ?? null;
145
+ };
129
146
  async function checkForUpdate(options) {
130
147
  if (__DEV__) return null;
131
148
  if (![
@@ -135,7 +152,7 @@ var __webpack_exports__ = {};
135
152
  options.onError?.(new HotUpdaterError("HotUpdater is only supported on iOS and Android"));
136
153
  return null;
137
154
  }
138
- const currentAppVersion = await getAppVersion();
155
+ const currentAppVersion = getAppVersion();
139
156
  const platform = external_react_native_.Platform.OS;
140
157
  const currentBundleId = getBundleId();
141
158
  const minBundleId = getMinBundleId();
@@ -150,7 +167,7 @@ var __webpack_exports__ = {};
150
167
  platform,
151
168
  minBundleId,
152
169
  channel: channel ?? void 0
153
- }, options.requestHeaders, options.onError);
170
+ }, options.requestHeaders, options.onError, options.requestTimeout);
154
171
  }
155
172
  const runUpdateProcess = async ({ reloadOnForceUpdate = true, ...checkForUpdateOptions })=>{
156
173
  const updateInfo = await checkForUpdate(checkForUpdateOptions);
@@ -304,6 +321,7 @@ var __webpack_exports__ = {};
304
321
  getBundleId: getBundleId,
305
322
  getMinBundleId: getMinBundleId,
306
323
  getChannel: getChannel,
324
+ setChannel: setChannel,
307
325
  addListener: addListener,
308
326
  checkForUpdate: checkForUpdate,
309
327
  runUpdateProcess: runUpdateProcess,
package/dist/index.mjs CHANGED
@@ -41,9 +41,14 @@ class HotUpdaterError extends Error {
41
41
  this.name = "HotUpdaterError";
42
42
  }
43
43
  }
44
- const fetchUpdateInfo = async (source, { appVersion, bundleId, platform, minBundleId, channel }, requestHeaders, onError)=>{
44
+ const fetchUpdateInfo = async (source, { appVersion, bundleId, platform, minBundleId, channel }, requestHeaders, onError, requestTimeout = 5000)=>{
45
+ const controller = new AbortController();
46
+ const timeoutId = setTimeout(()=>{
47
+ controller.abort();
48
+ }, requestTimeout);
45
49
  try {
46
50
  const response = await fetch(source, {
51
+ signal: controller.signal,
47
52
  headers: {
48
53
  "Content-Type": "application/json",
49
54
  "x-app-platform": platform,
@@ -58,9 +63,14 @@ const fetchUpdateInfo = async (source, { appVersion, bundleId, platform, minBund
58
63
  ...requestHeaders
59
64
  }
60
65
  });
66
+ clearTimeout(timeoutId);
61
67
  if (200 !== response.status) throw new Error(response.statusText);
62
68
  return response.json();
63
69
  } catch (error) {
70
+ if ("AbortError" === error.name) {
71
+ onError?.(new Error("Request timed out"));
72
+ return null;
73
+ }
64
74
  onError?.(error);
65
75
  return null;
66
76
  }
@@ -89,7 +99,10 @@ const addListener = (eventName, listener)=>{
89
99
  };
90
100
  };
91
101
  const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
92
- const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
102
+ const getAppVersion = ()=>{
103
+ const constants = HotUpdaterNative.getConstants();
104
+ return constants?.APP_VERSION ?? null;
105
+ };
93
106
  const reload = ()=>{
94
107
  requestAnimationFrame(()=>{
95
108
  HotUpdaterNative.reload();
@@ -100,7 +113,11 @@ const getMinBundleId = ()=>{
100
113
  return constants.MIN_BUNDLE_ID;
101
114
  };
102
115
  const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID === NIL_UUID ? getMinBundleId() : HotUpdater.HOT_UPDATER_BUNDLE_ID;
103
- const getChannel = ()=>HotUpdater.CHANNEL;
116
+ const setChannel = async (channel)=>HotUpdaterNative.setChannel(channel);
117
+ const getChannel = ()=>{
118
+ const constants = HotUpdaterNative.getConstants();
119
+ return constants?.CHANNEL ?? HotUpdater.CHANNEL ?? null;
120
+ };
104
121
  async function checkForUpdate(options) {
105
122
  if (__DEV__) return null;
106
123
  if (![
@@ -110,7 +127,7 @@ async function checkForUpdate(options) {
110
127
  options.onError?.(new HotUpdaterError("HotUpdater is only supported on iOS and Android"));
111
128
  return null;
112
129
  }
113
- const currentAppVersion = await getAppVersion();
130
+ const currentAppVersion = getAppVersion();
114
131
  const platform = external_react_native_.Platform.OS;
115
132
  const currentBundleId = getBundleId();
116
133
  const minBundleId = getMinBundleId();
@@ -125,7 +142,7 @@ async function checkForUpdate(options) {
125
142
  platform,
126
143
  minBundleId,
127
144
  channel: channel ?? void 0
128
- }, options.requestHeaders, options.onError);
145
+ }, options.requestHeaders, options.onError, options.requestTimeout);
129
146
  }
130
147
  const runUpdateProcess = async ({ reloadOnForceUpdate = true, ...checkForUpdateOptions })=>{
131
148
  const updateInfo = await checkForUpdate(checkForUpdateOptions);
@@ -275,6 +292,7 @@ const src_HotUpdater = {
275
292
  getBundleId: getBundleId,
276
293
  getMinBundleId: getMinBundleId,
277
294
  getChannel: getChannel,
295
+ setChannel: setChannel,
278
296
  addListener: addListener,
279
297
  checkForUpdate: checkForUpdate,
280
298
  runUpdateProcess: runUpdateProcess,
package/dist/native.d.ts CHANGED
@@ -15,7 +15,7 @@ export declare const updateBundle: (bundleId: string, zipUrl: string | null) =>
15
15
  /**
16
16
  * Fetches the current app version.
17
17
  */
18
- export declare const getAppVersion: () => Promise<string | null>;
18
+ export declare const getAppVersion: () => string | null;
19
19
  /**
20
20
  * Reloads the app.
21
21
  */
@@ -34,4 +34,8 @@ export declare const getMinBundleId: () => string;
34
34
  * @returns {Promise<string>} Resolves with the current version id or null if not available.
35
35
  */
36
36
  export declare const getBundleId: () => string;
37
+ /**
38
+ * Sets the channel for the app.
39
+ */
40
+ export declare const setChannel: (channel: string) => Promise<any>;
37
41
  export declare const getChannel: () => string | null;
@@ -1,4 +1,5 @@
1
1
  #import "HotUpdater.h"
2
+ #import "HotUpdaterPrefs.h"
2
3
  #import <React/RCTReloadCommand.h>
3
4
  #import <SSZipArchive/SSZipArchive.h>
4
5
  #import <Foundation/NSURLSession.h>
@@ -8,7 +9,7 @@
8
9
  }
9
10
 
10
11
  + (BOOL)requiresMainQueueSetup {
11
- return YES;
12
+ return YES;
12
13
  }
13
14
 
14
15
  - (instancetype)init {
@@ -42,7 +43,6 @@ RCT_EXPORT_MODULE();
42
43
  }
43
44
 
44
45
  uint64_t buildTimestampMs = (uint64_t)([buildDate timeIntervalSince1970] * 1000.0);
45
-
46
46
  unsigned char bytes[16];
47
47
  bytes[0] = (buildTimestampMs >> 40) & 0xFF;
48
48
  bytes[1] = (buildTimestampMs >> 32) & 0xFF;
@@ -76,34 +76,51 @@ RCT_EXPORT_MODULE();
76
76
  return uuid;
77
77
  }
78
78
 
79
- - (NSDictionary *)constantsToExport {
80
- return @{ @"MIN_BUNDLE_ID": [self getMinBundleId] };
79
+ - (NSString *)getChannel {
80
+ HotUpdaterPrefs *prefs = [self getPrefs];
81
+ return [prefs getItemForKey:@"HotUpdaterChannel"];
81
82
  }
82
83
 
84
+ - (NSString *)getAppVersion {
85
+ NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
86
+ return appVersion;
87
+ }
83
88
 
84
- - (NSDictionary*) getConstants {
89
+ - (NSDictionary *)constantsToExport {
90
+ return @{
91
+ @"MIN_BUNDLE_ID": [self getMinBundleId] ?: [NSNull null],
92
+ @"APP_VERSION": [self getAppVersion] ?: [NSNull null],
93
+ @"CHANNEL": [self getChannel] ?: [NSNull null]
94
+ };
95
+ }
96
+
97
+ - (NSDictionary *)getConstants {
85
98
  return [self constantsToExport];
86
99
  }
87
100
 
101
+ #pragma mark - Convenience: HotUpdaterPrefs Instance
102
+
103
+ - (HotUpdaterPrefs *)getPrefs {
104
+ return [HotUpdaterPrefs sharedInstanceWithAppVersion:[self getAppVersion]];
105
+ }
88
106
 
89
107
  #pragma mark - Bundle URL Management
90
108
 
91
- - (NSString *)getAppVersion {
109
+ + (void)setChannel:(NSString *)channel {
92
110
  NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
93
- return appVersion;
111
+ HotUpdaterPrefs *prefs = [HotUpdaterPrefs sharedInstanceWithAppVersion:appVersion];
112
+ [prefs setItem:channel forKey:@"HotUpdaterChannel"];
94
113
  }
95
114
 
96
115
  - (void)setBundleURL:(NSString *)localPath {
97
116
  NSLog(@"Setting bundle URL: %@", localPath);
98
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
99
- [defaults setObject:localPath forKey:@"HotUpdaterBundleURL"];
100
- [defaults synchronize];
117
+ HotUpdaterPrefs *prefs = [self getPrefs];
118
+ [prefs setItem:localPath forKey:@"HotUpdaterBundleURL"];
101
119
  }
102
120
 
103
- + (NSURL *)cachedURLFromBundle {
104
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
105
- NSString *savedURLString = [defaults objectForKey:@"HotUpdaterBundleURL"];
106
-
121
+ - (NSURL *)cachedURLFromBundle {
122
+ HotUpdaterPrefs *prefs = [self getPrefs];
123
+ NSString *savedURLString = [prefs getItemForKey:@"HotUpdaterBundleURL"];
107
124
  if (savedURLString) {
108
125
  NSURL *bundleURL = [NSURL URLWithString:savedURLString];
109
126
  if (bundleURL && [[NSFileManager defaultManager] fileExistsAtPath:[bundleURL path]]) {
@@ -118,7 +135,9 @@ RCT_EXPORT_MODULE();
118
135
  }
119
136
 
120
137
  + (NSURL *)bundleURL {
121
- return [self cachedURLFromBundle] ?: [self fallbackURL];
138
+ HotUpdater *instance = [[HotUpdater alloc] init];
139
+ NSURL *url = [instance cachedURLFromBundle];
140
+ return url ? url : [self fallbackURL];
122
141
  }
123
142
 
124
143
  #pragma mark - Utility Methods
@@ -151,8 +170,8 @@ RCT_EXPORT_MODULE();
151
170
  [bundleDirs addObject:fullPath];
152
171
  }
153
172
  }
154
-
155
- // Sort in descending order by modification time (keep latest 2)
173
+
174
+ // Sort in descending order by modification time (keep latest 1)
156
175
  [bundleDirs sortUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
157
176
  NSDictionary *attr1 = [fileManager attributesOfItemAtPath:path1 error:nil];
158
177
  NSDictionary *attr2 = [fileManager attributesOfItemAtPath:path2 error:nil];
@@ -161,8 +180,8 @@ RCT_EXPORT_MODULE();
161
180
  return [date2 compare:date1];
162
181
  }];
163
182
 
164
- if (bundleDirs.count > 2) {
165
- NSArray *oldBundles = [bundleDirs subarrayWithRange:NSMakeRange(2, bundleDirs.count - 2)];
183
+ if (bundleDirs.count > 1) {
184
+ NSArray *oldBundles = [bundleDirs subarrayWithRange:NSMakeRange(1, bundleDirs.count - 1)];
166
185
  for (NSString *oldBundle in oldBundles) {
167
186
  NSError *delError = nil;
168
187
  if ([fileManager removeItemAtPath:oldBundle error:&delError]) {
@@ -185,7 +204,6 @@ RCT_EXPORT_MODULE();
185
204
  return;
186
205
  }
187
206
 
188
- // Set document directory path and bundle store path
189
207
  NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
190
208
  NSString *bundleStoreDir = [documentsPath stringByAppendingPathComponent:@"bundle-store"];
191
209
 
@@ -194,10 +212,8 @@ RCT_EXPORT_MODULE();
194
212
  [fileManager createDirectoryAtPath:bundleStoreDir withIntermediateDirectories:YES attributes:nil error:nil];
195
213
  }
196
214
 
197
- // Final bundle path (bundle-store/<bundleId>)
198
215
  NSString *finalBundleDir = [bundleStoreDir stringByAppendingPathComponent:bundleId];
199
216
 
200
- // Check if cached bundle exists
201
217
  if ([fileManager fileExistsAtPath:finalBundleDir]) {
202
218
  NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:finalBundleDir];
203
219
  NSString *foundBundle = nil;
@@ -208,7 +224,6 @@ RCT_EXPORT_MODULE();
208
224
  }
209
225
  }
210
226
  if (foundBundle) {
211
- // Update modification time of final bundle
212
227
  NSDictionary *attributes = @{NSFileModificationDate: [NSDate date]};
213
228
  [fileManager setAttributes:attributes ofItemAtPath:finalBundleDir error:nil];
214
229
  NSString *bundlePath = [finalBundleDir stringByAppendingPathComponent:foundBundle];
@@ -223,7 +238,7 @@ RCT_EXPORT_MODULE();
223
238
  [fileManager removeItemAtPath:finalBundleDir error:nil];
224
239
  }
225
240
  }
226
-
241
+
227
242
  // Set up temporary folder (for download and extraction)
228
243
  NSString *tempDir = [documentsPath stringByAppendingPathComponent:@"bundle-temp"];
229
244
  if ([fileManager fileExistsAtPath:tempDir]) {
@@ -244,7 +259,7 @@ RCT_EXPORT_MODULE();
244
259
  if (completion) completion(NO);
245
260
  return;
246
261
  }
247
-
262
+
248
263
  // Save temporary zip file
249
264
  if ([fileManager fileExistsAtPath:tempZipFile]) {
250
265
  [fileManager removeItemAtPath:tempZipFile error:nil];
@@ -263,7 +278,7 @@ RCT_EXPORT_MODULE();
263
278
  if (completion) completion(NO);
264
279
  return;
265
280
  }
266
-
281
+
267
282
  // Search for index.ios.bundle in extracted folder
268
283
  NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:extractedDir];
269
284
  NSString *foundBundle = nil;
@@ -313,7 +328,7 @@ RCT_EXPORT_MODULE();
313
328
  if (completion) completion(NO);
314
329
  return;
315
330
  }
316
-
331
+
317
332
  // Update modification time of final bundle
318
333
  NSDictionary *attributes = @{NSFileModificationDate: [NSDate date]};
319
334
  [fileManager setAttributes:attributes ofItemAtPath:finalBundleDir error:nil];
@@ -409,6 +424,13 @@ RCT_EXPORT_MODULE();
409
424
 
410
425
  #pragma mark - React Native Exports
411
426
 
427
+ RCT_EXPORT_METHOD(setChannel:(NSString *)channel
428
+ resolve:(RCTPromiseResolveBlock)resolve
429
+ reject:(RCTPromiseRejectBlock)reject) {
430
+ [HotUpdater setChannel:channel];
431
+ resolve(nil);
432
+ }
433
+
412
434
  RCT_EXPORT_METHOD(reload) {
413
435
  NSLog(@"HotUpdater requested a reload");
414
436
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -0,0 +1,9 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ @interface HotUpdaterPrefs : NSObject
4
+
5
+ + (instancetype)sharedInstanceWithAppVersion:(NSString *)appVersion;
6
+ - (NSString *)getItemForKey:(NSString *)key;
7
+ - (void)setItem:(NSString *)value forKey:(NSString *)key;
8
+
9
+ @end
@@ -0,0 +1,45 @@
1
+ #import "HotUpdaterPrefs.h"
2
+
3
+ @interface HotUpdaterPrefs ()
4
+ @property (nonatomic, strong) NSUserDefaults *userDefaults;
5
+ @property (nonatomic, copy) NSString *suiteName;
6
+ @end
7
+
8
+ @implementation HotUpdaterPrefs
9
+
10
+ + (instancetype)sharedInstanceWithAppVersion:(NSString *)appVersion {
11
+ static HotUpdaterPrefs *instance = nil;
12
+ static NSString *cachedVersion = nil;
13
+ @synchronized(self) {
14
+ if (instance == nil) {
15
+ instance = [[HotUpdaterPrefs alloc] initWithAppVersion:appVersion];
16
+ cachedVersion = appVersion;
17
+ } else if (![cachedVersion isEqualToString:appVersion]) {
18
+ NSString *oldSuiteName = [NSString stringWithFormat:@"HotUpdaterPrefs_%@", cachedVersion];
19
+ [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:oldSuiteName];
20
+ instance = [[HotUpdaterPrefs alloc] initWithAppVersion:appVersion];
21
+ cachedVersion = appVersion;
22
+ }
23
+ }
24
+ return instance;
25
+ }
26
+
27
+ - (instancetype)initWithAppVersion:(NSString *)appVersion {
28
+ self = [super init];
29
+ if (self) {
30
+ _suiteName = [NSString stringWithFormat:@"HotUpdaterPrefs_%@", appVersion];
31
+ _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:_suiteName];
32
+ }
33
+ return self;
34
+ }
35
+
36
+ - (NSString *)getItemForKey:(NSString *)key {
37
+ return [self.userDefaults objectForKey:key];
38
+ }
39
+
40
+ - (void)setItem:(NSString *)value forKey:(NSString *)key {
41
+ [self.userDefaults setObject:value forKey:key];
42
+ [self.userDefaults synchronize];
43
+ }
44
+
45
+ @end
@@ -38,6 +38,10 @@ namespace facebook::react {
38
38
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getAppVersion", @selector(getAppVersion:reject:), args, count);
39
39
  }
40
40
 
41
+ static facebook::jsi::Value __hostFunction_NativeHotUpdaterSpecJSI_setChannel(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
42
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "setChannel", @selector(setChannel:resolve:reject:), args, count);
43
+ }
44
+
41
45
  static facebook::jsi::Value __hostFunction_NativeHotUpdaterSpecJSI_addListener(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
42
46
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "addListener", @selector(addListener:), args, count);
43
47
  }
@@ -62,6 +66,9 @@ namespace facebook::react {
62
66
  methodMap_["getAppVersion"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterSpecJSI_getAppVersion};
63
67
 
64
68
 
69
+ methodMap_["setChannel"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterSpecJSI_setChannel};
70
+
71
+
65
72
  methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterSpecJSI_addListener};
66
73
 
67
74
 
@@ -37,6 +37,8 @@ namespace JS {
37
37
  struct Builder {
38
38
  struct Input {
39
39
  RCTRequired<NSString *> MIN_BUNDLE_ID;
40
+ RCTRequired<NSString *> APP_VERSION;
41
+ RCTRequired<NSString *> CHANNEL;
40
42
  };
41
43
 
42
44
  /** Initialize with a set of values */
@@ -66,6 +68,9 @@ namespace JS {
66
68
  reject:(RCTPromiseRejectBlock)reject;
67
69
  - (void)getAppVersion:(RCTPromiseResolveBlock)resolve
68
70
  reject:(RCTPromiseRejectBlock)reject;
71
+ - (void)setChannel:(NSString *)channel
72
+ resolve:(RCTPromiseResolveBlock)resolve
73
+ reject:(RCTPromiseRejectBlock)reject;
69
74
  - (void)addListener:(NSString *)eventName;
70
75
  - (void)removeListeners:(double)count;
71
76
  - (facebook::react::ModuleConstants<JS::NativeHotUpdater::Constants::Builder>)constantsToExport;
@@ -95,6 +100,10 @@ inline JS::NativeHotUpdater::Constants::Builder::Builder(const Input i) : _facto
95
100
  NSMutableDictionary *d = [NSMutableDictionary new];
96
101
  auto MIN_BUNDLE_ID = i.MIN_BUNDLE_ID.get();
97
102
  d[@"MIN_BUNDLE_ID"] = MIN_BUNDLE_ID;
103
+ auto APP_VERSION = i.APP_VERSION.get();
104
+ d[@"APP_VERSION"] = APP_VERSION;
105
+ auto CHANNEL = i.CHANNEL.get();
106
+ d[@"CHANNEL"] = CHANNEL;
98
107
  return d;
99
108
  }) {}
100
109
  inline JS::NativeHotUpdater::Constants::Builder::Builder(Constants i) : _factory(^{
@@ -29,6 +29,12 @@ static jsi::Value __hostFunction_NativeHotUpdaterCxxSpecJSI_getAppVersion(jsi::R
29
29
  rt
30
30
  );
31
31
  }
32
+ static jsi::Value __hostFunction_NativeHotUpdaterCxxSpecJSI_setChannel(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
33
+ return static_cast<NativeHotUpdaterCxxSpecJSI *>(&turboModule)->setChannel(
34
+ rt,
35
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
36
+ );
37
+ }
32
38
  static jsi::Value __hostFunction_NativeHotUpdaterCxxSpecJSI_addListener(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
33
39
  static_cast<NativeHotUpdaterCxxSpecJSI *>(&turboModule)->addListener(
34
40
  rt,
@@ -54,6 +60,7 @@ NativeHotUpdaterCxxSpecJSI::NativeHotUpdaterCxxSpecJSI(std::shared_ptr<CallInvok
54
60
  methodMap_["reload"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterCxxSpecJSI_reload};
55
61
  methodMap_["updateBundle"] = MethodMetadata {2, __hostFunction_NativeHotUpdaterCxxSpecJSI_updateBundle};
56
62
  methodMap_["getAppVersion"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterCxxSpecJSI_getAppVersion};
63
+ methodMap_["setChannel"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterCxxSpecJSI_setChannel};
57
64
  methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterCxxSpecJSI_addListener};
58
65
  methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeHotUpdaterCxxSpecJSI_removeListeners};
59
66
  methodMap_["getConstants"] = MethodMetadata {0, __hostFunction_NativeHotUpdaterCxxSpecJSI_getConstants};
@@ -23,6 +23,7 @@ public:
23
23
  virtual void reload(jsi::Runtime &rt) = 0;
24
24
  virtual jsi::Value updateBundle(jsi::Runtime &rt, jsi::String bundleId, jsi::String zipUrl) = 0;
25
25
  virtual jsi::Value getAppVersion(jsi::Runtime &rt) = 0;
26
+ virtual jsi::Value setChannel(jsi::Runtime &rt, jsi::String channel) = 0;
26
27
  virtual void addListener(jsi::Runtime &rt, jsi::String eventName) = 0;
27
28
  virtual void removeListeners(jsi::Runtime &rt, double count) = 0;
28
29
  virtual jsi::Object getConstants(jsi::Runtime &rt) = 0;
@@ -76,6 +77,14 @@ private:
76
77
  return bridging::callFromJs<jsi::Value>(
77
78
  rt, &T::getAppVersion, jsInvoker_, instance_);
78
79
  }
80
+ jsi::Value setChannel(jsi::Runtime &rt, jsi::String channel) override {
81
+ static_assert(
82
+ bridging::getParameterCount(&T::setChannel) == 2,
83
+ "Expected setChannel(...) to have 2 parameters");
84
+
85
+ return bridging::callFromJs<jsi::Value>(
86
+ rt, &T::setChannel, jsInvoker_, instance_, std::move(channel));
87
+ }
79
88
  void addListener(jsi::Runtime &rt, jsi::String eventName) override {
80
89
  static_assert(
81
90
  bridging::getParameterCount(&T::addListener) == 2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -81,8 +81,8 @@
81
81
  },
82
82
  "dependencies": {
83
83
  "use-sync-external-store": "1.4.0",
84
- "@hot-updater/js": "0.15.1",
85
- "@hot-updater/core": "0.15.1"
84
+ "@hot-updater/js": "0.16.1",
85
+ "@hot-updater/core": "0.16.1"
86
86
  },
87
87
  "scripts": {
88
88
  "build": "rslib build",
@@ -12,6 +12,11 @@ export interface CheckForUpdateOptions {
12
12
  source: string;
13
13
  requestHeaders?: Record<string, string>;
14
14
  onError?: (error: Error) => void;
15
+ /**
16
+ * The timeout duration for the request.
17
+ * @default 5000
18
+ */
19
+ requestTimeout?: number;
15
20
  }
16
21
 
17
22
  export async function checkForUpdate(options: CheckForUpdateOptions) {
@@ -26,7 +31,7 @@ export async function checkForUpdate(options: CheckForUpdateOptions) {
26
31
  return null;
27
32
  }
28
33
 
29
- const currentAppVersion = await getAppVersion();
34
+ const currentAppVersion = getAppVersion();
30
35
  const platform = Platform.OS as "ios" | "android";
31
36
  const currentBundleId = getBundleId();
32
37
  const minBundleId = getMinBundleId();
@@ -48,5 +53,6 @@ export async function checkForUpdate(options: CheckForUpdateOptions) {
48
53
  },
49
54
  options.requestHeaders,
50
55
  options.onError,
56
+ options.requestTimeout,
51
57
  );
52
58
  }
@@ -5,12 +5,18 @@ export const fetchUpdateInfo = async (
5
5
  { appVersion, bundleId, platform, minBundleId, channel }: GetBundlesArgs,
6
6
  requestHeaders?: Record<string, string>,
7
7
  onError?: (error: Error) => void,
8
+ requestTimeout = 5000,
8
9
  ): Promise<AppUpdateInfo | null> => {
10
+ const controller = new AbortController();
11
+ const timeoutId = setTimeout(() => {
12
+ controller.abort();
13
+ }, requestTimeout);
14
+
9
15
  try {
10
16
  const response = await fetch(source, {
17
+ signal: controller.signal,
11
18
  headers: {
12
19
  "Content-Type": "application/json",
13
-
14
20
  "x-app-platform": platform,
15
21
  "x-app-version": appVersion,
16
22
  "x-bundle-id": bundleId,
@@ -20,11 +26,17 @@ export const fetchUpdateInfo = async (
20
26
  },
21
27
  });
22
28
 
29
+ clearTimeout(timeoutId);
30
+
23
31
  if (response.status !== 200) {
24
32
  throw new Error(response.statusText);
25
33
  }
26
34
  return response.json();
27
- } catch (error) {
35
+ } catch (error: any) {
36
+ if (error.name === "AbortError") {
37
+ onError?.(new Error("Request timed out"));
38
+ return null;
39
+ }
28
40
  onError?.(error as Error);
29
41
  return null;
30
42
  }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  getChannel,
7
7
  getMinBundleId,
8
8
  reload,
9
+ setChannel,
9
10
  updateBundle,
10
11
  } from "./native";
11
12
  import { runUpdateProcess } from "./runUpdateProcess";
@@ -78,6 +79,10 @@ export const HotUpdater = {
78
79
  * ```
79
80
  */
80
81
  getChannel,
82
+ /**
83
+ * Sets the channel for the app.
84
+ */
85
+ setChannel,
81
86
  /**
82
87
  * Adds a listener to HotUpdater events.
83
88
  *
package/src/native.ts CHANGED
@@ -67,8 +67,9 @@ export const updateBundle = (
67
67
  /**
68
68
  * Fetches the current app version.
69
69
  */
70
- export const getAppVersion = (): Promise<string | null> => {
71
- return HotUpdaterNative.getAppVersion();
70
+ export const getAppVersion = (): string | null => {
71
+ const constants = HotUpdaterNative.getConstants();
72
+ return constants?.APP_VERSION ?? null;
72
73
  };
73
74
 
74
75
  /**
@@ -103,6 +104,14 @@ export const getBundleId = (): string => {
103
104
  : HotUpdater.HOT_UPDATER_BUNDLE_ID;
104
105
  };
105
106
 
107
+ /**
108
+ * Sets the channel for the app.
109
+ */
110
+ export const setChannel = async (channel: string) => {
111
+ return HotUpdaterNative.setChannel(channel);
112
+ };
113
+
106
114
  export const getChannel = (): string | null => {
107
- return HotUpdater.CHANNEL;
115
+ const constants = HotUpdaterNative.getConstants();
116
+ return constants?.CHANNEL ?? HotUpdater.CHANNEL ?? null;
108
117
  };
@@ -5,13 +5,21 @@ interface Spec extends TurboModule {
5
5
  // Methods
6
6
  reload(): void;
7
7
  updateBundle(bundleId: string, zipUrl: string): Promise<boolean>;
8
+ /**
9
+ * @deprecated
10
+ * use getConstants().APP_VERSION instead
11
+ */
8
12
  getAppVersion(): Promise<string | null>;
9
13
 
14
+ setChannel(channel: string): Promise<void>;
15
+
10
16
  // EventEmitter
11
17
  addListener(eventName: string): void;
12
18
  removeListeners(count: number): void;
13
19
  readonly getConstants: () => {
14
20
  MIN_BUNDLE_ID: string;
21
+ APP_VERSION: string | null;
22
+ CHANNEL: string | null;
15
23
  };
16
24
  }
17
25