@hot-updater/react-native 0.10.1 → 0.11.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.
@@ -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,12 +126,19 @@ class HotUpdater : ReactPackage {
146
126
  return true
147
127
  }
148
128
 
149
- val downloadUrl = URL(zipUrl)
150
- val basePath = stripPrefixFromPath(bundleId, downloadUrl.path)
151
- val path = convertFileSystemPathFromBasePath(context, basePath)
129
+ val baseDir = context.getExternalFilesDir(null)
130
+ val bundleStoreDir = File(baseDir, "bundle-store")
131
+ val zipFilePath = File(bundleStoreDir, "build.zip")
132
+
133
+ // Delete existing folder logic
134
+ if (bundleStoreDir.exists()) {
135
+ bundleStoreDir.deleteRecursively()
136
+ }
137
+ bundleStoreDir.mkdirs()
152
138
 
153
139
  val isSuccess =
154
140
  withContext(Dispatchers.IO) {
141
+ val downloadUrl = URL(zipUrl)
155
142
  val conn =
156
143
  try {
157
144
  downloadUrl.openConnection() as HttpURLConnection
@@ -168,11 +155,9 @@ class HotUpdater : ReactPackage {
168
155
  return@withContext false
169
156
  }
170
157
 
171
- val file = File(path)
172
- file.parentFile?.mkdirs()
173
-
158
+ // File download
174
159
  conn.inputStream.use { input ->
175
- file.outputStream().use { output ->
160
+ zipFilePath.outputStream().use { output ->
176
161
  val buffer = ByteArray(8 * 1024)
177
162
  var bytesRead: Int
178
163
  var totalRead = 0L
@@ -182,13 +167,12 @@ class HotUpdater : ReactPackage {
182
167
  output.write(buffer, 0, bytesRead)
183
168
  totalRead += bytesRead
184
169
  val currentTime = System.currentTimeMillis()
185
- if (currentTime - lastProgressTime >= 100) { // Check every 100ms
170
+ if (currentTime - lastProgressTime >= 100) {
186
171
  val progress = totalRead.toDouble() / totalSize
187
172
  progressCallback.invoke(progress)
188
173
  lastProgressTime = currentTime
189
174
  }
190
175
  }
191
- // Send final progress (100%) after download completes
192
176
  progressCallback.invoke(1.0)
193
177
  }
194
178
  }
@@ -199,18 +183,17 @@ class HotUpdater : ReactPackage {
199
183
  conn.disconnect()
200
184
  }
201
185
 
202
- val extractedPath = File(path).parentFile?.path ?: return@withContext false
203
-
204
- if (!extractZipFileAtPath(path, extractedPath)) {
186
+ // Extract zip
187
+ if (!extractZipFileAtPath(zipFilePath.absolutePath, bundleStoreDir.absolutePath)) {
205
188
  Log.d("HotUpdater", "Failed to extract zip file.")
206
189
  return@withContext false
207
190
  }
208
191
 
209
- val extractedDirectory = File(extractedPath)
210
- val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }
192
+ // Find index.android.bundle
193
+ val indexFile = bundleStoreDir.walk().find { it.name == "index.android.bundle" }
211
194
 
212
195
  if (indexFile != null) {
213
- val bundlePath = indexFile.path
196
+ val bundlePath = indexFile.absolutePath
214
197
  Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
215
198
  setBundleURL(context, bundlePath)
216
199
  } else {
@@ -221,6 +204,7 @@ class HotUpdater : ReactPackage {
221
204
  Log.d("HotUpdater", "Downloaded and extracted file successfully.")
222
205
  true
223
206
  }
207
+
224
208
  return isSuccess
225
209
  }
226
210
  }
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
- HotUpdaterNative.reload();
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
- HotUpdaterNative.reload();
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"
@@ -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,7 @@ RCT_EXPORT_MODULE();
47
49
  }
48
50
 
49
51
  + (NSURL *)fallbackURL {
50
- // This Support React Native 0.72.6
52
+ // This supports React Native 0.72.6
51
53
  #if DEBUG
52
54
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
53
55
  #else
@@ -61,17 +63,6 @@ RCT_EXPORT_MODULE();
61
63
 
62
64
  #pragma mark - Utility Methods
63
65
 
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
66
  - (BOOL)extractZipFileAtPath:(NSString *)filePath toDestination:(NSString *)destinationPath {
76
67
  NSError *error = nil;
77
68
  BOOL success = [SSZipArchive unzipFileAtPath:filePath toDestination:destinationPath overwrite:YES password:nil error:&error];
@@ -81,6 +72,8 @@ RCT_EXPORT_MODULE();
81
72
  return success;
82
73
  }
83
74
 
75
+ #pragma mark - Update Bundle Method
76
+
84
77
  - (void)updateBundle:(NSString *)bundleId zipUrl:(NSURL *)zipUrl completion:(void (^)(BOOL success))completion {
85
78
  if (!zipUrl) {
86
79
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -89,13 +82,20 @@ RCT_EXPORT_MODULE();
89
82
  });
90
83
  return;
91
84
  }
92
-
93
- NSString *basePath = [self stripPrefixFromPath:bundleId path:[zipUrl path]];
94
- NSString *path = [self convertFileSystemPathFromBasePath:basePath];
95
-
85
+
86
+ // Set app-specific path (dynamically using NSSearchPathForDirectoriesInDomains)
87
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
88
+ NSString *bundleStoreDir = [documentsPath stringByAppendingPathComponent:@"bundle-store"];
89
+ NSString *zipFilePath = [bundleStoreDir stringByAppendingPathComponent:@"build.zip"];
90
+
91
+ // Delete existing folder
92
+ [self deleteFolderIfExists:bundleStoreDir];
93
+ // Create download folder
94
+ [[NSFileManager defaultManager] createDirectoryAtPath:bundleStoreDir withIntermediateDirectories:YES attributes:nil error:nil];
95
+
96
96
  NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
97
97
  NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
98
-
98
+
99
99
  NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:zipUrl
100
100
  completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
101
101
  if (error) {
@@ -103,45 +103,31 @@ RCT_EXPORT_MODULE();
103
103
  if (completion) completion(NO);
104
104
  return;
105
105
  }
106
-
106
+
107
107
  NSFileManager *fileManager = [NSFileManager defaultManager];
108
- NSError *folderError;
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
- }
108
+
109
+ // Remove existing file
110
+ if ([fileManager fileExistsAtPath:zipFilePath]) {
111
+ [fileManager removeItemAtPath:zipFilePath error:nil];
128
112
  }
129
-
113
+
114
+ // Move downloaded file
130
115
  NSError *moveError;
131
- if (![fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:path] error:&moveError]) {
116
+ if (![fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:zipFilePath] error:&moveError]) {
132
117
  NSLog(@"Failed to save data: %@", moveError);
133
118
  if (completion) completion(NO);
134
119
  return;
135
120
  }
