react-native-ota-hot-update 2.3.6 → 2.4.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/android/generated/java/com/otahotupdate/NativeOtaHotUpdateSpec.java +14 -1
  2. package/android/generated/jni/RNOtaHotUpdateSpec-generated.cpp +20 -2
  3. package/android/generated/jni/react/renderer/components/RNOtaHotUpdateSpec/RNOtaHotUpdateSpecJSI-generated.cpp +26 -2
  4. package/android/generated/jni/react/renderer/components/RNOtaHotUpdateSpec/RNOtaHotUpdateSpecJSI.h +32 -5
  5. package/android/src/main/java/com/otahotupdate/OtaHotUpdateModule.kt +365 -36
  6. package/android/src/main/java/com/otahotupdate/SharedPrefs.kt +12 -0
  7. package/android/src/main/java/com/otahotupdate/Utils.kt +9 -3
  8. package/android/src/oldarch/OtaHotUpdateSpec.kt +4 -1
  9. package/ios/OtaHotUpdate.mm +383 -42
  10. package/ios/generated/RNOtaHotUpdateSpec/RNOtaHotUpdateSpec-generated.mm +23 -2
  11. package/ios/generated/RNOtaHotUpdateSpec/RNOtaHotUpdateSpec.h +12 -0
  12. package/ios/generated/RNOtaHotUpdateSpecJSI-generated.cpp +26 -2
  13. package/ios/generated/RNOtaHotUpdateSpecJSI.h +32 -5
  14. package/lib/commonjs/NativeOtaHotUpdate.js.map +1 -1
  15. package/lib/commonjs/index.js +26 -3
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/module/NativeOtaHotUpdate.js.map +1 -1
  18. package/lib/module/index.js +26 -3
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/commonjs/src/NativeOtaHotUpdate.d.ts +4 -1
  21. package/lib/typescript/commonjs/src/NativeOtaHotUpdate.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/src/index.d.ts +8 -2
  23. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/src/type.d.ts +36 -0
  25. package/lib/typescript/commonjs/src/type.d.ts.map +1 -1
  26. package/lib/typescript/module/src/NativeOtaHotUpdate.d.ts +4 -1
  27. package/lib/typescript/module/src/NativeOtaHotUpdate.d.ts.map +1 -1
  28. package/lib/typescript/module/src/index.d.ts +8 -2
  29. package/lib/typescript/module/src/index.d.ts.map +1 -1
  30. package/lib/typescript/module/src/type.d.ts +36 -0
  31. package/lib/typescript/module/src/type.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/NativeOtaHotUpdate.ts +4 -1
  34. package/src/index.d.ts +25 -2
  35. package/src/index.tsx +36 -5
  36. package/src/type.ts +44 -1
@@ -19,6 +19,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
19
19
  import com.facebook.react.bridge.ReactMethod;
20
20
  import com.facebook.react.turbomodule.core.interfaces.TurboModule;
21
21
  import javax.annotation.Nonnull;
22
+ import javax.annotation.Nullable;
22
23
 
23
24
  public abstract class NativeOtaHotUpdateSpec extends ReactContextBaseJavaModule implements TurboModule {
24
25
  public static final String NAME = "OtaHotUpdate";
@@ -34,7 +35,7 @@ public abstract class NativeOtaHotUpdateSpec extends ReactContextBaseJavaModule
34
35
 
35
36
  @ReactMethod
36
37
  @DoNotStrip
37
- public abstract void setupBundlePath(String path, String extension, Promise promise);
38
+ public abstract void setupBundlePath(String path, String extension, @Nullable Double version, @Nullable Double maxVersions, @Nullable String metadata, Promise promise);
38
39
 
39
40
  @ReactMethod
40
41
  @DoNotStrip
@@ -67,4 +68,16 @@ public abstract class NativeOtaHotUpdateSpec extends ReactContextBaseJavaModule
67
68
  @ReactMethod
68
69
  @DoNotStrip
69
70
  public abstract void rollbackToPreviousBundle(double a, Promise promise);
71
+
72
+ @ReactMethod
73
+ @DoNotStrip
74
+ public abstract void getBundleList(double a, Promise promise);
75
+
76
+ @ReactMethod
77
+ @DoNotStrip
78
+ public abstract void deleteBundleById(String id, Promise promise);
79
+
80
+ @ReactMethod
81
+ @DoNotStrip
82
+ public abstract void clearAllBundles(double a, Promise promise);
70
83
  }
