@hot-updater/react-native 0.23.1 → 0.24.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/src/main/java/com/hotupdater/BundleFileStorageService.kt +393 -49
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +204 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +48 -36
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +134 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +168 -95
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +15 -3
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +17 -12
- package/android/src/newarch/HotUpdaterModule.kt +88 -23
- package/android/src/oldarch/HotUpdaterModule.kt +89 -22
- package/android/src/oldarch/HotUpdaterSpec.kt +6 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +401 -77
- package/ios/HotUpdater/Internal/BundleMetadata.swift +177 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +213 -47
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +96 -25
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +35 -29
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +2 -2
- package/ios/HotUpdater/Public/HotUpdater.h +8 -2
- package/lib/commonjs/DefaultResolver.js +38 -0
- package/lib/commonjs/DefaultResolver.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +33 -45
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/error.js +45 -1
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchUpdateInfo.js +7 -45
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
- package/lib/commonjs/index.js +249 -208
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +103 -3
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js +12 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +70 -1
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +34 -0
- package/lib/module/DefaultResolver.js.map +1 -0
- package/lib/module/checkForUpdate.js +34 -43
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/error.js +45 -0
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchUpdateInfo.js +7 -45
- package/lib/module/fetchUpdateInfo.js.map +1 -1
- package/lib/module/index.js +250 -203
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +87 -2
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js +12 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +71 -2
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts +10 -0
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +12 -13
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +120 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +38 -44
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +58 -2
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +115 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +132 -7
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts +10 -0
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +12 -13
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +120 -0
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +38 -44
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +58 -2
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +115 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +132 -7
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/plugin/build/withHotUpdater.js +3 -3
- package/src/DefaultResolver.ts +36 -0
- package/src/checkForUpdate.ts +51 -56
- package/src/error.ts +153 -0
- package/src/fetchUpdateInfo.ts +10 -58
- package/src/index.ts +315 -206
- package/src/native.ts +88 -2
- package/src/specs/NativeHotUpdater.ts +63 -0
- package/src/types.ts +135 -0
- package/src/wrap.tsx +245 -34
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +0 -52
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +0 -24
- package/lib/commonjs/runUpdateProcess.js +0 -69
- package/lib/commonjs/runUpdateProcess.js.map +0 -1
- package/lib/module/runUpdateProcess.js +0 -64
- package/lib/module/runUpdateProcess.js.map +0 -1
- package/lib/typescript/commonjs/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +0 -1
- package/lib/typescript/module/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/module/runUpdateProcess.d.ts.map +0 -1
- package/src/runUpdateProcess.ts +0 -80
|
@@ -9,11 +9,33 @@ import kotlinx.coroutines.withContext
|
|
|
9
9
|
/**
|
|
10
10
|
* Core implementation class for HotUpdater functionality
|
|
11
11
|
*/
|
|
12
|
-
class HotUpdaterImpl
|
|
13
|
-
private val context: Context
|
|
14
|
-
private val bundleStorage: BundleStorageService
|
|
15
|
-
private val preferences: PreferencesService
|
|
16
|
-
|
|
12
|
+
class HotUpdaterImpl {
|
|
13
|
+
private val context: Context
|
|
14
|
+
private val bundleStorage: BundleStorageService
|
|
15
|
+
private val preferences: PreferencesService
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Primary constructor with dependency injection (for testing)
|
|
19
|
+
*/
|
|
20
|
+
constructor(
|
|
21
|
+
context: Context,
|
|
22
|
+
bundleStorage: BundleStorageService,
|
|
23
|
+
preferences: PreferencesService,
|
|
24
|
+
) {
|
|
25
|
+
this.context = context.applicationContext
|
|
26
|
+
this.bundleStorage = bundleStorage
|
|
27
|
+
this.preferences = preferences
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convenience constructor for simple usage
|
|
32
|
+
*/
|
|
33
|
+
constructor(context: Context) : this(
|
|
34
|
+
context = context,
|
|
35
|
+
bundleStorage = createBundleStorage(context),
|
|
36
|
+
preferences = createPreferences(context),
|
|
37
|
+
)
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
40
|
* Gets the app version
|
|
19
41
|
* @param context Application context
|
|
@@ -38,8 +60,62 @@ class HotUpdaterImpl(
|
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
companion object {
|
|
63
|
+
private const val TAG = "HotUpdaterImpl"
|
|
41
64
|
private const val DEFAULT_CHANNEL = "production"
|
|
42
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Create BundleStorageService with all dependencies
|
|
68
|
+
*/
|
|
69
|
+
private fun createBundleStorage(context: Context): BundleStorageService {
|
|
70
|
+
val appContext = context.applicationContext
|
|
71
|
+
val fileSystem = FileManagerService(appContext)
|
|
72
|
+
val preferences = createPreferences(appContext)
|
|
73
|
+
val downloadService = OkHttpDownloadService()
|
|
74
|
+
val decompressService = DecompressService()
|
|
75
|
+
|
|
76
|
+
return BundleFileStorageService(
|
|
77
|
+
appContext,
|
|
78
|
+
fileSystem,
|
|
79
|
+
downloadService,
|
|
80
|
+
decompressService,
|
|
81
|
+
preferences,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create PreferencesService with isolation key
|
|
87
|
+
*/
|
|
88
|
+
private fun createPreferences(context: Context): PreferencesService {
|
|
89
|
+
val appContext = context.applicationContext
|
|
90
|
+
val isolationKey = getIsolationKey(appContext)
|
|
91
|
+
return VersionedPreferencesService(appContext, isolationKey)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets the complete isolation key for preferences storage
|
|
96
|
+
* @param context Application context
|
|
97
|
+
* @return The isolation key in format: HotUpdaterPrefs_{fingerprintOrVersion}_{channel}
|
|
98
|
+
*/
|
|
99
|
+
private fun getIsolationKey(context: Context): String {
|
|
100
|
+
// Get fingerprint hash directly from resources
|
|
101
|
+
val fingerprintId = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
|
|
102
|
+
val fingerprintHash =
|
|
103
|
+
if (fingerprintId != 0) {
|
|
104
|
+
context.getString(fingerprintId).takeIf { it.isNotEmpty() }
|
|
105
|
+
} else {
|
|
106
|
+
null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get app version and channel
|
|
110
|
+
val appVersion = getAppVersion(context) ?: "unknown"
|
|
111
|
+
val appChannel = getChannel(context)
|
|
112
|
+
|
|
113
|
+
// Use fingerprint if available, otherwise use app version
|
|
114
|
+
val baseKey = if (!fingerprintHash.isNullOrEmpty()) fingerprintHash else appVersion
|
|
115
|
+
|
|
116
|
+
return "HotUpdaterPrefs_${baseKey}_$appChannel"
|
|
117
|
+
}
|
|
118
|
+
|
|
43
119
|
fun getAppVersion(context: Context): String? =
|
|
44
120
|
try {
|
|
45
121
|
val packageInfo =
|
|
@@ -68,97 +144,72 @@ class HotUpdaterImpl(
|
|
|
68
144
|
}
|
|
69
145
|
|
|
70
146
|
/**
|
|
71
|
-
*
|
|
72
|
-
* @
|
|
73
|
-
* @return The isolation key in format: HotUpdaterPrefs_{fingerprintOrVersion}_{channel}
|
|
147
|
+
* Get minimum bundle ID string
|
|
148
|
+
* @return The minimum bundle ID string
|
|
74
149
|
*/
|
|
75
|
-
fun
|
|
76
|
-
// Get fingerprint hash directly from resources
|
|
77
|
-
val fingerprintId = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
|
|
78
|
-
val fingerprintHash =
|
|
79
|
-
if (fingerprintId != 0) {
|
|
80
|
-
context.getString(fingerprintId).takeIf { it.isNotEmpty() }
|
|
81
|
-
} else {
|
|
82
|
-
null
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Get app version and channel
|
|
86
|
-
val appVersion = getAppVersion(context) ?: "unknown"
|
|
87
|
-
val appChannel = getChannel(context)
|
|
88
|
-
|
|
89
|
-
// Use fingerprint if available, otherwise use app version
|
|
90
|
-
val baseKey = if (!fingerprintHash.isNullOrEmpty()) fingerprintHash else appVersion
|
|
91
|
-
|
|
92
|
-
// Build complete isolation key
|
|
93
|
-
return "HotUpdaterPrefs_${baseKey}_$appChannel"
|
|
94
|
-
}
|
|
95
|
-
}
|
|
150
|
+
fun getMinBundleId(): String = BuildConfig.MIN_BUNDLE_ID.takeIf { it != "null" } ?: generateMinBundleIdFromBuildTimestamp()
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
bytes[14].toInt() and 0xFF,
|
|
146
|
-
bytes[15].toInt() and 0xFF,
|
|
147
|
-
)
|
|
148
|
-
} catch (e: Exception) {
|
|
149
|
-
"00000000-0000-0000-0000-000000000000"
|
|
150
|
-
}
|
|
152
|
+
/**
|
|
153
|
+
* Generates a bundle ID based on build timestamp
|
|
154
|
+
* @return The generated minimum bundle ID string
|
|
155
|
+
*/
|
|
156
|
+
private fun generateMinBundleIdFromBuildTimestamp(): String =
|
|
157
|
+
try {
|
|
158
|
+
val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
|
|
159
|
+
val bytes =
|
|
160
|
+
ByteArray(16).apply {
|
|
161
|
+
this[0] = ((buildTimestampMs shr 40) and 0xFF).toByte()
|
|
162
|
+
this[1] = ((buildTimestampMs shr 32) and 0xFF).toByte()
|
|
163
|
+
this[2] = ((buildTimestampMs shr 24) and 0xFF).toByte()
|
|
164
|
+
this[3] = ((buildTimestampMs shr 16) and 0xFF).toByte()
|
|
165
|
+
this[4] = ((buildTimestampMs shr 8) and 0xFF).toByte()
|
|
166
|
+
this[5] = (buildTimestampMs and 0xFF).toByte()
|
|
167
|
+
this[6] = 0x70.toByte()
|
|
168
|
+
this[7] = 0x00.toByte()
|
|
169
|
+
this[8] = 0x80.toByte()
|
|
170
|
+
this[9] = 0x00.toByte()
|
|
171
|
+
this[10] = 0x00.toByte()
|
|
172
|
+
this[11] = 0x00.toByte()
|
|
173
|
+
this[12] = 0x00.toByte()
|
|
174
|
+
this[13] = 0x00.toByte()
|
|
175
|
+
this[14] = 0x00.toByte()
|
|
176
|
+
this[15] = 0x00.toByte()
|
|
177
|
+
}
|
|
178
|
+
String.format(
|
|
179
|
+
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
180
|
+
bytes[0].toInt() and 0xFF,
|
|
181
|
+
bytes[1].toInt() and 0xFF,
|
|
182
|
+
bytes[2].toInt() and 0xFF,
|
|
183
|
+
bytes[3].toInt() and 0xFF,
|
|
184
|
+
bytes[4].toInt() and 0xFF,
|
|
185
|
+
bytes[5].toInt() and 0xFF,
|
|
186
|
+
bytes[6].toInt() and 0xFF,
|
|
187
|
+
bytes[7].toInt() and 0xFF,
|
|
188
|
+
bytes[8].toInt() and 0xFF,
|
|
189
|
+
bytes[9].toInt() and 0xFF,
|
|
190
|
+
bytes[10].toInt() and 0xFF,
|
|
191
|
+
bytes[11].toInt() and 0xFF,
|
|
192
|
+
bytes[12].toInt() and 0xFF,
|
|
193
|
+
bytes[13].toInt() and 0xFF,
|
|
194
|
+
bytes[14].toInt() and 0xFF,
|
|
195
|
+
bytes[15].toInt() and 0xFF,
|
|
196
|
+
)
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
"00000000-0000-0000-0000-000000000000"
|
|
199
|
+
}
|
|
151
200
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Gets the current fingerprint hash
|
|
203
|
+
* @param context Application context
|
|
204
|
+
* @return The fingerprint hash or null if not set
|
|
205
|
+
*/
|
|
206
|
+
fun getFingerprintHash(context: Context): String? {
|
|
207
|
+
val id = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
|
|
208
|
+
return if (id != 0) {
|
|
209
|
+
context.getString(id).takeIf { it.isNotEmpty() }
|
|
210
|
+
} else {
|
|
211
|
+
null
|
|
212
|
+
}
|
|
162
213
|
}
|
|
163
214
|
}
|
|
164
215
|
|
|
@@ -187,14 +238,16 @@ class HotUpdaterImpl(
|
|
|
187
238
|
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
188
239
|
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
189
240
|
* @param progressCallback Callback for download progress updates
|
|
190
|
-
* @
|
|
241
|
+
* @throws HotUpdaterException if the update fails
|
|
191
242
|
*/
|
|
192
243
|
suspend fun updateBundle(
|
|
193
244
|
bundleId: String,
|
|
194
245
|
fileUrl: String?,
|
|
195
246
|
fileHash: String?,
|
|
196
247
|
progressCallback: (Double) -> Unit,
|
|
197
|
-
)
|
|
248
|
+
) {
|
|
249
|
+
bundleStorage.updateBundle(bundleId, fileUrl, fileHash, progressCallback)
|
|
250
|
+
}
|
|
198
251
|
|
|
199
252
|
/**
|
|
200
253
|
* Reloads the React Native application
|
|
@@ -217,4 +270,24 @@ class HotUpdaterImpl(
|
|
|
217
270
|
Log.e("HotUpdaterImpl", "Failed to reload application", e)
|
|
218
271
|
}
|
|
219
272
|
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Notifies the system that the app has successfully started with the given bundle.
|
|
276
|
+
* If the bundle matches the staging bundle, it promotes to stable.
|
|
277
|
+
* @param bundleId The ID of the currently running bundle
|
|
278
|
+
* @return Map containing status and optional crashedBundleId
|
|
279
|
+
*/
|
|
280
|
+
fun notifyAppReady(bundleId: String): Map<String, Any?> = bundleStorage.notifyAppReady(bundleId)
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Gets the crashed bundle history.
|
|
284
|
+
* @return List of crashed bundle IDs
|
|
285
|
+
*/
|
|
286
|
+
fun getCrashHistory(): List<String> = bundleStorage.getCrashHistory().bundles.map { it.bundleId }
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Clears the crashed bundle history.
|
|
290
|
+
* @return true if clearing was successful
|
|
291
|
+
*/
|
|
292
|
+
fun clearCrashHistory(): Boolean = bundleStorage.clearCrashHistory()
|
|
220
293
|
}
|
|
@@ -20,6 +20,14 @@ import java.net.URL
|
|
|
20
20
|
import java.net.UnknownHostException
|
|
21
21
|
import java.util.concurrent.TimeUnit
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Exception for incomplete downloads with size information
|
|
25
|
+
*/
|
|
26
|
+
class IncompleteDownloadException(
|
|
27
|
+
val expectedSize: Long,
|
|
28
|
+
val actualSize: Long,
|
|
29
|
+
) : IOException("Download incomplete: received $actualSize bytes, expected $expectedSize bytes")
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* Result wrapper for download operations
|
|
25
33
|
*/
|
|
@@ -255,12 +263,16 @@ class OkHttpDownloadService : DownloadService {
|
|
|
255
263
|
// Verify file size
|
|
256
264
|
val finalSize = destination.length()
|
|
257
265
|
if (finalSize != totalSize) {
|
|
258
|
-
|
|
259
|
-
Log.d(TAG, errorMsg)
|
|
266
|
+
Log.d(TAG, "Download incomplete: $finalSize / $totalSize bytes")
|
|
260
267
|
|
|
261
268
|
// Delete incomplete file
|
|
262
269
|
destination.delete()
|
|
263
|
-
return@withContext DownloadResult.Error(
|
|
270
|
+
return@withContext DownloadResult.Error(
|
|
271
|
+
IncompleteDownloadException(
|
|
272
|
+
expectedSize = totalSize,
|
|
273
|
+
actualSize = finalSize,
|
|
274
|
+
),
|
|
275
|
+
)
|
|
264
276
|
}
|
|
265
277
|
|
|
266
278
|
Log.d(TAG, "Download completed successfully: $finalSize bytes")
|
|
@@ -35,24 +35,29 @@ sealed class SignatureVerificationException(
|
|
|
35
35
|
"Public key format is invalid. Ensure the public key is in PEM format (BEGIN PUBLIC KEY)",
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
class MissingFileHash :
|
|
39
|
+
SignatureVerificationException(
|
|
40
|
+
"File hash is missing or empty. Ensure the bundle update includes a valid file hash",
|
|
41
|
+
)
|
|
42
|
+
|
|
38
43
|
class InvalidSignatureFormat :
|
|
39
44
|
SignatureVerificationException(
|
|
40
|
-
"Signature format is invalid. The signature
|
|
45
|
+
"Signature format is invalid or corrupted. The signature data is malformed or cannot be decoded",
|
|
41
46
|
)
|
|
42
47
|
|
|
43
|
-
class
|
|
48
|
+
class SignatureVerificationFailed :
|
|
44
49
|
SignatureVerificationException(
|
|
45
50
|
"Bundle signature verification failed. The bundle may be corrupted or tampered with",
|
|
46
51
|
)
|
|
47
52
|
|
|
48
|
-
class
|
|
53
|
+
class FileHashMismatch :
|
|
49
54
|
SignatureVerificationException(
|
|
50
|
-
"
|
|
55
|
+
"File hash verification failed. The bundle file hash does not match the expected value. File may be corrupted",
|
|
51
56
|
)
|
|
52
57
|
|
|
53
|
-
class
|
|
58
|
+
class FileReadFailed :
|
|
54
59
|
SignatureVerificationException(
|
|
55
|
-
"Failed to
|
|
60
|
+
"Failed to read file for verification. Could not read file for hash verification",
|
|
56
61
|
)
|
|
57
62
|
|
|
58
63
|
class UnsignedNotAllowed :
|
|
@@ -156,7 +161,7 @@ object SignatureVerifier {
|
|
|
156
161
|
// Rule: null/empty fileHash → REJECT
|
|
157
162
|
if (fileHash.isNullOrEmpty()) {
|
|
158
163
|
Log.e(TAG, "fileHash is null or empty. Rejecting update.")
|
|
159
|
-
throw SignatureVerificationException.
|
|
164
|
+
throw SignatureVerificationException.MissingFileHash()
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
if (isSignedFormat(fileHash)) {
|
|
@@ -193,7 +198,7 @@ object SignatureVerifier {
|
|
|
193
198
|
* Verifies SHA256 hash of a file.
|
|
194
199
|
* @param bundleFile The file to verify
|
|
195
200
|
* @param expectedHash Expected SHA256 hash (hex string)
|
|
196
|
-
* @throws SignatureVerificationException.
|
|
201
|
+
* @throws SignatureVerificationException.FileHashMismatch if verification fails
|
|
197
202
|
*/
|
|
198
203
|
fun verifyHash(
|
|
199
204
|
bundleFile: File,
|
|
@@ -203,7 +208,7 @@ object SignatureVerifier {
|
|
|
203
208
|
|
|
204
209
|
if (!HashUtils.verifyHash(bundleFile, expectedHash)) {
|
|
205
210
|
Log.e(TAG, "Hash mismatch!")
|
|
206
|
-
throw SignatureVerificationException.
|
|
211
|
+
throw SignatureVerificationException.FileHashMismatch()
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
Log.i(TAG, "✅ Hash verified successfully")
|
|
@@ -242,7 +247,7 @@ object SignatureVerifier {
|
|
|
242
247
|
HashUtils.calculateSHA256(bundleFile)
|
|
243
248
|
?: run {
|
|
244
249
|
Log.e(TAG, "Failed to calculate file hash")
|
|
245
|
-
throw SignatureVerificationException.
|
|
250
|
+
throw SignatureVerificationException.FileReadFailed()
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
Log.d(TAG, "Calculated file hash: $fileHashHex")
|
|
@@ -269,7 +274,7 @@ object SignatureVerifier {
|
|
|
269
274
|
Log.i(TAG, "✅ Signature verified successfully")
|
|
270
275
|
} else {
|
|
271
276
|
Log.e(TAG, "❌ Signature verification failed")
|
|
272
|
-
throw SignatureVerificationException.
|
|
277
|
+
throw SignatureVerificationException.SignatureVerificationFailed()
|
|
273
278
|
}
|
|
274
279
|
} catch (e: SignatureVerificationException) {
|
|
275
280
|
throw e
|
|
@@ -318,7 +323,7 @@ object SignatureVerifier {
|
|
|
318
323
|
* Converts hex string to ByteArray.
|
|
319
324
|
* @param hexString Hex-encoded string
|
|
320
325
|
* @return ByteArray
|
|
321
|
-
* @throws SignatureVerificationException.
|
|
326
|
+
* @throws SignatureVerificationException.SignatureVerificationFailed if conversion fails
|
|
322
327
|
*/
|
|
323
328
|
private fun hexToByteArray(hexString: String): ByteArray {
|
|
324
329
|
try {
|
|
@@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope
|
|
|
6
6
|
import com.facebook.react.bridge.Promise
|
|
7
7
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
8
|
import com.facebook.react.bridge.ReadableMap
|
|
9
|
+
import com.facebook.react.bridge.WritableNativeArray
|
|
9
10
|
import com.facebook.react.bridge.WritableNativeMap
|
|
10
11
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
12
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -19,10 +20,17 @@ class HotUpdaterModule internal constructor(
|
|
|
19
20
|
|
|
20
21
|
override fun getName(): String = NAME
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Gets the singleton HotUpdaterImpl instance
|
|
25
|
+
*/
|
|
26
|
+
private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
|
|
27
|
+
|
|
22
28
|
override fun reload(promise: Promise) {
|
|
23
29
|
CoroutineScope(Dispatchers.Main.immediate).launch {
|
|
24
30
|
try {
|
|
25
|
-
|
|
31
|
+
val impl = getInstance()
|
|
32
|
+
val currentActivity = mReactApplicationContext.currentActivity
|
|
33
|
+
impl.reload(currentActivity)
|
|
26
34
|
promise.resolve(null)
|
|
27
35
|
} catch (e: Exception) {
|
|
28
36
|
Log.d("HotUpdater", "Failed to reload", e)
|
|
@@ -32,41 +40,66 @@ class HotUpdaterModule internal constructor(
|
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
override fun updateBundle(
|
|
35
|
-
params: ReadableMap
|
|
43
|
+
params: ReadableMap?,
|
|
36
44
|
promise: Promise,
|
|
37
45
|
) {
|
|
38
46
|
(mReactApplicationContext.currentActivity as FragmentActivity?)?.lifecycleScope?.launch {
|
|
39
47
|
try {
|
|
40
|
-
|
|
48
|
+
// Parameter validation
|
|
49
|
+
if (params == null) {
|
|
50
|
+
promise.reject("UNKNOWN_ERROR", "Missing or invalid parameters for updateBundle")
|
|
51
|
+
return@launch
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val bundleId = params.getString("bundleId")
|
|
55
|
+
if (bundleId == null || bundleId.isEmpty()) {
|
|
56
|
+
promise.reject("MISSING_BUNDLE_ID", "Missing or empty 'bundleId'")
|
|
57
|
+
return@launch
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
val fileUrl = params.getString("fileUrl")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
val progressParams =
|
|
51
|
-
WritableNativeMap().apply {
|
|
52
|
-
putDouble("progress", progress)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
this@HotUpdaterModule
|
|
56
|
-
.mReactApplicationContext
|
|
57
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
58
|
-
.emit("onProgress", progressParams)
|
|
61
|
+
|
|
62
|
+
// Validate fileUrl format if provided
|
|
63
|
+
if (fileUrl != null && fileUrl.isNotEmpty()) {
|
|
64
|
+
try {
|
|
65
|
+
java.net.URL(fileUrl)
|
|
66
|
+
} catch (e: java.net.MalformedURLException) {
|
|
67
|
+
promise.reject("INVALID_FILE_URL", "Invalid 'fileUrl' provided: $fileUrl")
|
|
68
|
+
return@launch
|
|
59
69
|
}
|
|
60
|
-
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val fileHash = params.getString("fileHash")
|
|
73
|
+
|
|
74
|
+
val impl = getInstance()
|
|
75
|
+
|
|
76
|
+
impl.updateBundle(
|
|
77
|
+
bundleId,
|
|
78
|
+
fileUrl,
|
|
79
|
+
fileHash,
|
|
80
|
+
) { progress ->
|
|
81
|
+
val progressParams =
|
|
82
|
+
WritableNativeMap().apply {
|
|
83
|
+
putDouble("progress", progress)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this@HotUpdaterModule
|
|
87
|
+
.mReactApplicationContext
|
|
88
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
89
|
+
.emit("onProgress", progressParams)
|
|
90
|
+
}
|
|
91
|
+
promise.resolve(true)
|
|
92
|
+
} catch (e: HotUpdaterException) {
|
|
93
|
+
promise.reject(e.code, e.message)
|
|
61
94
|
} catch (e: Exception) {
|
|
62
|
-
promise.reject("
|
|
95
|
+
promise.reject("UNKNOWN_ERROR", e.message ?: "An unknown error occurred")
|
|
63
96
|
}
|
|
64
97
|
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
override fun getTypedExportedConstants(): Map<String, Any?> {
|
|
68
101
|
val constants: MutableMap<String, Any?> = HashMap()
|
|
69
|
-
constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId(
|
|
102
|
+
constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId()
|
|
70
103
|
constants["APP_VERSION"] = HotUpdater.getAppVersion(mReactApplicationContext)
|
|
71
104
|
constants["CHANNEL"] = HotUpdater.getChannel(mReactApplicationContext)
|
|
72
105
|
constants["FINGERPRINT_HASH"] = HotUpdater.getFingerprintHash(mReactApplicationContext)
|
|
@@ -85,6 +118,38 @@ class HotUpdaterModule internal constructor(
|
|
|
85
118
|
// No-op
|
|
86
119
|
}
|
|
87
120
|
|
|
121
|
+
override fun notifyAppReady(params: ReadableMap?): WritableNativeMap {
|
|
122
|
+
val result = WritableNativeMap()
|
|
123
|
+
val bundleId = params?.getString("bundleId")
|
|
124
|
+
if (bundleId == null) {
|
|
125
|
+
result.putString("status", "STABLE")
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
val impl = getInstance()
|
|
130
|
+
val statusMap = impl.notifyAppReady(bundleId)
|
|
131
|
+
|
|
132
|
+
result.putString("status", statusMap["status"] as? String ?: "STABLE")
|
|
133
|
+
statusMap["crashedBundleId"]?.let {
|
|
134
|
+
result.putString("crashedBundleId", it as String)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override fun getCrashHistory(): WritableNativeArray {
|
|
141
|
+
val impl = getInstance()
|
|
142
|
+
val crashHistory = impl.getCrashHistory()
|
|
143
|
+
val result = WritableNativeArray()
|
|
144
|
+
crashHistory.forEach { result.pushString(it) }
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override fun clearCrashHistory(): Boolean {
|
|
149
|
+
val impl = getInstance()
|
|
150
|
+
return impl.clearCrashHistory()
|
|
151
|
+
}
|
|
152
|
+
|
|
88
153
|
companion object {
|
|
89
154
|
const val NAME = "HotUpdater"
|
|
90
155
|
}
|