@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.
- package/android/generated/java/com/hotupdater/NativeHotUpdaterSpec.java +8 -1
- package/android/generated/jni/HotUpdaterSpec-generated.cpp +6 -0
- package/android/generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI-generated.cpp +7 -0
- package/android/generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI.h +9 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +39 -28
- package/android/src/main/java/com/hotupdater/HotUpdaterPrefs.kt +42 -0
- package/android/src/newarch/HotUpdaterModule.kt +18 -3
- package/android/src/oldarch/HotUpdaterModule.kt +18 -3
- package/android/src/oldarch/HotUpdaterSpec.kt +5 -0
- package/dist/checkForUpdate.d.ts +5 -0
- package/dist/fetchUpdateInfo.d.ts +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +23 -5
- package/dist/index.mjs +23 -5
- package/dist/native.d.ts +5 -1
- package/ios/HotUpdater/HotUpdater.mm +49 -27
- package/ios/HotUpdater/HotUpdaterPrefs.h +9 -0
- package/ios/HotUpdater/HotUpdaterPrefs.mm +45 -0
- package/ios/generated/HotUpdaterSpec/HotUpdaterSpec-generated.mm +7 -0
- package/ios/generated/HotUpdaterSpec/HotUpdaterSpec.h +9 -0
- package/ios/generated/HotUpdaterSpecJSI-generated.cpp +7 -0
- package/ios/generated/HotUpdaterSpecJSI.h +9 -0
- package/package.json +3 -3
- package/src/checkForUpdate.ts +7 -1
- package/src/fetchUpdateInfo.ts +14 -2
- package/src/index.ts +5 -0
- package/src/native.ts +12 -3
- package/src/specs/NativeHotUpdater.ts +8 -0
|
@@ -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
|
|
39
|
-
|
|
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
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
74
|
+
override fun addListener(
|
|
75
|
+
@Suppress("UNUSED_PARAMETER") eventName: String?,
|
|
76
|
+
) {
|
|
64
77
|
// No-op
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
override fun removeListeners(
|
|
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(
|
|
67
|
+
fun addListener(
|
|
68
|
+
@Suppress("UNUSED_PARAMETER") eventName: String?,
|
|
69
|
+
) {
|
|
59
70
|
// No-op
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
@ReactMethod
|
|
63
|
-
fun removeListeners(
|
|
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(
|
|
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
|
|
package/dist/checkForUpdate.d.ts
CHANGED
|
@@ -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: () =>
|
|
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 = ()=>
|
|
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
|
|
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 =
|
|
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 = ()=>
|
|
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
|
|
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 =
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
- (
|
|
80
|
-
|
|
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*)
|
|
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
|
-
|
|
109
|
+
+ (void)setChannel:(NSString *)channel {
|
|
92
110
|
NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
|
93
|
-
|
|
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
|
-
|
|
99
|
-
[
|
|
100
|
-
[defaults synchronize];
|
|
117
|
+
HotUpdaterPrefs *prefs = [self getPrefs];
|
|
118
|
+
[prefs setItem:localPath forKey:@"HotUpdaterBundleURL"];
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
NSString *savedURLString = [
|
|
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
|
-
|
|
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
|
|
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 >
|
|
165
|
-
NSArray *oldBundles = [bundleDirs subarrayWithRange:NSMakeRange(
|
|
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.
|
|
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.
|
|
85
|
-
"@hot-updater/core": "0.
|
|
84
|
+
"@hot-updater/js": "0.16.1",
|
|
85
|
+
"@hot-updater/core": "0.16.1"
|
|
86
86
|
},
|
|
87
87
|
"scripts": {
|
|
88
88
|
"build": "rslib build",
|
package/src/checkForUpdate.ts
CHANGED
|
@@ -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 =
|
|
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
|
}
|
package/src/fetchUpdateInfo.ts
CHANGED
|
@@ -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 = ():
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|