@@ -14,7 +14,7 @@ namespace facebook::react {
14
14
 
15
15
  static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_setupBundlePath(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
16
16
  static jmethodID cachedMethodId = nullptr;
17
- return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "setupBundlePath", "(Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
17
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "setupBundlePath", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
18
18
  }
19
19
 
20
20
  static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_setExactBundlePath(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
@@ -57,9 +57,24 @@ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_rollbackToP
57
57
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "rollbackToPreviousBundle", "(DLcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
58
58
  }
59
59
 
60
+ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_getBundleList(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
61
+ static jmethodID cachedMethodId = nullptr;
62
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "getBundleList", "(DLcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
63
+ }
64
+
65
+ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_deleteBundleById(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
66
+ static jmethodID cachedMethodId = nullptr;
67
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "deleteBundleById", "(Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
68
+ }
69
+
70
+ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_clearAllBundles(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
71
+ static jmethodID cachedMethodId = nullptr;
72
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "clearAllBundles", "(DLcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
73
+ }
74
+
60
75
  NativeOtaHotUpdateSpecJSI::NativeOtaHotUpdateSpecJSI(const JavaTurboModule::InitParams &params)
61
76
  : JavaTurboModule(params) {
62
- methodMap_["setupBundlePath"] = MethodMetadata {2, __hostFunction_NativeOtaHotUpdateSpecJSI_setupBundlePath};
77
+ methodMap_["setupBundlePath"] = MethodMetadata {5, __hostFunction_NativeOtaHotUpdateSpecJSI_setupBundlePath};
63
78
  methodMap_["setExactBundlePath"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_setExactBundlePath};
64
79
  methodMap_["deleteBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_deleteBundle};
65
80
  methodMap_["restart"] = MethodMetadata {0, __hostFunction_NativeOtaHotUpdateSpecJSI_restart};
@@ -68,6 +83,9 @@ NativeOtaHotUpdateSpecJSI::NativeOtaHotUpdateSpecJSI(const JavaTurboModule::Init
68
83
  methodMap_["setCurrentVersion"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_setCurrentVersion};
69
84
  methodMap_["setUpdateMetadata"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_setUpdateMetadata};
70
85
  methodMap_["rollbackToPreviousBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_rollbackToPreviousBundle};
86
+ methodMap_["getBundleList"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_getBundleList};
87
+ methodMap_["deleteBundleById"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_deleteBundleById};
88
+ methodMap_["clearAllBundles"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_clearAllBundles};
71
89
  }
72
90
 
73
91
  std::shared_ptr<TurboModule> RNOtaHotUpdateSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params) {
@@ -15,7 +15,10 @@ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setupBundlePath(js
15
15
  return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->setupBundlePath(
16
16
  rt,
17
17
  count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
18
- count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt)
18
+ count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
19
+ count <= 2 || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asNumber()),
20
+ count <= 3 || args[3].isUndefined() ? std::nullopt : std::make_optional(args[3].asNumber()),
21
+ count <= 4 || args[4].isUndefined() ? std::nullopt : std::make_optional(args[4].asString(rt))
19
22
  );
20
23
  }
21
24
  static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setExactBundlePath(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
@@ -66,10 +69,28 @@ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_rollbackToPrevious
66
69
  count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
67
70
  );
68
71
  }
72
+ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_getBundleList(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
73
+ return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->getBundleList(
74
+ rt,
75
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
76
+ );
77
+ }
78
+ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_deleteBundleById(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
79
+ return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->deleteBundleById(
80
+ rt,
81
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
82
+ );
83
+ }
84
+ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_clearAllBundles(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
85
+ return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->clearAllBundles(
86
+ rt,
87
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
88
+ );
89
+ }
69
90
 
70
91
  NativeOtaHotUpdateCxxSpecJSI::NativeOtaHotUpdateCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
71
92
  : TurboModule("OtaHotUpdate", jsInvoker) {
72
- methodMap_["setupBundlePath"] = MethodMetadata {2, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setupBundlePath};
93
+ methodMap_["setupBundlePath"] = MethodMetadata {5, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setupBundlePath};
73
94
  methodMap_["setExactBundlePath"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setExactBundlePath};
74
95
  methodMap_["deleteBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_deleteBundle};
75
96
  methodMap_["restart"] = MethodMetadata {0, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_restart};
@@ -78,6 +99,9 @@ NativeOtaHotUpdateCxxSpecJSI::NativeOtaHotUpdateCxxSpecJSI(std::shared_ptr<CallI
78
99
  methodMap_["setCurrentVersion"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setCurrentVersion};
79
100
  methodMap_["setUpdateMetadata"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setUpdateMetadata};
80
101
  methodMap_["rollbackToPreviousBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_rollbackToPreviousBundle};
102
+ methodMap_["getBundleList"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_getBundleList};
103
+ methodMap_["deleteBundleById"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_deleteBundleById};
104
+ methodMap_["clearAllBundles"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_clearAllBundles};
81
105
  }
82
106
 
83
107
 
@@ -20,7 +20,7 @@ protected:
20
20
  NativeOtaHotUpdateCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);
21
21
 
22
22
  public:
23
- virtual jsi::Value setupBundlePath(jsi::Runtime &rt, jsi::String path, jsi::String extension) = 0;
23
+ virtual jsi::Value setupBundlePath(jsi::Runtime &rt, jsi::String path, jsi::String extension, std::optional<double> version, std::optional<double> maxVersions, std::optional<jsi::String> metadata) = 0;
24
24
  virtual jsi::Value setExactBundlePath(jsi::Runtime &rt, jsi::String path) = 0;
25
25
  virtual jsi::Value deleteBundle(jsi::Runtime &rt, double i) = 0;
26
26
  virtual void restart(jsi::Runtime &rt) = 0;
@@ -29,6 +29,9 @@ public:
29
29
  virtual jsi::Value setCurrentVersion(jsi::Runtime &rt, jsi::String version) = 0;
30
30
  virtual jsi::Value setUpdateMetadata(jsi::Runtime &rt, jsi::String metadata) = 0;
31
31
  virtual jsi::Value rollbackToPreviousBundle(jsi::Runtime &rt, double a) = 0;
32
+ virtual jsi::Value getBundleList(jsi::Runtime &rt, double a) = 0;
33
+ virtual jsi::Value deleteBundleById(jsi::Runtime &rt, jsi::String id) = 0;
34
+ virtual jsi::Value clearAllBundles(jsi::Runtime &rt, double a) = 0;
32
35
 
33
36
  };
34
37
 
@@ -55,13 +58,13 @@ private:
55
58
 
56
59
  }
57
60
 
58
- jsi::Value setupBundlePath(jsi::Runtime &rt, jsi::String path, jsi::String extension) override {
61
+ jsi::Value setupBundlePath(jsi::Runtime &rt, jsi::String path, jsi::String extension, std::optional<double> version, std::optional<double> maxVersions, std::optional<jsi::String> metadata) override {
59
62
  static_assert(
60
- bridging::getParameterCount(&T::setupBundlePath) == 3,
61
- "Expected setupBundlePath(...) to have 3 parameters");
63
+ bridging::getParameterCount(&T::setupBundlePath) == 6,
64
+ "Expected setupBundlePath(...) to have 6 parameters");
62
65
 
63
66
  return bridging::callFromJs<jsi::Value>(
64
- rt, &T::setupBundlePath, jsInvoker_, instance_, std::move(path), std::move(extension));
67
+ rt, &T::setupBundlePath, jsInvoker_, instance_, std::move(path), std::move(extension), std::move(version), std::move(maxVersions), std::move(metadata));
65
68
  }