136
-
137
- NSString *extractedPath = [path stringByDeletingLastPathComponent];
138
- if (![self extractZipFileAtPath:path toDestination:extractedPath]) {
121
+
122
+ // Extract zip
123
+ if (![self extractZipFileAtPath:zipFilePath toDestination:bundleStoreDir]) {
139
124
  NSLog(@"Failed to extract zip file.");
140
125
  if (completion) completion(NO);
141
126
  return;
142
127
  }
143
-
144
- NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:extractedPath];
128
+
129
+ // Search for bundle file (index.ios.bundle)
130
+ NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:bundleStoreDir];
145
131
  NSString *filename = nil;
146
132
  for (NSString *file in enumerator) {
147
133
  if ([file isEqualToString:@"index.ios.bundle"]) {
@@ -149,9 +135,9 @@ RCT_EXPORT_MODULE();
149
135
  break;
150
136
  }
151
137
  }
152
-
138
+
153
139
  if (filename) {
154
- NSString *bundlePath = [extractedPath stringByAppendingPathComponent:filename];
140
+ NSString *bundlePath = [bundleStoreDir stringByAppendingPathComponent:filename];
155
141
  NSLog(@"Setting bundle URL: %@", bundlePath);
156
142
  dispatch_async(dispatch_get_main_queue(), ^{
157
143
  [self setBundleURL:bundlePath];
@@ -163,7 +149,7 @@ RCT_EXPORT_MODULE();
163
149
  }
164
150
  }];
165
151
 
166
-
152
+
167
153
  // Add observer for progress updates
168
154
  [downloadTask addObserver:self
169
155
  forKeyPath:@"countOfBytesReceived"
@@ -181,13 +167,27 @@ RCT_EXPORT_MODULE();
181
167
  usingBlock:^(NSNotification * _Nonnull note) {
182
168
  [weakSelf removeObserversForTask:downloadTask];
183
169
  }];
170
+
184
171
  [downloadTask resume];
172
+ }
185
173
 
174
+ #pragma mark - Folder Deletion Utility
175
+
176
+ - (void)deleteFolderIfExists:(NSString *)path {
177
+ NSFileManager *fileManager = [NSFileManager defaultManager];
178
+ if ([fileManager fileExistsAtPath:path]) {
179
+ NSError *error;
180
+ [fileManager removeItemAtPath:path error:&error];
181
+ if (error) {
182
+ NSLog(@"Failed to delete existing folder: %@", error);
183
+ } else {
184
+ NSLog(@"Successfully deleted existing folder: %@", path);
185
+ }
186
+ }
186
187
  }
