@hot-updater/react-native 0.23.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +393 -49
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +204 -0
  3. package/android/src/main/java/com/hotupdater/HotUpdater.kt +48 -36
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +134 -0
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +168 -95
  6. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +15 -3
  7. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +17 -12
  8. package/android/src/newarch/HotUpdaterModule.kt +88 -23
  9. package/android/src/oldarch/HotUpdaterModule.kt +89 -22
  10. package/android/src/oldarch/HotUpdaterSpec.kt +6 -0
  11. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +401 -77
  12. package/ios/HotUpdater/Internal/BundleMetadata.swift +177 -0
  13. package/ios/HotUpdater/Internal/HotUpdater.mm +213 -47
  14. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +96 -25
  15. package/ios/HotUpdater/Internal/SignatureVerifier.swift +35 -29
  16. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +2 -2
  17. package/ios/HotUpdater/Public/HotUpdater.h +8 -2
  18. package/lib/commonjs/checkForUpdate.js +31 -28
  19. package/lib/commonjs/checkForUpdate.js.map +1 -1
  20. package/lib/commonjs/error.js +45 -1
  21. package/lib/commonjs/error.js.map +1 -1
  22. package/lib/commonjs/fetchUpdateInfo.js +7 -45
  23. package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
  24. package/lib/commonjs/index.js +237 -208
  25. package/lib/commonjs/index.js.map +1 -1
  26. package/lib/commonjs/native.js +103 -3
  27. package/lib/commonjs/native.js.map +1 -1
  28. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  29. package/lib/commonjs/wrap.js +39 -1
  30. package/lib/commonjs/wrap.js.map +1 -1
  31. package/lib/module/checkForUpdate.js +32 -26
  32. package/lib/module/checkForUpdate.js.map +1 -1
  33. package/lib/module/error.js +45 -0
  34. package/lib/module/error.js.map +1 -1
  35. package/lib/module/fetchUpdateInfo.js +7 -45
  36. package/lib/module/fetchUpdateInfo.js.map +1 -1
  37. package/lib/module/index.js +238 -203
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/native.js +87 -2
  40. package/lib/module/native.js.map +1 -1
  41. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  42. package/lib/module/wrap.js +40 -2
  43. package/lib/module/wrap.js.map +1 -1
  44. package/lib/typescript/commonjs/checkForUpdate.d.ts +11 -13
  45. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/error.d.ts +120 -0
  47. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +3 -5
  49. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/index.d.ts +35 -41
  51. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/native.d.ts +58 -2
  53. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +62 -0
  55. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/wrap.d.ts +76 -5
  57. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  58. package/lib/typescript/module/checkForUpdate.d.ts +11 -13
  59. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  60. package/lib/typescript/module/error.d.ts +120 -0
  61. package/lib/typescript/module/error.d.ts.map +1 -1
  62. package/lib/typescript/module/fetchUpdateInfo.d.ts +3 -5
  63. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
  64. package/lib/typescript/module/index.d.ts +35 -41
  65. package/lib/typescript/module/index.d.ts.map +1 -1
  66. package/lib/typescript/module/native.d.ts +58 -2
  67. package/lib/typescript/module/native.d.ts.map +1 -1
  68. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +62 -0
  69. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  70. package/lib/typescript/module/wrap.d.ts +76 -5
  71. package/lib/typescript/module/wrap.d.ts.map +1 -1
  72. package/package.json +7 -7
  73. package/plugin/build/withHotUpdater.js +3 -3
  74. package/src/checkForUpdate.ts +51 -40
  75. package/src/error.ts +153 -0
  76. package/src/fetchUpdateInfo.ts +10 -58
  77. package/src/index.ts +283 -206
  78. package/src/native.ts +88 -2
  79. package/src/specs/NativeHotUpdater.ts +63 -0
  80. package/src/wrap.tsx +131 -9
  81. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +0 -52
  82. package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +0 -24
  83. package/lib/commonjs/runUpdateProcess.js +0 -69
  84. package/lib/commonjs/runUpdateProcess.js.map +0 -1
  85. package/lib/module/runUpdateProcess.js +0 -64
  86. package/lib/module/runUpdateProcess.js.map +0 -1
  87. package/lib/typescript/commonjs/runUpdateProcess.d.ts +0 -49
  88. package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +0 -1
  89. package/lib/typescript/module/runUpdateProcess.d.ts +0 -49
  90. package/lib/typescript/module/runUpdateProcess.d.ts.map +0 -1
  91. package/src/runUpdateProcess.ts +0 -80
