@hot-updater/react-native 0.10.2 → 0.12.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.
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +97 -46
- package/dist/index.js +4 -5
- package/dist/index.mjs +4 -5
- package/dist/native.d.ts +1 -1
- package/ios/HotUpdater/HotUpdater.mm +198 -105
- package/package.json +3 -3
- package/src/checkUpdate.ts +0 -3
- package/src/native.ts +4 -2
|
@@ -26,26 +26,6 @@ class HotUpdater : ReactPackage {
|
|
|
26
26
|
listOf(HotUpdaterModule(context)).toMutableList()
|
|
27
27
|
|
|
28
28
|
companion object {
|
|
29
|
-
private fun convertFileSystemPathFromBasePath(
|
|
30
|
-
context: Context,
|
|
31
|
-
basePath: String,
|
|
32
|
-
): String {
|
|
33
|
-
val documentsDir =
|
|
34
|
-
context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
|
|
35
|
-
val separator = if (basePath.startsWith("/")) "" else "/"
|
|
36
|
-
return "$documentsDir$separator$basePath"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private fun stripPrefixFromPath(
|
|
40
|
-
prefix: String,
|
|
41
|
-
path: String,
|
|
42
|
-
): String =
|
|
43
|
-
if (path.startsWith("/$prefix/")) {
|
|
44
|
-
path.replaceFirst("/$prefix/", "")
|
|
45
|
-
} else {
|
|
46
|
-
path
|
|
47
|
-
}
|
|
48
|
-
|
|
49
29
|
fun getAppVersion(context: Context): String? {
|
|
50
30
|
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
|
51
31
|
return packageInfo.versionName
|
|
@@ -146,17 +126,46 @@ class HotUpdater : ReactPackage {
|
|
|
146
126
|
return true
|
|
147
127
|
}
|
|
148
128
|
|
|
149
|
-
val
|
|
150
|
-
val
|
|
151
|
-
|
|
129
|
+
val baseDir = context.getExternalFilesDir(null)
|
|
130
|
+
val bundleStoreDir = File(baseDir, "bundle-store")
|
|
131
|
+
if (!bundleStoreDir.exists()) {
|
|
132
|
+
bundleStoreDir.mkdirs()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
val finalBundleDir = File(bundleStoreDir, bundleId)
|
|
136
|
+
if (finalBundleDir.exists()) {
|
|
137
|
+
Log.d("HotUpdater", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
|
|
138
|
+
val existingIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
139
|
+
if (existingIndexFile != null) {
|
|
140
|
+
// Update directory modification time to current time after update
|
|
141
|
+
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
142
|
+
setBundleURL(context, existingIndexFile.absolutePath)
|
|
143
|
+
cleanupOldBundles(bundleStoreDir)
|
|
144
|
+
return true
|
|
145
|
+
} else {
|
|
146
|
+
finalBundleDir.deleteRecursively()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
val tempDir = File(baseDir, "bundle-temp")
|
|
151
|
+
if (tempDir.exists()) {
|
|
152
|
+
tempDir.deleteRecursively()
|
|
153
|
+
}
|
|
154
|
+
tempDir.mkdirs()
|
|
155
|
+
|
|
156
|
+
val tempZipFile = File(tempDir, "build.zip")
|
|
157
|
+
val extractedDir = File(tempDir, "extracted")
|
|
158
|
+
extractedDir.mkdirs()
|
|
152
159
|
|
|
153
160
|
val isSuccess =
|
|
154
161
|
withContext(Dispatchers.IO) {
|
|
162
|
+
val downloadUrl = URL(zipUrl)
|
|
155
163
|
val conn =
|
|
156
164
|
try {
|
|
157
165
|
downloadUrl.openConnection() as HttpURLConnection
|
|
158
166
|
} catch (e: Exception) {
|
|
159
167
|
Log.d("HotUpdater", "Failed to open connection: ${e.message}")
|
|
168
|
+
tempDir.deleteRecursively()
|
|
160
169
|
return@withContext false
|
|
161
170
|
}
|
|
162
171
|
|
|
@@ -165,14 +174,11 @@ class HotUpdater : ReactPackage {
|
|
|
165
174
|
val totalSize = conn.contentLength
|
|
166
175
|
if (totalSize <= 0) {
|
|
167
176
|
Log.d("HotUpdater", "Invalid content length: $totalSize")
|
|
177
|
+
tempDir.deleteRecursively()
|
|
168
178
|
return@withContext false
|
|
169
179
|
}
|
|
170
|
-
|
|
171
|
-
val file = File(path)
|
|
172
|
-
file.parentFile?.mkdirs()
|
|
173
|
-
|
|
174
180
|
conn.inputStream.use { input ->
|
|
175
|
-
|
|
181
|
+
tempZipFile.outputStream().use { output ->
|
|
176
182
|
val buffer = ByteArray(8 * 1024)
|
|
177
183
|
var bytesRead: Int
|
|
178
184
|
var totalRead = 0L
|
|
@@ -182,46 +188,91 @@ class HotUpdater : ReactPackage {
|
|
|
182
188
|
output.write(buffer, 0, bytesRead)
|
|
183
189
|
totalRead += bytesRead
|
|
184
190
|
val currentTime = System.currentTimeMillis()
|
|
185
|
-
if (currentTime - lastProgressTime >= 100) {
|
|
191
|
+
if (currentTime - lastProgressTime >= 100) {
|
|
186
192
|
val progress = totalRead.toDouble() / totalSize
|
|
187
193
|
progressCallback.invoke(progress)
|
|
188
194
|
lastProgressTime = currentTime
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
|
-
// Send final progress (100%) after download completes
|
|
192
197
|
progressCallback.invoke(1.0)
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
} catch (e: Exception) {
|
|
196
201
|
Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
|
|
202
|
+
tempDir.deleteRecursively()
|
|
197
203
|
return@withContext false
|
|
198
204
|
} finally {
|
|
199
205
|
conn.disconnect()
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!extractZipFileAtPath(path, extractedPath)) {
|
|
208
|
+
if (!extractZipFileAtPath(tempZipFile.absolutePath, extractedDir.absolutePath)) {
|
|
205
209
|
Log.d("HotUpdater", "Failed to extract zip file.")
|
|
210
|
+
tempDir.deleteRecursively()
|
|
206
211
|
return@withContext false
|
|
207
212
|
}
|
|
213
|
+
true
|
|
214
|
+
}
|
|
208
215
|
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
if (!isSuccess) {
|
|
217
|
+
tempDir.deleteRecursively()
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return@withContext false
|
|
219
|
-
}
|
|
221
|
+
val indexFileExtracted = extractedDir.walk().find { it.name == "index.android.bundle" }
|
|
222
|
+
if (indexFileExtracted == null) {
|
|
223
|
+
Log.d("HotUpdater", "index.android.bundle not found in extracted files.")
|
|
224
|
+
tempDir.deleteRecursively()
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
220
227
|
|
|
221
|
-
|
|
222
|
-
|
|
228
|
+
// Move (or copy) contents from temp folder to finalBundleDir
|
|
229
|
+
if (finalBundleDir.exists()) {
|
|
230
|
+
finalBundleDir.deleteRecursively()
|
|
231
|
+
}
|
|
232
|
+
if (!extractedDir.renameTo(finalBundleDir)) {
|
|
233
|
+
extractedDir.copyRecursively(finalBundleDir, overwrite = true)
|
|
234
|
+
extractedDir.deleteRecursively()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
238
|
+
if (finalIndexFile == null) {
|
|
239
|
+
Log.d("HotUpdater", "index.android.bundle not found in final directory.")
|
|
240
|
+
tempDir.deleteRecursively()
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Update final bundle directory modification time to current time after bundle update
|
|
245
|
+
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
246
|
+
|
|
247
|
+
val bundlePath = finalIndexFile.absolutePath
|
|
248
|
+
Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
|
|
249
|
+
setBundleURL(context, bundlePath)
|
|
250
|
+
|
|
251
|
+
// Clean up old bundles in the bundle store to keep only up to 2 bundles
|
|
252
|
+
cleanupOldBundles(bundleStoreDir)
|
|
253
|
+
|
|
254
|
+
// Clean up temp directory
|
|
255
|
+
tempDir.deleteRecursively()
|
|
256
|
+
|
|
257
|
+
Log.d("HotUpdater", "Downloaded and extracted file successfully.")
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Helper function to delete old bundles, keeping only up to 2 bundles in the bundle-store folder
|
|
262
|
+
private fun cleanupOldBundles(bundleStoreDir: File) {
|
|
263
|
+
// Get list of all directories in bundle-store folder
|
|
264
|
+
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
|
+
val sortedBundles = bundles.sortedByDescending { it.lastModified() }
|
|
268
|
+
|
|
269
|
+
// Delete all bundles except the top 2
|
|
270
|
+
if (sortedBundles.size > 2) {
|
|
271
|
+
sortedBundles.drop(2).forEach { oldBundle ->
|
|
272
|
+
Log.d("HotUpdater", "Removing old bundle: ${oldBundle.name}")
|
|
273
|
+
oldBundle.deleteRecursively()
|
|
223
274
|
}
|
|
224
|
-
|
|
275
|
+
}
|
|
225
276
|
}
|
|
226
277
|
}
|
|
227
278
|
}
|
package/dist/index.js
CHANGED
|
@@ -1689,14 +1689,13 @@ var __webpack_exports__ = {};
|
|
|
1689
1689
|
const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
|
|
1690
1690
|
const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
|
|
1691
1691
|
const reload = ()=>{
|
|
1692
|
-
|
|
1692
|
+
requestAnimationFrame(()=>{
|
|
1693
|
+
HotUpdaterNative.reload();
|
|
1694
|
+
});
|
|
1693
1695
|
};
|
|
1694
1696
|
const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
|
|
1695
1697
|
async function checkForUpdate(config) {
|
|
1696
|
-
if (__DEV__)
|
|
1697
|
-
console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
|
|
1698
|
-
return null;
|
|
1699
|
-
}
|
|
1698
|
+
if (__DEV__) return null;
|
|
1700
1699
|
if (![
|
|
1701
1700
|
"ios",
|
|
1702
1701
|
"android"
|
package/dist/index.mjs
CHANGED
|
@@ -1658,14 +1658,13 @@ const addListener = (eventName, listener)=>{
|
|
|
1658
1658
|
const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
|
|
1659
1659
|
const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
|
|
1660
1660
|
const reload = ()=>{
|
|
1661
|
-
|
|
1661
|
+
requestAnimationFrame(()=>{
|
|
1662
|
+
HotUpdaterNative.reload();
|
|
1663
|
+
});
|
|
1662
1664
|
};
|
|
1663
1665
|
const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
|
|
1664
1666
|
async function checkForUpdate(config) {
|
|
1665
|
-
if (__DEV__)
|
|
1666
|
-
console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
|
|
1667
|
-
return null;
|
|
1668
|
-
}
|
|
1667
|
+
if (__DEV__) return null;
|
|
1669
1668
|
if (![
|
|
1670
1669
|
"ios",
|
|
1671
1670
|
"android"
|
package/dist/native.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const addListener: <T extends keyof HotUpdaterEvent>(eventName: T
|
|
|
7
7
|
/**
|
|
8
8
|
* Downloads files from given URLs.
|
|
9
9
|
*
|
|
10
|
-
* @param {string} bundleId - identifier for the bundle
|
|
10
|
+
* @param {string} bundleId - identifier for the bundle id.
|
|
11
11
|
* @param {string | null} zipUrl - zip file URL. If null, it means rolling back to the built-in bundle
|
|
12
12
|
* @returns {Promise<boolean>} Resolves with true if download was successful, otherwise rejects with an error.
|
|
13
13
|
*/
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
bool hasListeners;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
11
|
+
return YES;
|
|
12
|
+
}
|
|
10
13
|
|
|
11
14
|
- (instancetype)init {
|
|
12
15
|
self = [super init];
|
|
@@ -20,7 +23,6 @@ RCT_EXPORT_MODULE();
|
|
|
20
23
|
|
|
21
24
|
#pragma mark - Bundle URL Management
|
|
22
25
|
|
|
23
|
-
|
|
24
26
|
- (NSString *)getAppVersion {
|
|
25
27
|
NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
|
26
28
|
return appVersion;
|
|
@@ -47,7 +49,6 @@ RCT_EXPORT_MODULE();
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
+ (NSURL *)fallbackURL {
|
|
50
|
-
// This Support React Native 0.72.6
|
|
51
52
|
#if DEBUG
|
|
52
53
|
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
|
|
53
54
|
#else
|
|
@@ -61,17 +62,6 @@ RCT_EXPORT_MODULE();
|
|
|
61
62
|
|
|
62
63
|
#pragma mark - Utility Methods
|
|
63
64
|
|
|
64
|
-
- (NSString *)convertFileSystemPathFromBasePath:(NSString *)basePath {
|
|
65
|
-
return [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:basePath];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
- (NSString *)stripPrefixFromPath:(NSString *)prefix path:(NSString *)path {
|
|
69
|
-
if ([path hasPrefix:[NSString stringWithFormat:@"/%@/", prefix]]) {
|
|
70
|
-
return [path stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"/%@/", prefix] withString:@""];
|
|
71
|
-
}
|
|
72
|
-
return path;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
65
|
- (BOOL)extractZipFileAtPath:(NSString *)filePath toDestination:(NSString *)destinationPath {
|
|
76
66
|
NSError *error = nil;
|
|
77
67
|
BOOL success = [SSZipArchive unzipFileAtPath:filePath toDestination:destinationPath overwrite:YES password:nil error:&error];
|
|
@@ -81,6 +71,50 @@ RCT_EXPORT_MODULE();
|
|
|
81
71
|
return success;
|
|
82
72
|
}
|
|
83
73
|
|
|
74
|
+
#pragma mark - Cleanup Old Bundles
|
|
75
|
+
|
|
76
|
+
- (void)cleanupOldBundlesAtDirectory:(NSString *)bundleStoreDir {
|
|
77
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
78
|
+
NSError *error = nil;
|
|
79
|
+
NSArray *contents = [fileManager contentsOfDirectoryAtPath:bundleStoreDir error:&error];
|
|
80
|
+
if (error) {
|
|
81
|
+
NSLog(@"Failed to list bundle store directory: %@", error);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
NSMutableArray *bundleDirs = [NSMutableArray array];
|
|
86
|
+
for (NSString *item in contents) {
|
|
87
|
+
NSString *fullPath = [bundleStoreDir stringByAppendingPathComponent:item];
|
|
88
|
+
BOOL isDir = NO;
|
|
89
|
+
if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) {
|
|
90
|
+
[bundleDirs addObject:fullPath];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Sort in descending order by modification time (keep latest 2)
|
|
95
|
+
[bundleDirs sortUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
|
|
96
|
+
NSDictionary *attr1 = [fileManager attributesOfItemAtPath:path1 error:nil];
|
|
97
|
+
NSDictionary *attr2 = [fileManager attributesOfItemAtPath:path2 error:nil];
|
|
98
|
+
NSDate *date1 = attr1[NSFileModificationDate] ?: [NSDate dateWithTimeIntervalSince1970:0];
|
|
99
|
+
NSDate *date2 = attr2[NSFileModificationDate] ?: [NSDate dateWithTimeIntervalSince1970:0];
|
|
100
|
+
return [date2 compare:date1];
|
|
101
|
+
}];
|
|
102
|
+
|
|
103
|
+
if (bundleDirs.count > 2) {
|
|
104
|
+
NSArray *oldBundles = [bundleDirs subarrayWithRange:NSMakeRange(2, bundleDirs.count - 2)];
|
|
105
|
+
for (NSString *oldBundle in oldBundles) {
|
|
106
|
+
NSError *delError = nil;
|
|
107
|
+
if ([fileManager removeItemAtPath:oldBundle error:&delError]) {
|
|
108
|
+
NSLog(@"Removed old bundle: %@", oldBundle);
|
|
109
|
+
} else {
|
|
110
|
+
NSLog(@"Failed to remove old bundle %@: %@", oldBundle, delError);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#pragma mark - Update Bundle Method
|
|
117
|
+
|
|
84
118
|
- (void)updateBundle:(NSString *)bundleId zipUrl:(NSURL *)zipUrl completion:(void (^)(BOOL success))completion {
|
|
85
119
|
if (!zipUrl) {
|
|
86
120
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
@@ -89,105 +123,182 @@ RCT_EXPORT_MODULE();
|
|
|
89
123
|
});
|
|
90
124
|
return;
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
NSString *
|
|
95
|
-
|
|
126
|
+
|
|
127
|
+
// Set document directory path and bundle store path
|
|
128
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
129
|
+
NSString *bundleStoreDir = [documentsPath stringByAppendingPathComponent:@"bundle-store"];
|
|
130
|
+
|
|
131
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
132
|
+
if (![fileManager fileExistsAtPath:bundleStoreDir]) {
|
|
133
|
+
[fileManager createDirectoryAtPath:bundleStoreDir withIntermediateDirectories:YES attributes:nil error:nil];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Final bundle path (bundle-store/<bundleId>)
|
|
137
|
+
NSString *finalBundleDir = [bundleStoreDir stringByAppendingPathComponent:bundleId];
|
|
138
|
+
|
|
139
|
+
// Check if cached bundle exists
|
|
140
|
+
if ([fileManager fileExistsAtPath:finalBundleDir]) {
|
|
141
|
+
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:finalBundleDir];
|
|
142
|
+
NSString *foundBundle = nil;
|
|
143
|
+
for (NSString *file in enumerator) {
|
|
144
|
+
if ([file isEqualToString:@"index.ios.bundle"]) {
|
|
145
|
+
foundBundle = file;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (foundBundle) {
|
|
150
|
+
// Update modification time of final bundle
|
|
151
|
+
NSDictionary *attributes = @{NSFileModificationDate: [NSDate date]};
|
|
152
|
+
[fileManager setAttributes:attributes ofItemAtPath:finalBundleDir error:nil];
|
|
153
|
+
NSString *bundlePath = [finalBundleDir stringByAppendingPathComponent:foundBundle];
|
|
154
|
+
NSLog(@"Using cached bundle at path: %@", bundlePath);
|
|
155
|
+
[self setBundleURL:bundlePath];
|
|
156
|
+
[self cleanupOldBundlesAtDirectory:bundleStoreDir];
|
|
157
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
158
|
+
if (completion) completion(YES);
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
} else {
|
|
162
|
+
[fileManager removeItemAtPath:finalBundleDir error:nil];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Set up temporary folder (for download and extraction)
|
|
167
|
+
NSString *tempDir = [documentsPath stringByAppendingPathComponent:@"bundle-temp"];
|
|
168
|
+
if ([fileManager fileExistsAtPath:tempDir]) {
|
|
169
|
+
[fileManager removeItemAtPath:tempDir error:nil];
|
|
170
|
+
}
|
|
171
|
+
[fileManager createDirectoryAtPath:tempDir withIntermediateDirectories:YES attributes:nil error:nil];
|
|
172
|
+
|
|
173
|
+
NSString *tempZipFile = [tempDir stringByAppendingPathComponent:@"build.zip"];
|
|
174
|
+
NSString *extractedDir = [tempDir stringByAppendingPathComponent:@"extracted"];
|
|
175
|
+
[fileManager createDirectoryAtPath:extractedDir withIntermediateDirectories:YES attributes:nil error:nil];
|
|
176
|
+
|
|
96
177
|
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
97
178
|
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
|
|
98
|
-
|
|
99
|
-
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:zipUrl
|
|
100
|
-
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
|
|
179
|
+
|
|
180
|
+
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:zipUrl completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
|
|
101
181
|
if (error) {
|
|
102
182
|
NSLog(@"Failed to download data from URL: %@, error: %@", zipUrl, error);
|
|
103
183
|
if (completion) completion(NO);
|
|
104
184
|
return;
|
|
105
185
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Ensure directory exists
|
|
111
|
-
if (![fileManager createDirectoryAtPath:[path stringByDeletingLastPathComponent]
|
|
112
|
-
withIntermediateDirectories:YES
|
|
113
|
-
attributes:nil
|
|
114
|
-
error:&folderError]) {
|
|
115
|
-
NSLog(@"Failed to create folder: %@", folderError);
|
|
116
|
-
if (completion) completion(NO);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Check if file already exists and remove it
|
|
121
|
-
if ([fileManager fileExistsAtPath:path]) {
|
|
122
|
-
NSError *removeError;
|
|
123
|
-
if (![fileManager removeItemAtPath:path error:&removeError]) {
|
|
124
|
-
NSLog(@"Failed to remove existing file: %@", removeError);
|
|
125
|
-
if (completion) completion(NO);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
186
|
+
|
|
187
|
+
// Save temporary zip file
|
|
188
|
+
if ([fileManager fileExistsAtPath:tempZipFile]) {
|
|
189
|
+
[fileManager removeItemAtPath:tempZipFile error:nil];
|
|
128
190
|
}
|
|
129
|
-
|
|
130
|
-
NSError *moveError;
|
|
131
|
-
if (![fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:
|
|
132
|
-
NSLog(@"Failed to save
|
|
191
|
+
|
|
192
|
+
NSError *moveError = nil;
|
|
193
|
+
if (![fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:tempZipFile] error:&moveError]) {
|
|
194
|
+
NSLog(@"Failed to save downloaded file: %@", moveError);
|
|
133
195
|
if (completion) completion(NO);
|
|
134
196
|
return;
|
|
135
197
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (![self extractZipFileAtPath:
|
|
198
|
+
|
|
199
|
+
// Extract zip
|
|
200
|
+
if (![self extractZipFileAtPath:tempZipFile toDestination:extractedDir]) {
|
|
139
201
|
NSLog(@"Failed to extract zip file.");
|
|
140
202
|
if (completion) completion(NO);
|
|
141
203
|
return;
|
|
142
204
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
205
|
+
|
|
206
|
+
// Search for index.ios.bundle in extracted folder
|
|
207
|
+
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:extractedDir];
|
|
208
|
+
NSString *foundBundle = nil;
|
|
146
209
|
for (NSString *file in enumerator) {
|
|
147
210
|
if ([file isEqualToString:@"index.ios.bundle"]) {
|
|
148
|
-
|
|
211
|
+
foundBundle = file;
|
|
149
212
|
break;
|
|
150
213
|
}
|
|
151
214
|
}
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
NSLog(@"Setting bundle URL: %@", bundlePath);
|
|
156
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
157
|
-
[self setBundleURL:bundlePath];
|
|
158
|
-
if (completion) completion(YES);
|
|
159
|
-
});
|
|
160
|
-
} else {
|
|
161
|
-
NSLog(@"index.ios.bundle not found.");
|
|
215
|
+
|
|
216
|
+
if (!foundBundle) {
|
|
217
|
+
NSLog(@"index.ios.bundle not found in extracted files.");
|
|
162
218
|
if (completion) completion(NO);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Move extracted folder to final bundle folder
|
|
223
|
+
if ([fileManager fileExistsAtPath:finalBundleDir]) {
|
|
224
|
+
[fileManager removeItemAtPath:finalBundleDir error:nil];
|
|
225
|
+
}
|
|
226
|
+
NSError *moveFinalError = nil;
|
|
227
|
+
BOOL moved = [fileManager moveItemAtPath:extractedDir toPath:finalBundleDir error:&moveFinalError];
|
|
228
|
+
if (!moved) {
|
|
229
|
+
// Try copy and delete if move fails
|
|
230
|
+
BOOL copied = [fileManager copyItemAtPath:extractedDir toPath:finalBundleDir error:&moveFinalError];
|
|
231
|
+
if (copied) {
|
|
232
|
+
[fileManager removeItemAtPath:extractedDir error:nil];
|
|
233
|
+
} else {
|
|
234
|
+
NSLog(@"Failed to move or copy extracted bundle: %@", moveFinalError);
|
|
235
|
+
if (completion) completion(NO);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
163
238
|
}
|
|
239
|
+
|
|
240
|
+
// Recheck index.ios.bundle in final folder
|
|
241
|
+
NSDirectoryEnumerator *finalEnum = [fileManager enumeratorAtPath:finalBundleDir];
|
|
242
|
+
NSString *finalFoundBundle = nil;
|
|
243
|
+
for (NSString *file in finalEnum) {
|
|
244
|
+
if ([file isEqualToString:@"index.ios.bundle"]) {
|
|
245
|
+
finalFoundBundle = file;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!finalFoundBundle) {
|
|
251
|
+
NSLog(@"index.ios.bundle not found in final directory.");
|
|
252
|
+
if (completion) completion(NO);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Update modification time of final bundle
|
|
257
|
+
NSDictionary *attributes = @{NSFileModificationDate: [NSDate date]};
|
|
258
|
+
[fileManager setAttributes:attributes ofItemAtPath:finalBundleDir error:nil];
|
|
259
|
+
|
|
260
|
+
NSString *bundlePath = [finalBundleDir stringByAppendingPathComponent:finalFoundBundle];
|
|
261
|
+
NSLog(@"Setting bundle URL: %@", bundlePath);
|
|
262
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
263
|
+
[self setBundleURL:bundlePath];
|
|
264
|
+
[self cleanupOldBundlesAtDirectory:bundleStoreDir];
|
|
265
|
+
[fileManager removeItemAtPath:tempDir error:nil];
|
|
266
|
+
if (completion) completion(YES);
|
|
267
|
+
});
|
|
164
268
|
}];
|
|
165
|
-
|
|
166
269
|
|
|
167
|
-
//
|
|
168
|
-
[downloadTask addObserver:self
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
[downloadTask addObserver:self
|
|
173
|
-
forKeyPath:@"countOfBytesExpectedToReceive"
|
|
174
|
-
options:NSKeyValueObservingOptionNew
|
|
175
|
-
context:nil];
|
|
176
|
-
|
|
177
|
-
__block HotUpdater *weakSelf = self;
|
|
270
|
+
// Register KVO for progress updates
|
|
271
|
+
[downloadTask addObserver:self forKeyPath:@"countOfBytesReceived" options:NSKeyValueObservingOptionNew context:nil];
|
|
272
|
+
[downloadTask addObserver:self forKeyPath:@"countOfBytesExpectedToReceive" options:NSKeyValueObservingOptionNew context:nil];
|
|
273
|
+
|
|
274
|
+
__weak HotUpdater *weakSelf = self;
|
|
178
275
|
[[NSNotificationCenter defaultCenter] addObserverForName:@"NSURLSessionDownloadTaskDidFinishDownloading"
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
276
|
+
object:downloadTask
|
|
277
|
+
queue:[NSOperationQueue mainQueue]
|
|
278
|
+
usingBlock:^(NSNotification * _Nonnull note) {
|
|
182
279
|
[weakSelf removeObserversForTask:downloadTask];
|
|
183
280
|
}];
|
|
281
|
+
|
|
184
282
|
[downloadTask resume];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#pragma mark - Folder Deletion Utility
|
|
185
286
|
|
|
287
|
+
- (void)deleteFolderIfExists:(NSString *)path {
|
|
288
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
289
|
+
if ([fileManager fileExistsAtPath:path]) {
|
|
290
|
+
NSError *error;
|
|
291
|
+
[fileManager removeItemAtPath:path error:&error];
|
|
292
|
+
if (error) {
|
|
293
|
+
NSLog(@"Failed to delete existing folder: %@", error);
|
|
294
|
+
} else {
|
|
295
|
+
NSLog(@"Successfully deleted existing folder: %@", path);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
186
298
|
}
|
|
187
299
|
|
|
188
300
|
#pragma mark - Progress Updates
|
|
189
301
|
|
|
190
|
-
|
|
191
302
|
- (void)removeObserversForTask:(NSURLSessionDownloadTask *)task {
|
|
192
303
|
@try {
|
|
193
304
|
if ([task observationInfo]) {
|
|
@@ -200,53 +311,41 @@ RCT_EXPORT_MODULE();
|
|
|
200
311
|
}
|
|
201
312
|
}
|
|
202
313
|
|
|
203
|
-
|
|
204
314
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
205
315
|
ofObject:(id)object
|
|
206
316
|
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
|
|
207
317
|
context:(void *)context {
|
|
208
318
|
if ([keyPath isEqualToString:@"countOfBytesReceived"] || [keyPath isEqualToString:@"countOfBytesExpectedToReceive"]) {
|
|
209
319
|
NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)object;
|
|
210
|
-
|
|
211
320
|
if (task.countOfBytesExpectedToReceive > 0) {
|
|
212
321
|
double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive;
|
|
213
|
-
|
|
214
|
-
// Get current timestamp
|
|
215
|
-
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000; // Convert to milliseconds
|
|
216
|
-
|
|
217
|
-
// Send event only if 100ms has passed OR progress is 100%
|
|
322
|
+
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000; // In milliseconds
|
|
218
323
|
if ((currentTime - self.lastUpdateTime) >= 100 || progress >= 1.0) {
|
|
219
|
-
self.lastUpdateTime = currentTime;
|
|
220
|
-
|
|
221
|
-
// Send progress to React Native
|
|
324
|
+
self.lastUpdateTime = currentTime;
|
|
222
325
|
[self sendEventWithName:@"onProgress" body:@{@"progress": @(progress)}];
|
|
223
326
|
}
|
|
224
327
|
}
|
|
225
328
|
}
|
|
226
329
|
}
|
|
227
330
|
|
|
228
|
-
|
|
229
331
|
#pragma mark - React Native Events
|
|
332
|
+
|
|
230
333
|
- (NSArray<NSString *> *)supportedEvents {
|
|
231
334
|
return @[@"onProgress"];
|
|
232
335
|
}
|
|
233
336
|
|
|
234
|
-
- (void)startObserving
|
|
235
|
-
{
|
|
337
|
+
- (void)startObserving {
|
|
236
338
|
hasListeners = YES;
|
|
237
339
|
}
|
|
238
340
|
|
|
239
|
-
- (void)stopObserving
|
|
240
|
-
{
|
|
341
|
+
- (void)stopObserving {
|
|
241
342
|
hasListeners = NO;
|
|
242
343
|
}
|
|
243
344
|
|
|
244
|
-
|
|
245
345
|
- (void)sendEventWithName:(NSString * _Nonnull)name result:(NSDictionary *)result {
|
|
246
346
|
[self sendEventWithName:name body:result];
|
|
247
347
|
}
|
|
248
348
|
|
|
249
|
-
|
|
250
349
|
#pragma mark - React Native Exports
|
|
251
350
|
|
|
252
351
|
RCT_EXPORT_METHOD(reload) {
|
|
@@ -257,8 +356,7 @@ RCT_EXPORT_METHOD(reload) {
|
|
|
257
356
|
});
|
|
258
357
|
}
|
|
259
358
|
|
|
260
|
-
RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
|
|
261
|
-
reject:(RCTPromiseRejectBlock)reject) {
|
|
359
|
+
RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
262
360
|
NSString *version = [self getAppVersion];
|
|
263
361
|
resolve(version ?: [NSNull null]);
|
|
264
362
|
}
|
|
@@ -268,7 +366,6 @@ RCT_EXPORT_METHOD(updateBundle:(NSString *)bundleId zipUrl:(NSString *)zipUrlStr
|
|
|
268
366
|
if (![zipUrlString isEqualToString:@""]) {
|
|
269
367
|
zipUrl = [NSURL URLWithString:zipUrlString];
|
|
270
368
|
}
|
|
271
|
-
|
|
272
369
|
[self updateBundle:bundleId zipUrl:zipUrl completion:^(BOOL success) {
|
|
273
370
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
274
371
|
resolve(@[@(success)]);
|
|
@@ -276,14 +373,10 @@ RCT_EXPORT_METHOD(updateBundle:(NSString *)bundleId zipUrl:(NSString *)zipUrlStr
|
|
|
276
373
|
}];
|
|
277
374
|
}
|
|
278
375
|
|
|
279
|
-
|
|
280
|
-
// Don't compile this code when we build for the old architecture.
|
|
281
376
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
282
|
-
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
283
|
-
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
284
|
-
{
|
|
377
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
285
378
|
return std::make_shared<facebook::react::NativeHotUpdaterSpecJSI>(params);
|
|
286
379
|
}
|
|
287
380
|
#endif
|
|
288
381
|
|
|
289
|
-
@end
|
|
382
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"react-native-builder-bob": "^0.33.1"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@hot-updater/js": "0.
|
|
82
|
-
"@hot-updater/core": "0.
|
|
81
|
+
"@hot-updater/js": "0.12.0",
|
|
82
|
+
"@hot-updater/core": "0.12.0"
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "rslib build",
|
package/src/checkUpdate.ts
CHANGED
|
@@ -12,9 +12,6 @@ export interface CheckForUpdateConfig {
|
|
|
12
12
|
|
|
13
13
|
export async function checkForUpdate(config: CheckForUpdateConfig) {
|
|
14
14
|
if (__DEV__) {
|
|
15
|
-
console.warn(
|
|
16
|
-
"[HotUpdater] __DEV__ is true, HotUpdater is only supported in production",
|
|
17
|
-
);
|
|
18
15
|
return null;
|
|
19
16
|
}
|
|
20
17
|
|
package/src/native.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const addListener = <T extends keyof HotUpdaterEvent>(
|
|
|
52
52
|
/**
|
|
53
53
|
* Downloads files from given URLs.
|
|
54
54
|
*
|
|
55
|
-
* @param {string} bundleId - identifier for the bundle
|
|
55
|
+
* @param {string} bundleId - identifier for the bundle id.
|
|
56
56
|
* @param {string | null} zipUrl - zip file URL. If null, it means rolling back to the built-in bundle
|
|
57
57
|
* @returns {Promise<boolean>} Resolves with true if download was successful, otherwise rejects with an error.
|
|
58
58
|
*/
|
|
@@ -74,7 +74,9 @@ export const getAppVersion = (): Promise<string | null> => {
|
|
|
74
74
|
* Reloads the app.
|
|
75
75
|
*/
|
|
76
76
|
export const reload = () => {
|
|
77
|
-
|
|
77
|
+
requestAnimationFrame(() => {
|
|
78
|
+
HotUpdaterNative.reload();
|
|
79
|
+
});
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|