187
188
 
188
189
  #pragma mark - Progress Updates
189
190
 
190
-
191
191
  - (void)removeObserversForTask:(NSURLSessionDownloadTask *)task {
192
192
  @try {
193
193
  if ([task observationInfo]) {
@@ -200,53 +200,41 @@ RCT_EXPORT_MODULE();
200
200
  }
201
201
  }
202
202
 
203
-
204
203
  - (void)observeValueForKeyPath:(NSString *)keyPath
205
204
  ofObject:(id)object
206
205
  change:(NSDictionary<NSKeyValueChangeKey, id> *)change
207
206
  context:(void *)context {
208
207
  if ([keyPath isEqualToString:@"countOfBytesReceived"] || [keyPath isEqualToString:@"countOfBytesExpectedToReceive"]) {
209
208
  NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)object;
210
-
211
209
  if (task.countOfBytesExpectedToReceive > 0) {
212
210
  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%
211
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000; // In milliseconds
218
212
  if ((currentTime - self.lastUpdateTime) >= 100 || progress >= 1.0) {
219
- self.lastUpdateTime = currentTime; // Update last event timestamp
220
-
221
- // Send progress to React Native
213
+ self.lastUpdateTime = currentTime;
222
214
  [self sendEventWithName:@"onProgress" body:@{@"progress": @(progress)}];
223
215
  }
224
216
  }
225
217
  }
226
218
  }
227
219
 
228
-
229
220
  #pragma mark - React Native Events
221
+
230
222
  - (NSArray<NSString *> *)supportedEvents {
231
223
  return @[@"onProgress"];
232
224
  }
233
225
 
234
- - (void)startObserving
235
- {
226
+ - (void)startObserving {
236
227
  hasListeners = YES;
237
228
  }
238
229
 
239
- - (void)stopObserving
240
- {
230
+ - (void)stopObserving {
241
231
  hasListeners = NO;
242
232
  }
243
233
 
244
-
245
234
  - (void)sendEventWithName:(NSString * _Nonnull)name result:(NSDictionary *)result {
246
235
  [self sendEventWithName:name body:result];
247
236
  }
248
237
 
249
-
250
238
  #pragma mark - React Native Exports
251
239
 
252
240
  RCT_EXPORT_METHOD(reload) {
@@ -257,8 +245,7 @@ RCT_EXPORT_METHOD(reload) {
257
245
  });
258
246
  }
259
247
 
260
- RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
261
- reject:(RCTPromiseRejectBlock)reject) {
248
+ RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
262
249
  NSString *version = [self getAppVersion];
263
250
  resolve(version ?: [NSNull null]);
264
251
  }
@@ -268,7 +255,6 @@ RCT_EXPORT_METHOD(updateBundle:(NSString *)bundleId zipUrl:(NSString *)zipUrlStr
268
255
  if (![zipUrlString isEqualToString:@""]) {
269
256
  zipUrl = [NSURL URLWithString:zipUrlString];
270
257
  }
271
-
272
258
  [self updateBundle:bundleId zipUrl:zipUrl completion:^(BOOL success) {
273
259
  dispatch_async(dispatch_get_main_queue(), ^{
274
260
  resolve(@[@(success)]);
@@ -276,14 +262,10 @@ RCT_EXPORT_METHOD(updateBundle:(NSString *)bundleId zipUrl:(NSString *)zipUrlStr
276
262
  }];
277
263
  }
278
264
 
279
-
280
- // Don't compile this code when we build for the old architecture.
281
265
  #ifdef RCT_NEW_ARCH_ENABLED
282
- - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
283
- (const facebook::react::ObjCTurboModule::InitParams &)params
284
- {
266
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
285
267
  return std::make_shared<facebook::react::NativeHotUpdaterSpecJSI>(params);
286
268
  }
287
269
  #endif
288
270
 
289
- @end
271
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.10.1",
3
+ "version": "0.11.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.10.1",
82
- "@hot-updater/core": "0.10.1"
81
+ "@hot-updater/js": "0.11.0",
82
+ "@hot-updater/core": "0.11.0"
83
83
  },
84
84
  "scripts": {
85
85
  "build": "rslib build",
@@ -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
@@ -74,7 +74,9 @@ export const getAppVersion = (): Promise<string | null> => {
74
74
  * Reloads the app.
75
75
  */
76
76
  export const reload = () => {
77
- HotUpdaterNative.reload();
77
+ requestAnimationFrame(() => {
78
+ HotUpdaterNative.reload();
79
+ });
78
80
  };
79
81
 
80
82
  /**