66
69
  jsi::Value setExactBundlePath(jsi::Runtime &rt, jsi::String path) override {
67
70
  static_assert(
@@ -127,6 +130,30 @@ private:
127
130
  return bridging::callFromJs<jsi::Value>(
128
131
  rt, &T::rollbackToPreviousBundle, jsInvoker_, instance_, std::move(a));
129
132
  }
133
+ jsi::Value getBundleList(jsi::Runtime &rt, double a) override {
134
+ static_assert(
135
+ bridging::getParameterCount(&T::getBundleList) == 2,
136
+ "Expected getBundleList(...) to have 2 parameters");
137
+
138
+ return bridging::callFromJs<jsi::Value>(
139
+ rt, &T::getBundleList, jsInvoker_, instance_, std::move(a));
140
+ }
141
+ jsi::Value deleteBundleById(jsi::Runtime &rt, jsi::String id) override {
142
+ static_assert(
143
+ bridging::getParameterCount(&T::deleteBundleById) == 2,
144
+ "Expected deleteBundleById(...) to have 2 parameters");
145
+
146
+ return bridging::callFromJs<jsi::Value>(
147
+ rt, &T::deleteBundleById, jsInvoker_, instance_, std::move(id));
148
+ }
149
+ jsi::Value clearAllBundles(jsi::Runtime &rt, double a) override {
150
+ static_assert(
151
+ bridging::getParameterCount(&T::clearAllBundles) == 2,
152
+ "Expected clearAllBundles(...) to have 2 parameters");
153
+
154
+ return bridging::callFromJs<jsi::Value>(
155
+ rt, &T::clearAllBundles, jsInvoker_, instance_, std::move(a));
156
+ }
130
157
 
131
158
  private:
132
159
  friend class NativeOtaHotUpdateCxxSpec;
@@ -6,12 +6,15 @@ import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.bridge.ReactMethod
7
7
  import com.jakewharton.processphoenix.ProcessPhoenix
8
8
  import com.otahotupdate.OtaHotUpdate.Companion.getVersionCode
9
+ import com.rnhotupdate.Common
9
10
  import com.rnhotupdate.Common.CURRENT_VERSION_CODE
10
11
  import com.rnhotupdate.Common.PATH
11
12
  import com.rnhotupdate.Common.PREVIOUS_PATH
12
13
  import com.rnhotupdate.Common.VERSION
13
14
  import com.rnhotupdate.Common.PREVIOUS_VERSION
14
15
  import com.rnhotupdate.Common.METADATA
16
+ import com.rnhotupdate.Common.BUNDLE_HISTORY
17
+ import com.rnhotupdate.Common.DEFAULT_MAX_BUNDLE_VERSIONS
15
18
  import com.rnhotupdate.SharedPrefs
16
19
  import kotlinx.coroutines.CoroutineScope
17
20
  import kotlinx.coroutines.Dispatchers
@@ -20,6 +23,15 @@ import kotlinx.coroutines.cancel
20
23
  import kotlinx.coroutines.launch
21
24
  import kotlinx.coroutines.withContext
22
25
  import java.io.File
26
+ import org.json.JSONArray
27
+ import org.json.JSONObject
28
+
29
+ data class BundleVersion(
30
+ val version: Int,
31
+ val path: String,
32
+ val timestamp: Long,
33
+ val metadata: String? = null
34
+ )
23
35
 
24
36
  class OtaHotUpdateModule internal constructor(context: ReactApplicationContext) :
25
37
  OtaHotUpdateSpec(context) {
@@ -35,20 +47,173 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
35
47
  scope.cancel()
36
48
  }
37
49
 
38
- private fun processBundleFile(path: String?, extension: String?): Boolean {
50
+ private fun loadBundleHistory(): List<BundleVersion> {
51
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
52
+ val historyJson = sharedPrefs.getString(BUNDLE_HISTORY)
53
+
54
+ // If history exists, load it
55
+ if (!historyJson.isNullOrEmpty()) {
56
+ return try {
57
+ val jsonArray = JSONArray(historyJson)
58
+ (0 until jsonArray.length()).map { i ->
59
+ val obj = jsonArray.getJSONObject(i)
60
+ BundleVersion(
61
+ version = obj.getInt("version"),
62
+ path = obj.getString("path"),
63
+ timestamp = obj.getLong("timestamp"),
64
+ metadata = if (obj.has("metadata") && !obj.isNull("metadata")) obj.getString("metadata") else null
65
+ )
66
+ }
67
+ } catch (e: Exception) {
68
+ emptyList()
69
+ }
70
+ }
71
+
72
+ // Migration: If history is empty but PATH exists, migrate from old system
73
+ val currentPath = sharedPrefs.getString(PATH)
74
+ val currentVersion = sharedPrefs.getString(VERSION)
75
+ val previousPath = sharedPrefs.getString(PREVIOUS_PATH)
76
+ val previousVersion = sharedPrefs.getString(PREVIOUS_VERSION)
77
+
78
+ if (currentPath.isNullOrEmpty()) {
79
+ return emptyList()
80
+ }
81
+
82
+ // Migrate current bundle
83
+ val migratedHistory = mutableListOf<BundleVersion>()
84
+
85
+ // Add current bundle if has version
86
+ if (!currentVersion.isNullOrEmpty()) {
87
+ try {
88
+ val version = currentVersion.toInt()
89
+ val bundleFile = File(currentPath)
90
+ if (bundleFile.exists()) {
91
+ migratedHistory.add(
92
+ BundleVersion(
93
+ version = version,
94
+ path = currentPath,
95
+ timestamp = bundleFile.lastModified(), // Use file modification time
96
+ metadata = null
97
+ )
98
+ )
99
+ }
100
+ } catch (e: Exception) {
101
+ // Version is not a number, skip
102
+ }
103
+ }
104
+
105
+ // Add previous bundle if exists
106
+ if (!previousPath.isNullOrEmpty() && !previousVersion.isNullOrEmpty()) {
107
+ try {
108
+ val version = previousVersion.toInt()
109
+ val bundleFile = File(previousPath)
110
+ if (bundleFile.exists()) {
111
+ migratedHistory.add(
112
+ BundleVersion(
113
+ version = version,
114
+ path = previousPath,
115
+ timestamp = bundleFile.lastModified(),
116
+ metadata = null
117
+ )
118
+ )
119
+ }
120
+ } catch (e: Exception) {
121
+ // Version is not a number, skip
122
+ }
123
+ }
124
+
125
+ // Save migrated history if any
126
+ if (migratedHistory.isNotEmpty()) {
127
+ saveBundleHistory(migratedHistory.sortedByDescending { it.version })
128
+ }
129
+
130
+ return migratedHistory.sortedByDescending { it.version }
131
+ }
132
+
133
+ private fun saveBundleHistory(history: List<BundleVersion>) {
134
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
135
+ val jsonArray = JSONArray()
136
+ history.forEach { bundle ->
137
+ val obj = JSONObject()
138
+ obj.put("version", bundle.version)
139
+ obj.put("path", bundle.path)
140
+ obj.put("timestamp", bundle.timestamp)
141
+ if (bundle.metadata != null) {
142
+ obj.put("metadata", bundle.metadata)
143
+ } else {
144
+ obj.put("metadata", JSONObject.NULL)
145
+ }
146
+ jsonArray.put(obj)
147
+ }
148
+ sharedPrefs.putString(BUNDLE_HISTORY, jsonArray.toString())
149
+ }
150
+
151
+ private fun extractFolderName(path: String): String {
152
+ val file = File(path)
153
+ return file.parentFile?.name ?: ""
154
+ }
155
+
156
+ private fun saveBundleVersion(
157
+ newPath: String,
158
+ version: Int,
159
+ maxVersions: Int,
160
+ metadata: String?
161
+ ) {
162
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
163
+ val history = loadBundleHistory()
164
+
165
+ // Add new version
166
+ val newBundle = BundleVersion(
167
+ version = version,
168
+ path = newPath,
169
+ timestamp = System.currentTimeMillis(),
170
+ metadata = metadata
171
+ )
172
+
173
+ // Combine and sort by version descending
174
+ val updatedHistory = (listOf(newBundle) + history)
175
+ .sortedByDescending { it.version }
176
+ .distinctBy { it.version } // Remove duplicates by version
177
+
178
+ // Keep only maxVersions most recent
179
+ val finalHistory = updatedHistory.take(maxVersions)
180
+
181
+ // Delete old versions beyond limit
182
+ val versionsToKeep = finalHistory.map { it.version }.toSet()
183
+ updatedHistory.forEach { bundle ->
184
+ if (bundle.version !in versionsToKeep) {
185
+ utils.deleteOldBundleIfneeded(bundle.path)
186
+ }
187
+ }
188
+
189
+ // Save updated history
190
+ saveBundleHistory(finalHistory)
191
+
192
+ // Set current path
193
+ sharedPrefs.putString(PATH, newPath)
194
+ sharedPrefs.putString(VERSION, version.toString())
195
+ }
196
+
197
+ private fun processBundleFile(path: String?, extension: String?, version: Int?, maxVersions: Int?, metadata: String?): Boolean {
39
198
  if (path != null) {
40
199
  val file = File(path)
41
200
  if (file.exists() && file.isFile) {
42
- val fileUnzip = utils.extractZipFile(file, extension ?: ".bundle")
201
+ val fileUnzip = utils.extractZipFile(file, extension ?: ".bundle", version)
43
202
  if (fileUnzip != null) {
44
203
  file.delete()
45
204
  utils.deleteOldBundleIfneeded(null)
46
205
  val sharedPrefs = SharedPrefs(reactApplicationContext)
47
206
  val oldPath = sharedPrefs.getString(PATH)
48
- if (!oldPath.isNullOrEmpty()) {
49
- sharedPrefs.putString(PREVIOUS_PATH, oldPath)
207
+
208
+ // If version is provided, save to history system
209
+ if (version != null) {
210
+ val maxVersionsToKeep = maxVersions ?: Common.DEFAULT_MAX_BUNDLE_VERSIONS
211
+ saveBundleVersion(fileUnzip, version, maxVersionsToKeep, metadata)
212
+ } else {
213
+ // No version (e.g., Git update) - just set path, no history
214
+ sharedPrefs.putString(PATH, fileUnzip)
50
215
  }
51
- sharedPrefs.putString(PATH, fileUnzip)
216
+
52
217
  sharedPrefs.putString(
53
218
  CURRENT_VERSION_CODE,
54
219
  reactApplicationContext.getVersionCode()
@@ -66,10 +231,12 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
66
231
  }
67
232
  }
68
233
  @ReactMethod
69
- override fun setupBundlePath(path: String?, extension: String?, promise: Promise) {
234
+ override fun setupBundlePath(path: String?, extension: String?, version: Double?, maxVersions: Double?, metadata: String?, promise: Promise) {
70
235
  scope.launch {
71
236
  try {
72
- val result = processBundleFile(path, extension)
237
+ val versionInt = version?.toInt()
238
+ val maxVersionsInt = maxVersions?.toInt()
239
+ val result = processBundleFile(path, extension, versionInt, maxVersionsInt, metadata)
73
240
  withContext(Dispatchers.Main) {
74
241
  promise.resolve(result)
75
242
  }
@@ -83,11 +250,34 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
83
250
 
84
251
  @ReactMethod
85
252
  override fun deleteBundle(i: Double, promise: Promise) {
86
- val isDeleted = utils.deleteOldBundleIfneeded(PATH)
87
- val isDeletedOldPath = utils.deleteOldBundleIfneeded(PREVIOUS_PATH)
88
- val sharedPrefs = SharedPrefs(reactApplicationContext)
89
- sharedPrefs.putString(VERSION, "0")
90
- promise.resolve(isDeleted && isDeletedOldPath)
253
+ scope.launch {
254
+ try {
255
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
256
+ val currentPath = sharedPrefs.getString(PATH)
257
+
258
+ // Delete current bundle from file system
259
+ val isDeleted = utils.deleteOldBundleIfneeded(PATH)
260
+
261
+ // Remove current bundle from history if exists
262
+ if (currentPath != null && currentPath.isNotEmpty()) {
263
+ val history = loadBundleHistory()
264
+ val updatedHistory = history.filter { it.path != currentPath }
265
+ saveBundleHistory(updatedHistory)
266
+ }
267
+
268
+ // Clear paths and version (no longer clear PREVIOUS_PATH, use history instead)
269
+ sharedPrefs.putString(PATH, "")
270
+ sharedPrefs.putString(VERSION, "0")
271
+
272
+ withContext(Dispatchers.Main) {
273
+ promise.resolve(isDeleted)
274
+ }
275
+ } catch (e: Exception) {
276
+ withContext(Dispatchers.Main) {
277
+ promise.reject("DELETE_BUNDLE_ERROR", e)
278
+ }
279
+ }
280
+ }
91
281
  }
92
282
 
93
283
  @ReactMethod
@@ -110,12 +300,7 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
110
300
  @ReactMethod
111
301
  override fun setCurrentVersion(version: String?, promise: Promise) {
112
302
  val sharedPrefs = SharedPrefs(reactApplicationContext)
113
-
114
- val currentVersion = sharedPrefs.getString(VERSION)
115
- if (currentVersion != "" && currentVersion != version) {
116
- sharedPrefs.putString(PREVIOUS_VERSION, currentVersion)
117
- }
118
-
303
+ // No longer save PREVIOUS_VERSION, use history instead
119
304
  sharedPrefs.putString(VERSION, version)
120
305
  promise.resolve(true)
121
306
  }
@@ -157,31 +342,175 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
157
342
 
158
343
  @ReactMethod
159
344
  override fun rollbackToPreviousBundle(a: Double, promise: Promise) {
160
- val sharedPrefs = SharedPrefs(reactApplicationContext)
161
- val oldPath = sharedPrefs.getString(PREVIOUS_PATH)
162
- val previousVersion = sharedPrefs.getString(PREVIOUS_VERSION)
345
+ scope.launch {
346
+ try {
347
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
348
+ val currentPath = sharedPrefs.getString(PATH)
163
349
 
164
- if (oldPath != "") {
165
- val isDeleted = utils.deleteOldBundleIfneeded(PATH)
166
- if (isDeleted) {
167
- sharedPrefs.putString(PATH, oldPath)
168
- sharedPrefs.putString(PREVIOUS_PATH, "")
350
+ // Use history to find previous version (closest to current)
351
+ val history = loadBundleHistory()
352
+ if (history.isNotEmpty() && currentPath != null && currentPath.isNotEmpty()) {
353
+ // Find current bundle in history
354
+ val currentBundle = history.find { it.path == currentPath }
355
+ if (currentBundle != null) {
356
+ // Find previous version (version < current, max version = closest to current)
357
+ val previousBundle = history
358
+ .filter { it.version < currentBundle.version }
359
+ .maxByOrNull { it.version }
169
360
 
170
- if (previousVersion != "") {
171
- sharedPrefs.putString(VERSION, previousVersion)
172
- sharedPrefs.putString(PREVIOUS_VERSION, "")
173
- } else {
174
- sharedPrefs.putString(VERSION, "")
361
+ if (previousBundle != null && File(previousBundle.path).exists()) {
362
+ // Rollback to previous bundle from history
363
+ val isDeleted = utils.deleteOldBundleIfneeded(PATH)
364
+ if (isDeleted) {
365
+ sharedPrefs.putString(PATH, previousBundle.path)
366
+ sharedPrefs.putString(VERSION, previousBundle.version.toString())
367
+
368
+ withContext(Dispatchers.Main) {
369
+ promise.resolve(true)
370
+ }
371
+ return@launch
372
+ }
373
+ }
374
+ }
175
375
  }
176
376
 
177
- promise.resolve(true)
178
- } else {
179
- promise.resolve(false)
377
+ withContext(Dispatchers.Main) {
378
+ promise.resolve(false)
379
+ }
380
+ } catch (e: Exception) {
381
+ withContext(Dispatchers.Main) {
382
+ promise.reject("ROLLBACK_ERROR", e)
383
+ }
180
384
  }
181
- } else {
182
- promise.resolve(false)
183
385
  }
184
386
  }
387
+
388
+ @ReactMethod
389
+ override fun getBundleList(a: Double, promise: Promise) {
390
+ scope.launch {
391
+ try {
392
+ val history = loadBundleHistory()
393
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
394
+ val activePath = sharedPrefs.getString(PATH)
395
+
396
+ val bundleList = history.map { bundle ->
397
+ val folderName = extractFolderName(bundle.path)
398
+ val bundleObj = JSONObject()
399
+ bundleObj.put("id", folderName)
400
+ bundleObj.put("version", bundle.version)
401
+ bundleObj.put("date", bundle.timestamp)
402
+ bundleObj.put("path", bundle.path)
403
+ bundleObj.put("isActive", bundle.path == activePath)
404
+ if (bundle.metadata != null) {
405
+ try {
406
+ // Try to parse as JSON, if fails use as string
407
+ val metadataJson = JSONObject(bundle.metadata)
408
+ bundleObj.put("metadata", metadataJson)
409
+ } catch (e: Exception) {
410
+ bundleObj.put("metadata", bundle.metadata)
411
+ }
412
+ } else {
413
+ bundleObj.put("metadata", JSONObject.NULL)
414
+ }
415
+ bundleObj
416
+ }
417
+
418
+ val jsonArray = JSONArray()
419
+ bundleList.forEach { jsonArray.put(it) }
420
+
421
+ withContext(Dispatchers.Main) {
422
+ promise.resolve(jsonArray.toString())
423
+ }
424
+ } catch (e: Exception) {
425
+ withContext(Dispatchers.Main) {
426
+ promise.reject("GET_BUNDLE_LIST_ERROR", e)
427
+ }
428
+ }
429
+ }
430
+ }
431
+
432
+ @ReactMethod
433
+ override fun deleteBundleById(id: String, promise: Promise) {
434
+ scope.launch {
435
+ try {
436
+ val history = loadBundleHistory()
437
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
438
+ val activePath = sharedPrefs.getString(PATH)
439
+
440
+ val bundleToDelete = history.find { extractFolderName(it.path) == id }
441
+ if (bundleToDelete == null) {
442
+ withContext(Dispatchers.Main) {
443
+ promise.resolve(false)
444
+ }
445
+ return@launch
446
+ }
447
+
448
+ // If deleting active bundle, rollback to oldest remaining bundle or clear
449
+ if (bundleToDelete.path == activePath) {
450
+ val remainingBundles = history.filter { it.path != bundleToDelete.path }
451
+ if (remainingBundles.isNotEmpty()) {
452
+ val oldestBundle = remainingBundles.minByOrNull { it.version }
453
+ if (oldestBundle != null) {
454
+ sharedPrefs.putString(PATH, oldestBundle.path)
455
+ sharedPrefs.putString(VERSION, oldestBundle.version.toString())
456
+ } else {
457
+ sharedPrefs.putString(PATH, "")
458
+ sharedPrefs.putString(VERSION, "")
459
+ }
460
+ } else {
461
+ sharedPrefs.putString(PATH, "")
462
+ sharedPrefs.putString(VERSION, "")
463
+ }
464
+ }
465
+
466
+ // Delete bundle folder
467
+ val isDeleted = utils.deleteOldBundleIfneeded(bundleToDelete.path)
468
+
469
+ // Remove from history
470
+ val updatedHistory = history.filter { it.path != bundleToDelete.path }
471
+ saveBundleHistory(updatedHistory)
472
+
473
+ withContext(Dispatchers.Main) {
474
+ promise.resolve(isDeleted)
475
+ }
476
+ } catch (e: Exception) {
477
+ withContext(Dispatchers.Main) {
478
+ promise.reject("DELETE_BUNDLE_ERROR", e)
479
+ }
480
+ }
481
+ }
482
+ }
483
+
484
+ @ReactMethod
485
+ override fun clearAllBundles(a: Double, promise: Promise) {
486
+ scope.launch {
487
+ try {
488
+ val history = loadBundleHistory()
489
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
490
+
491
+ // Delete all bundle folders
492
+ history.forEach { bundle ->
493
+ utils.deleteOldBundleIfneeded(bundle.path)
494
+ }
495
+
496
+ // Clear history
497
+ saveBundleHistory(emptyList())
498
+
499
+ // Clear current path and version
500
+ sharedPrefs.putString(PATH, "")
501
+ sharedPrefs.putString(VERSION, "")
502
+
503
+ withContext(Dispatchers.Main) {
504
+ promise.resolve(true)
505
+ }
506
+ } catch (e: Exception) {
507
+ withContext(Dispatchers.Main) {
508
+ promise.reject("CLEAR_ALL_BUNDLES_ERROR", e)
509
+ }
510
+ }
511
+ }
512
+ }
513
+
185
514
  companion object {
186
515
  const val NAME = "OtaHotUpdate"
187
516
  }