@@ -0,0 +1,204 @@
1
+ package com.hotupdater
2
+
3
+ import android.util.Log
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+ import java.io.File
7
+
8
+ /**
9
+ * Bundle metadata for managing stable/staging bundles and verification state
10
+ */
11
+ data class BundleMetadata(
12
+ val schema: String = SCHEMA_VERSION,
13
+ val stableBundleId: String? = null,
14
+ val stagingBundleId: String? = null,
15
+ val verificationPending: Boolean = false,
16
+ val verificationAttemptedAt: Long? = null,
17
+ val stagingExecutionCount: Int? = null,
18
+ val updatedAt: Long = System.currentTimeMillis(),
19
+ ) {
20
+ companion object {
21
+ private const val TAG = "BundleMetadata"
22
+ const val SCHEMA_VERSION = "metadata-v1"
23
+ const val METADATA_FILENAME = "metadata.json"
24
+
25
+ fun fromJson(json: JSONObject): BundleMetadata =
26
+ BundleMetadata(
27
+ schema = json.optString("schema", SCHEMA_VERSION),
28
+ stableBundleId = json.optString("stableBundleId", null)?.takeIf { it.isNotEmpty() },
29
+ stagingBundleId = json.optString("stagingBundleId", null)?.takeIf { it.isNotEmpty() },
30
+ verificationPending = json.optBoolean("verificationPending", false),
31
+ verificationAttemptedAt =
32
+ if (json.has("verificationAttemptedAt") && !json.isNull("verificationAttemptedAt")) {
33
+ json.getLong("verificationAttemptedAt")
34
+ } else {
35
+ null
36
+ },
37
+ stagingExecutionCount =
38
+ if (json.has("stagingExecutionCount") && !json.isNull("stagingExecutionCount")) {
39
+ json.getInt("stagingExecutionCount")
40
+ } else {
41
+ null
42
+ },
43
+ updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
44
+ )
45
+
46
+ fun loadFromFile(file: File): BundleMetadata? {
47
+ return try {
48
+ if (!file.exists()) {
49
+ Log.d(TAG, "Metadata file does not exist: ${file.absolutePath}")
50
+ return null
51
+ }
52
+ val jsonString = file.readText()
53
+ val json = JSONObject(jsonString)
54
+ fromJson(json)
55
+ } catch (e: Exception) {
56
+ Log.e(TAG, "Failed to load metadata from file", e)
57
+ null
58
+ }
59
+ }
60
+ }
61
+
62
+ fun toJson(): JSONObject =
63
+ JSONObject().apply {
64
+ put("schema", schema)
65
+ put("stableBundleId", stableBundleId ?: JSONObject.NULL)
66
+ put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
67
+ put("verificationPending", verificationPending)
68
+ put("verificationAttemptedAt", verificationAttemptedAt ?: JSONObject.NULL)
69
+ put("stagingExecutionCount", stagingExecutionCount ?: JSONObject.NULL)
70
+ put("updatedAt", updatedAt)
71
+ }
72
+
73
+ fun saveToFile(file: File): Boolean =
74
+ try {
75
+ file.parentFile?.mkdirs()
76
+ file.writeText(toJson().toString(2))
77
+ Log.d(TAG, "Saved metadata to file: ${file.absolutePath}")
78
+ true
79
+ } catch (e: Exception) {
80
+ Log.e(TAG, "Failed to save metadata to file", e)
81
+ false
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Entry for a crashed bundle in history
87
+ */
88
+ data class CrashedBundleEntry(
89
+ val bundleId: String,
90
+ val crashedAt: Long,
91
+ val crashCount: Int = 1,
92
+ ) {
93
+ companion object {
94
+ fun fromJson(json: JSONObject): CrashedBundleEntry =
95
+ CrashedBundleEntry(
96
+ bundleId = json.getString("bundleId"),
97
+ crashedAt = json.getLong("crashedAt"),
98
+ crashCount = json.optInt("crashCount", 1),
99
+ )
100
+ }
101
+
102
+ fun toJson(): JSONObject =
103
+ JSONObject().apply {
104
+ put("bundleId", bundleId)
105
+ put("crashedAt", crashedAt)
106
+ put("crashCount", crashCount)
107
+ }
108
+ }
109
+
110
+ /**
111
+ * History of crashed bundles
112
+ */
113
+ data class CrashedHistory(
114
+ val bundles: MutableList<CrashedBundleEntry> = mutableListOf(),
115
+ val maxHistorySize: Int = DEFAULT_MAX_HISTORY_SIZE,
116
+ ) {
117
+ companion object {
118
+ private const val TAG = "CrashedHistory"
119
+ const val DEFAULT_MAX_HISTORY_SIZE = 10
120
+ const val CRASHED_HISTORY_FILENAME = "crashed-history.json"
121
+
122
+ fun fromJson(json: JSONObject): CrashedHistory {
123
+ val bundlesArray = json.optJSONArray("bundles") ?: JSONArray()
124
+ val bundles = mutableListOf<CrashedBundleEntry>()
125
+ for (i in 0 until bundlesArray.length()) {
126
+ bundles.add(CrashedBundleEntry.fromJson(bundlesArray.getJSONObject(i)))
127
+ }
128
+ return CrashedHistory(
129
+ bundles = bundles,
130
+ maxHistorySize = json.optInt("maxHistorySize", DEFAULT_MAX_HISTORY_SIZE),
131
+ )
132
+ }
133
+
134
+ fun loadFromFile(file: File): CrashedHistory {
135
+ return try {
136
+ if (!file.exists()) {
137
+ Log.d(TAG, "Crashed history file does not exist, returning empty history")
138
+ return CrashedHistory()
139
+ }
140
+ val jsonString = file.readText()
141
+ val json = JSONObject(jsonString)
142
+ fromJson(json)
143
+ } catch (e: Exception) {
144
+ Log.e(TAG, "Failed to load crashed history from file", e)
145
+ CrashedHistory()
146
+ }
147
+ }
148
+ }
149
+
150
+ fun toJson(): JSONObject =
151
+ JSONObject().apply {
152
+ val bundlesArray = JSONArray()
153
+ bundles.forEach { bundlesArray.put(it.toJson()) }
154
+ put("bundles", bundlesArray)
155
+ put("maxHistorySize", maxHistorySize)
156
+ }
157
+
158
+ fun saveToFile(file: File): Boolean =
159
+ try {
160
+ file.parentFile?.mkdirs()
161
+ file.writeText(toJson().toString(2))
162
+ Log.d(TAG, "Saved crashed history to file: ${file.absolutePath}")
163
+ true
164
+ } catch (e: Exception) {
165
+ Log.e(TAG, "Failed to save crashed history to file", e)
166
+ false
167
+ }
168
+
169
+ fun contains(bundleId: String): Boolean = bundles.any { it.bundleId == bundleId }
170
+
171
+ fun addEntry(bundleId: String) {
172
+ val existingIndex = bundles.indexOfFirst { it.bundleId == bundleId }
173
+ if (existingIndex >= 0) {
174
+ // Update existing entry
175
+ val existing = bundles[existingIndex]
176
+ bundles[existingIndex] =
177
+ existing.copy(
178
+ crashedAt = System.currentTimeMillis(),
179
+ crashCount = existing.crashCount + 1,
180
+ )
181
+ } else {
182
+ // Add new entry
183
+ bundles.add(
184
+ CrashedBundleEntry(
185
+ bundleId = bundleId,
186
+ crashedAt = System.currentTimeMillis(),
187
+ crashCount = 1,
188
+ ),
189
+ )
190
+ }
191
+
192
+ // Trim to max size (keep most recent)
193
+ if (bundles.size > maxHistorySize) {
194
+ bundles.sortBy { it.crashedAt }
195
+ while (bundles.size > maxHistorySize) {
196
+ bundles.removeAt(0)
197
+ }
198
+ }
199
+ }
200
+
201
+ fun clear() {
202
+ bundles.clear()
203
+ }
204
+ }
@@ -6,52 +6,41 @@ import com.facebook.react.bridge.ReactApplicationContext
6
6
 
7
7
  /**
8
8
  * Main React Native package for HotUpdater
9
+ * Provides static utility methods and a default singleton instance
9
10
  */
10
11
  class HotUpdater {
11
12
  companion object {
12
- /**
13
- * Gets the app version
14
- * @param context Application context
15
- * @return App version name or null if not available
16
- */
17
- fun getAppVersion(context: Context): String? = HotUpdaterFactory.getInstance(context).getAppVersion()
18
-
19
- /**
20
- * Generates a bundle ID based on build timestamp
21
- * @param context Application context
22
- * @return The minimum bundle ID string
23
- */
24
- fun getMinBundleId(context: Context): String = HotUpdaterFactory.getInstance(context).getMinBundleId()
13
+ @Volatile
14
+ private var instance: HotUpdaterImpl? = null
25
15
 
26
16
  /**
27
- * Gets the current fingerprint hash
17
+ * Gets or creates the singleton instance
18
+ * Thread-safe double-checked locking
28
19
  * @param context Application context
29
- * @return The fingerprint hash or null if not set
20
+ * @return The singleton HotUpdaterImpl instance
30
21
  */
31
- fun getFingerprintHash(context: Context): String? = HotUpdaterFactory.getInstance(context).getFingerprintHash()
32
-
33
- /**
34
- * Gets the current update channel
35
- * @param context Application context
36
- * @return The channel name or null if not set
37
- */
38
- fun getChannel(context: Context): String? = HotUpdaterFactory.getInstance(context).getChannel()
22
+ fun getInstance(context: Context): HotUpdaterImpl =
23
+ instance ?: synchronized(this) {
24
+ instance ?: HotUpdaterImpl(context.applicationContext).also {
25
+ instance = it
26
+ }
27
+ }
39
28
 
40
29
  /**
41
- * Gets the path to the bundle file
30
+ * Gets the JS bundle file path using the default singleton instance
42
31
  * @param context Application context
43
32
  * @return The path to the bundle file
44
33
  */
45
- fun getJSBundleFile(context: Context): String = HotUpdaterFactory.getInstance(context).getJSBundleFile()
34
+ fun getJSBundleFile(context: Context): String = getInstance(context).getJSBundleFile()
46
35
 
47
36
  /**
48
- * Updates the bundle from the specified URL
37
+ * Updates the bundle using the default singleton instance
49
38
  * @param context Application context
50
39
  * @param bundleId ID of the bundle to update
51
40
  * @param fileUrl URL of the bundle file to download (or null to reset)
52
41
  * @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
53
42
  * @param progressCallback Callback for download progress updates
54
- * @return true if the update was successful
43
+ * @throws HotUpdaterException if the update fails
55
44
  */
56
45
  suspend fun updateBundle(
57
46
  context: Context,
@@ -59,23 +48,46 @@ class HotUpdater {
59
48
  fileUrl: String?,
60
49
  fileHash: String?,
61
50
  progressCallback: (Double) -> Unit,
62
- ): Boolean =
63
- HotUpdaterFactory.getInstance(context).updateBundle(
64
- bundleId,
65
- fileUrl,
66
- fileHash,
67
- progressCallback,
68
- )
51
+ ) {
52
+ getInstance(context).updateBundle(bundleId, fileUrl, fileHash, progressCallback)
53
+ }
69
54
 
70
55
  /**
71
- * Reloads the React Native application
56
+ * Reloads the React Native application using the default singleton instance
72
57
  * @param context Application context
73
58
  */
74
59
  suspend fun reload(context: Context) {
75
60
  val currentActivity = getCurrentActivity(context)
76
- HotUpdaterFactory.getInstance(context).reload(currentActivity)
61
+ getInstance(context).reload(currentActivity)
77
62
  }
78
63
 
64
+ /**
65
+ * Gets the app version - delegates to HotUpdaterImpl static method
66
+ * @param context Application context
67
+ * @return App version name or null if not available
68
+ */
69
+ fun getAppVersion(context: Context): String? = HotUpdaterImpl.getAppVersion(context)
70
+
71
+ /**
72
+ * Gets the minimum bundle ID - delegates to HotUpdaterImpl static method
73
+ * @return The minimum bundle ID string
74
+ */
75
+ fun getMinBundleId(): String = HotUpdaterImpl.getMinBundleId()
76
+
77
+ /**
78
+ * Gets the current fingerprint hash - delegates to HotUpdaterImpl static method
79
+ * @param context Application context
80
+ * @return The fingerprint hash or null if not set
81
+ */
82
+ fun getFingerprintHash(context: Context): String? = HotUpdaterImpl.getFingerprintHash(context)
83
+
84
+ /**
85
+ * Gets the current update channel - delegates to HotUpdaterImpl static method
86
+ * @param context Application context
87
+ * @return The channel name or null if not set
88
+ */
89
+ fun getChannel(context: Context): String = HotUpdaterImpl.getChannel(context)
90
+
79
91
  /**
80
92
  * Gets the current activity from ReactApplicationContext
81
93
  * @param context Context that might be a ReactApplicationContext
@@ -0,0 +1,134 @@
1
+ package com.hotupdater
2
+
3
+ /**
4
+ * Exception class for Hot Updater errors
5
+ * Matches error codes defined in packages/react-native/src/errors.ts
6
+ */
7
+ class HotUpdaterException(
8
+ val code: String,
9
+ message: String,
10
+ cause: Throwable? = null,
11
+ ) : Exception(message, cause) {
12
+ companion object {
13
+ // Parameter validation errors
14
+ fun missingBundleId() =
15
+ HotUpdaterException(
16
+ "MISSING_BUNDLE_ID",
17
+ "Missing or empty 'bundleId'",
18
+ )
19
+
20
+ fun invalidFileUrl() =
21
+ HotUpdaterException(
22
+ "INVALID_FILE_URL",
23
+ "Invalid 'fileUrl' provided",
24
+ )
25
+
26
+ // Bundle storage errors
27
+ fun directoryCreationFailed() =
28
+ HotUpdaterException(
29
+ "DIRECTORY_CREATION_FAILED",
30
+ "Failed to create bundle directory",
31
+ )
32
+
33
+ fun downloadFailed(cause: Throwable? = null) =
34
+ HotUpdaterException(
35
+ "DOWNLOAD_FAILED",
36
+ "Failed to download bundle",
37
+ cause,
38
+ )
39
+
40
+ fun incompleteDownload(
41
+ expectedSize: Long,
42
+ actualSize: Long,
43
+ ) = HotUpdaterException(
44
+ "INCOMPLETE_DOWNLOAD",
45
+ "Download incomplete: received $actualSize bytes, expected $expectedSize bytes",
46
+ )
47
+
48
+ fun extractionFormatError(cause: Throwable? = null) =
49
+ HotUpdaterException(
50
+ "EXTRACTION_FORMAT_ERROR",
51
+ "Invalid or corrupted bundle archive format",
52
+ cause,
53
+ )
54
+
55
+ fun invalidBundle() =
56
+ HotUpdaterException(
57
+ "INVALID_BUNDLE",
58
+ "Bundle missing required platform files (index.ios.bundle or index.android.bundle)",
59
+ )
60
+
61
+ fun insufficientDiskSpace(
62
+ required: Long,
63
+ available: Long,
64
+ ) = HotUpdaterException(
65
+ "INSUFFICIENT_DISK_SPACE",
66
+ "Insufficient disk space: need $required bytes, available $available bytes",
67
+ )
68
+
69
+ fun signatureVerificationFailed(cause: Throwable? = null) =
70
+ HotUpdaterException(
71
+ "SIGNATURE_VERIFICATION_FAILED",
72
+ "Bundle signature verification failed",
73
+ cause,
74
+ )
75
+
76
+ fun moveOperationFailed() =
77
+ HotUpdaterException(
78
+ "MOVE_OPERATION_FAILED",
79
+ "Failed to move bundle files",
80
+ )
81
+
82
+ fun bundleInCrashedHistory(bundleId: String) =
83
+ HotUpdaterException(
84
+ "BUNDLE_IN_CRASHED_HISTORY",
85
+ "Bundle '$bundleId' is in crashed history and cannot be applied",
86
+ )
87
+
88
+ // Signature verification errors
89
+ fun publicKeyNotConfigured() =
90
+ HotUpdaterException(
91
+ "PUBLIC_KEY_NOT_CONFIGURED",
92
+ "Public key not configured for signature verification",
93
+ )
94
+
95
+ fun invalidPublicKeyFormat() =
96
+ HotUpdaterException(
97
+ "INVALID_PUBLIC_KEY_FORMAT",
98
+ "Invalid public key format",
99
+ )
100
+
101
+ fun fileHashMismatch() =
102
+ HotUpdaterException(
103
+ "FILE_HASH_MISMATCH",
104
+ "File hash verification failed",
105
+ )
106
+
107
+ fun fileReadFailed() =
108
+ HotUpdaterException(
109
+ "FILE_READ_FAILED",
110
+ "Failed to read file for verification",
111
+ )
112
+
113
+ fun unsignedNotAllowed() =
114
+ HotUpdaterException(
115
+ "UNSIGNED_NOT_ALLOWED",
116
+ "Unsigned bundles are not allowed",
117
+ )
118
+
119
+ fun securityFrameworkError(cause: Throwable? = null) =
120
+ HotUpdaterException(
121
+ "SECURITY_FRAMEWORK_ERROR",
122
+ "Security framework error occurred",
123
+ cause,
124
+ )
125
+
126
+ // Internal errors
127
+ fun unknownError(cause: Throwable? = null) =
128
+ HotUpdaterException(
129
+ "UNKNOWN_ERROR",
130
+ "An unknown error occurred",
131
+ cause,
132
+ )
133
+ }
134
+ }