cordova-plugin-hot-updates 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,18 @@
1
- # Cordova Hot Updates Plugin v2.2.1
1
+ # Cordova Hot Updates Plugin v2.2.2
2
2
 
3
3
  Frontend-controlled manual hot updates for Cordova iOS applications using WebView Reload approach.
4
4
 
5
+ ## Changelog
6
+
7
+ ### v2.2.2 (2024-11-28)
8
+ - **Fixed**: Failed download no longer corrupts previously downloaded update
9
+ - **Fixed**: Infinite loop when pending update installation fails (flags now cleared)
10
+ - **Fixed**: UserDefaults now saved only after successful download (not before)
11
+ - **Improved**: ZIP file validation with magic bytes check (PK\x03\x04) before extraction
12
+ - **Improved**: Cleaner logs without duplicates and full paths
13
+ - **Improved**: All errors now have consistent format with `ERROR:` prefix
14
+ - **Removed**: Redundant initialization code
15
+
5
16
  [![npm version](https://badge.fury.io/js/cordova-plugin-hot-updates.svg)](https://badge.fury.io/js/cordova-plugin-hot-updates)
6
17
  [![License](https://img.shields.io/badge/License-Custom%20Non--Commercial-blue.svg)](#license)
7
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-hot-updates",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "Frontend-controlled manual hot updates for Cordova iOS apps using WebView Reload approach. Manual updates only, JavaScript controls all decisions.",
5
5
  "main": "www/HotUpdates.js",
6
6
  "scripts": {
package/plugin.xml CHANGED
@@ -1,6 +1,6 @@
1
1
  <?xml version='1.0' encoding='utf-8'?>
2
2
  <plugin id="cordova-plugin-hot-updates"
3
- version="2.2.1"
3
+ version="2.2.2"
4
4
  xmlns="http://apache.org/cordova/ns/plugins/1.0"
5
5
  xmlns:android="http://schemas.android.com/apk/res/android">
6
6
 
@@ -12,8 +12,8 @@
12
12
  * - IgnoreList for tracking problematic versions
13
13
  * - Auto-install pending updates on next app launch
14
14
  *
15
- * @version 2.1.2
16
- * @date 2025-11-26
15
+ * @version 2.2.2
16
+ * @date 2025-11-28
17
17
  * @author Mustafin Vladimir
18
18
  * @copyright Copyright (c) 2025. All rights reserved.
19
19
  */
@@ -60,21 +60,14 @@ static BOOL hasPerformedInitialReload = NO;
60
60
  isDownloadingUpdate = NO;
61
61
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
62
62
 
63
- isUpdateReadyToInstall = NO;
64
- pendingUpdateURL = nil;
65
- pendingUpdateVersion = nil;
66
-
63
+ // Читаем состояние из UserDefaults
67
64
  pendingUpdateURL = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingUpdateURL];
68
65
  isUpdateReadyToInstall = [[NSUserDefaults standardUserDefaults] boolForKey:kPendingUpdateReady];
69
66
  if (isUpdateReadyToInstall) {
70
67
  pendingUpdateVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
71
- NSLog(@"[HotUpdates] Found pending update ready to install: %@", pendingUpdateVersion);
72
68
  }
73
69
 
74
- NSLog(@"[HotUpdates] Startup sequence initiated");
75
- NSLog(@"[HotUpdates] Bundle www path: %@", [[NSBundle mainBundle] pathForResource:kWWWDirName ofType:nil]);
76
- NSLog(@"[HotUpdates] Documents www path: %@", wwwPath);
77
- NSLog(@"[HotUpdates] Ignore list: %@", ignoreList);
70
+ NSLog(@"[HotUpdates] Initializing plugin...");
78
71
 
79
72
  [self checkAndInstallPendingUpdate];
80
73
  [self initializeWWWFolder];
@@ -85,15 +78,12 @@ static BOOL hasPerformedInitialReload = NO;
85
78
  NSString *canaryVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kCanaryVersion];
86
79
 
87
80
  if (!canaryVersion || ![canaryVersion isEqualToString:currentVersion]) {
88
- NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", currentVersion);
89
-
81
+ NSLog(@"[HotUpdates] Starting canary timer for version %@", currentVersion);
90
82
  [self startCanaryTimer];
91
- } else {
92
- NSLog(@"[HotUpdates] Canary already confirmed for version %@", currentVersion);
93
83
  }
94
84
  }
95
85
 
96
- NSLog(@"[HotUpdates] Plugin initialized.");
86
+ NSLog(@"[HotUpdates] Plugin initialized (v%@)", appBundleVersion);
97
87
  }
98
88
 
99
89
  - (void)loadConfiguration {
@@ -101,9 +91,6 @@ static BOOL hasPerformedInitialReload = NO;
101
91
  if (!appBundleVersion) {
102
92
  appBundleVersion = @"1.0.0";
103
93
  }
104
-
105
- NSLog(@"[HotUpdates] Configuration loaded:");
106
- NSLog(@" App bundle version: %@", appBundleVersion);
107
94
  }
108
95
 
109
96
  /*!
@@ -115,7 +102,7 @@ static BOOL hasPerformedInitialReload = NO;
115
102
  NSString *pendingVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
116
103
 
117
104
  if (hasPendingUpdate && pendingVersion) {
118
- NSLog(@"[HotUpdates] Installing pending update %@ to Documents/www (auto-install on launch)", pendingVersion);
105
+ NSLog(@"[HotUpdates] Auto-installing pending update: %@", pendingVersion);
119
106
 
120
107
  [self backupCurrentVersion];
121
108
 
@@ -140,9 +127,15 @@ static BOOL hasPerformedInitialReload = NO;
140
127
 
141
128
  [[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
142
129
 
143
- NSLog(@"[HotUpdates] Update %@ installed successfully (canary timer will start)", pendingVersion);
130
+ NSLog(@"[HotUpdates] Update %@ installed successfully", pendingVersion);
144
131
  } else {
145
- NSLog(@"[HotUpdates] Failed to install update: %@", copyError.localizedDescription);
132
+ NSLog(@"[HotUpdates] Failed to install pending update: %@", copyError.localizedDescription);
133
+ // Очищаем флаги чтобы не пытаться снова при следующем запуске
134
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kHasPending];
135
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPendingVersion];
136
+ [[NSUserDefaults standardUserDefaults] synchronize];
137
+ // Удаляем битое обновление
138
+ [[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
146
139
  }
147
140
  }
148
141
  }
@@ -156,7 +149,6 @@ static BOOL hasPerformedInitialReload = NO;
156
149
  - (void)switchToUpdatedContentWithReload {
157
150
  // Предотвращаем повторные перезагрузки при навигации между страницами
158
151
  if (hasPerformedInitialReload) {
159
- NSLog(@"[HotUpdates] Initial reload already performed, skipping");
160
152
  return;
161
153
  }
162
154
 
@@ -167,24 +159,19 @@ static BOOL hasPerformedInitialReload = NO;
167
159
  NSString *indexPath = [documentsWwwPath stringByAppendingPathComponent:@"index.html"];
168
160
 
169
161
  if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath]) {
170
- NSLog(@"[HotUpdates] Using WebView reload approach");
171
- NSLog(@"[HotUpdates] Found installed update version: %@", installedVersion);
162
+ NSLog(@"[HotUpdates] Loading installed version: %@", installedVersion);
172
163
 
173
164
  ((CDVViewController *)self.viewController).wwwFolderName = documentsWwwPath;
174
- NSLog(@"[HotUpdates] Changed wwwFolderName to: %@", documentsWwwPath);
175
-
176
165
  hasPerformedInitialReload = YES;
177
166
 
178
167
  // Очищаем кэш перед перезагрузкой, иначе может загрузиться старая версия
179
168
  [self clearWebViewCacheWithCompletion:^{
180
169
  [self reloadWebView];
181
- NSLog(@"[HotUpdates] WebView reloaded with updated content (version: %@)", installedVersion);
182
170
  }];
183
171
  } else {
184
- NSLog(@"[HotUpdates] Documents/www/index.html not found, keeping bundle www");
172
+ NSLog(@"[HotUpdates] WARNING: Documents/www/index.html not found, using bundle");
185
173
  }
186
174
  } else {
187
- NSLog(@"[HotUpdates] No installed updates, using bundle www");
188
175
  hasPerformedInitialReload = YES;
189
176
  }
190
177
  }
@@ -202,8 +189,6 @@ static BOOL hasPerformedInitialReload = NO;
202
189
  * @param completion Block called after cache is cleared (on main thread)
203
190
  */
204
191
  - (void)clearWebViewCacheWithCompletion:(void (^)(void))completion {
205
- NSLog(@"[HotUpdates] Clearing WebView cache");
206
-
207
192
  NSSet *websiteDataTypes = [NSSet setWithArray:@[
208
193
  WKWebsiteDataTypeDiskCache,
209
194
  WKWebsiteDataTypeMemoryCache,
@@ -215,7 +200,6 @@ static BOOL hasPerformedInitialReload = NO;
215
200
  [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes
216
201
  modifiedSince:dateFrom
217
202
  completionHandler:^{
218
- NSLog(@"[HotUpdates] WebView cache cleared");
219
203
  if (completion) {
220
204
  dispatch_async(dispatch_get_main_queue(), completion);
221
205
  }
@@ -231,26 +215,22 @@ static BOOL hasPerformedInitialReload = NO;
231
215
  NSURL *fileURL = [NSURL fileURLWithPath:indexPath];
232
216
  NSURL *allowReadAccessToURL = [NSURL fileURLWithPath:documentsWwwPath];
233
217
 
234
- NSLog(@"[HotUpdates] Loading WebView with new URL: %@", fileURL.absoluteString);
235
-
236
218
  id webViewEngine = cdvViewController.webViewEngine;
237
219
  if (webViewEngine && [webViewEngine respondsToSelector:@selector(engineWebView)]) {
238
220
  WKWebView *webView = [webViewEngine performSelector:@selector(engineWebView)];
239
221
 
240
222
  if (webView && [webView isKindOfClass:[WKWebView class]]) {
241
- // loadFileURL:allowingReadAccessToURL: правильно настраивает sandbox permissions для локальных файлов
242
223
  dispatch_async(dispatch_get_main_queue(), ^{
243
224
  [webView loadFileURL:fileURL allowingReadAccessToURL:allowReadAccessToURL];
244
- NSLog(@"[HotUpdates] WebView loadFileURL executed with sandbox permissions");
245
225
  });
246
226
  } else {
247
- NSLog(@"[HotUpdates] Could not access WKWebView for reload");
227
+ NSLog(@"[HotUpdates] ERROR: Could not access WKWebView for reload");
248
228
  }
249
229
  } else {
250
- NSLog(@"[HotUpdates] WebView engine not available for reload");
230
+ NSLog(@"[HotUpdates] ERROR: WebView engine not available for reload");
251
231
  }
252
232
  } else {
253
- NSLog(@"[HotUpdates] ViewController is not CDVViewController type");
233
+ NSLog(@"[HotUpdates] ERROR: ViewController is not CDVViewController type");
254
234
  }
255
235
  }
256
236
 
@@ -259,120 +239,126 @@ static BOOL hasPerformedInitialReload = NO;
259
239
  NSFileManager *fileManager = [NSFileManager defaultManager];
260
240
 
261
241
  if (![fileManager fileExistsAtPath:wwwPath]) {
262
- NSLog(@"[HotUpdates] WWW folder not found in Documents. Creating and copying from bundle...");
263
-
264
242
  NSString *bundleWWWPath = [[NSBundle mainBundle] pathForResource:@"www" ofType:nil];
265
243
  if (bundleWWWPath) {
266
244
  NSError *error;
267
245
  [fileManager copyItemAtPath:bundleWWWPath toPath:wwwPath error:&error];
268
246
  if (error) {
269
- NSLog(@"[HotUpdates] Error copying www folder: %@", error.localizedDescription);
247
+ NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", error.localizedDescription);
270
248
  } else {
271
- NSLog(@"[HotUpdates] WWW folder copied successfully to Documents");
249
+ NSLog(@"[HotUpdates] Initialized www folder from bundle");
272
250
  }
273
251
  } else {
274
- NSLog(@"[HotUpdates] Error: Bundle www folder not found");
252
+ NSLog(@"[HotUpdates] ERROR: Bundle www folder not found");
275
253
  }
276
- } else {
277
- NSLog(@"[HotUpdates] WWW folder already exists in Documents");
278
254
  }
279
255
  }
280
256
 
281
257
  - (BOOL)unzipFile:(NSString*)zipPath toDestination:(NSString*)destination {
282
- NSLog(@"[HotUpdates] Unzipping %@ to %@", zipPath, destination);
283
-
284
258
  NSFileManager *fileManager = [NSFileManager defaultManager];
285
259
  NSError *error = nil;
286
260
 
261
+ // Проверяем существование ZIP файла
262
+ if (![fileManager fileExistsAtPath:zipPath]) {
263
+ NSLog(@"[HotUpdates] ERROR: ZIP file does not exist");
264
+ return NO;
265
+ }
266
+
267
+ // Проверяем magic bytes (PK\x03\x04)
268
+ if (![self isValidZipFile:zipPath]) {
269
+ NSLog(@"[HotUpdates] ERROR: Invalid file format (not a ZIP archive)");
270
+ return NO;
271
+ }
272
+
273
+ NSLog(@"[HotUpdates] Extracting update package...");
274
+
275
+ // Создаём директорию назначения если не существует
287
276
  if (![fileManager fileExistsAtPath:destination]) {
288
277
  [fileManager createDirectoryAtPath:destination withIntermediateDirectories:YES attributes:nil error:&error];
289
278
  if (error) {
290
- NSLog(@"[HotUpdates] Error creating destination directory: %@", error.localizedDescription);
279
+ NSLog(@"[HotUpdates] ERROR: Failed to create destination directory: %@", error.localizedDescription);
291
280
  return NO;
292
281
  }
293
282
  }
294
283
 
295
- NSLog(@"[HotUpdates] Extracting ZIP archive using SSZipArchive library");
296
-
297
- if (![[NSFileManager defaultManager] fileExistsAtPath:zipPath]) {
298
- NSLog(@"[HotUpdates] ZIP file does not exist: %@", zipPath);
299
- return NO;
300
- }
301
-
302
284
  NSString *tempExtractPath = [destination stringByAppendingPathComponent:@"temp_extract"];
303
285
 
304
- if ([[NSFileManager defaultManager] fileExistsAtPath:tempExtractPath]) {
305
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
286
+ // Очищаем временную директорию
287
+ if ([fileManager fileExistsAtPath:tempExtractPath]) {
288
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
306
289
  }
307
290
 
308
- if (![[NSFileManager defaultManager] createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
309
- NSLog(@"[HotUpdates] Failed to create temp extraction folder: %@", error.localizedDescription);
291
+ if (![fileManager createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
292
+ NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
310
293
  return NO;
311
294
  }
312
295
 
313
- NSLog(@"[HotUpdates] Extracting to temp location: %@", tempExtractPath);
314
-
315
296
  BOOL extractSuccess = [SSZipArchive unzipFileAtPath:zipPath toDestination:tempExtractPath];
316
297
 
317
- if (extractSuccess) {
318
- NSLog(@"[HotUpdates] ZIP extraction successful");
319
-
320
- NSArray *extractedContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tempExtractPath error:nil];
321
- NSLog(@"[HotUpdates] Extracted contents: %@", extractedContents);
322
-
323
- // Ищем папку www (может быть вложенной)
324
- NSString *wwwSourcePath = nil;
325
- for (NSString *item in extractedContents) {
326
- NSString *itemPath = [tempExtractPath stringByAppendingPathComponent:item];
327
- BOOL isDirectory;
328
- if ([[NSFileManager defaultManager] fileExistsAtPath:itemPath isDirectory:&isDirectory] && isDirectory) {
329
- if ([item isEqualToString:@"www"]) {
330
- wwwSourcePath = itemPath;
331
- break;
332
- }
333
- NSString *nestedWwwPath = [itemPath stringByAppendingPathComponent:@"www"];
334
- if ([[NSFileManager defaultManager] fileExistsAtPath:nestedWwwPath]) {
335
- wwwSourcePath = nestedWwwPath;
336
- break;
337
- }
298
+ if (!extractSuccess) {
299
+ NSLog(@"[HotUpdates] ERROR: Failed to extract ZIP archive");
300
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
301
+ return NO;
302
+ }
303
+
304
+ NSArray *extractedContents = [fileManager contentsOfDirectoryAtPath:tempExtractPath error:nil];
305
+
306
+ // Ищем папку www (может быть вложенной)
307
+ NSString *wwwSourcePath = nil;
308
+ for (NSString *item in extractedContents) {
309
+ NSString *itemPath = [tempExtractPath stringByAppendingPathComponent:item];
310
+ BOOL isDirectory;
311
+ if ([fileManager fileExistsAtPath:itemPath isDirectory:&isDirectory] && isDirectory) {
312
+ if ([item isEqualToString:@"www"]) {
313
+ wwwSourcePath = itemPath;
314
+ break;
315
+ }
316
+ NSString *nestedWwwPath = [itemPath stringByAppendingPathComponent:@"www"];
317
+ if ([fileManager fileExistsAtPath:nestedWwwPath]) {
318
+ wwwSourcePath = nestedWwwPath;
319
+ break;
338
320
  }
339
321
  }
322
+ }
323
+
324
+ if (!wwwSourcePath) {
325
+ NSLog(@"[HotUpdates] ERROR: www folder not found in ZIP archive");
326
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
327
+ return NO;
328
+ }
340
329
 
341
- if (wwwSourcePath) {
342
- NSLog(@"[HotUpdates] Found www folder at: %@", wwwSourcePath);
330
+ NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
343
331
 
344
- NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
332
+ if ([fileManager fileExistsAtPath:finalWwwPath]) {
333
+ [fileManager removeItemAtPath:finalWwwPath error:nil];
334
+ }
345
335
 
346
- if ([[NSFileManager defaultManager] fileExistsAtPath:finalWwwPath]) {
347
- [[NSFileManager defaultManager] removeItemAtPath:finalWwwPath error:nil];
348
- }
336
+ NSError *copyError = nil;
337
+ BOOL copySuccess = [fileManager copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
349
338
 
350
- NSError *copyError = nil;
351
- BOOL copySuccess = [[NSFileManager defaultManager] copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
339
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
352
340
 
353
- if (copySuccess) {
354
- NSLog(@"[HotUpdates] www folder copied successfully to: %@", finalWwwPath);
341
+ if (!copySuccess) {
342
+ NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", copyError.localizedDescription);
343
+ return NO;
344
+ }
355
345
 
356
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
346
+ NSLog(@"[HotUpdates] Extraction completed successfully");
347
+ return YES;
348
+ }
357
349
 
358
- NSLog(@"[HotUpdates] ZIP extraction completed successfully");
359
- return YES;
360
- } else {
361
- NSLog(@"[HotUpdates] Error copying www folder: %@", copyError.localizedDescription);
362
- }
363
- } else {
364
- NSLog(@"[HotUpdates] www folder not found in ZIP archive");
365
- NSLog(@"[HotUpdates] Available contents: %@", extractedContents);
366
- }
350
+ - (BOOL)isValidZipFile:(NSString*)filePath {
351
+ NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:filePath];
352
+ if (!file) return NO;
367
353
 
368
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
369
- } else {
370
- NSLog(@"[HotUpdates] Failed to extract ZIP archive");
371
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
372
- }
354
+ NSData *header = [file readDataOfLength:4];
355
+ [file closeFile];
373
356
 
374
- NSLog(@"[HotUpdates] ZIP extraction failed");
375
- return NO;
357
+ if (header.length < 4) return NO;
358
+
359
+ const uint8_t *bytes = header.bytes;
360
+ // ZIP magic: PK\x03\x04 (0x504B0304)
361
+ return (bytes[0] == 0x50 && bytes[1] == 0x4B && bytes[2] == 0x03 && bytes[3] == 0x04);
376
362
  }
377
363
 
378
364
  #pragma mark - Settings Management
@@ -603,8 +589,7 @@ static BOOL hasPerformedInitialReload = NO;
603
589
  updateVersion = @"pending";
604
590
  }
605
591
 
606
- NSLog(@"[HotUpdates] getUpdate() called - downloading update from: %@", downloadURL);
607
- NSLog(@"[HotUpdates] Version: %@", updateVersion);
592
+ NSLog(@"[HotUpdates] getUpdate: v%@ from %@", updateVersion, downloadURL);
608
593
 
609
594
  NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
610
595
  if (installedVersion && [installedVersion isEqualToString:updateVersion]) {
@@ -632,11 +617,9 @@ static BOOL hasPerformedInitialReload = NO;
632
617
  return;
633
618
  }
634
619
 
620
+ // Сохраняем только в память, в UserDefaults запишем после успешной загрузки
635
621
  pendingUpdateURL = downloadURL;
636
622
  pendingUpdateVersion = updateVersion;
637
- [[NSUserDefaults standardUserDefaults] setObject:downloadURL forKey:kPendingUpdateURL];
638
- [[NSUserDefaults standardUserDefaults] setObject:updateVersion forKey:kPendingVersion];
639
- [[NSUserDefaults standardUserDefaults] synchronize];
640
623
 
641
624
  [self downloadUpdateOnly:downloadURL callbackId:command.callbackId];
642
625
  }
@@ -650,7 +633,7 @@ static BOOL hasPerformedInitialReload = NO;
650
633
 
651
634
  NSURL *url = [NSURL URLWithString:downloadURL];
652
635
  if (!url) {
653
- NSLog(@"[HotUpdates] Invalid URL: %@", downloadURL);
636
+ NSLog(@"[HotUpdates] ERROR: Invalid URL format");
654
637
  isDownloadingUpdate = NO;
655
638
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
656
639
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -678,7 +661,7 @@ static BOOL hasPerformedInitialReload = NO;
678
661
  [[NSUserDefaults standardUserDefaults] synchronize];
679
662
 
680
663
  if (error) {
681
- NSLog(@"[HotUpdates] Download failed: %@", error.localizedDescription);
664
+ NSLog(@"[HotUpdates] ERROR: Download failed: %@", error.localizedDescription);
682
665
 
683
666
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
684
667
  messageAsDictionary:[self createError:kErrorDownloadFailed
@@ -689,7 +672,7 @@ static BOOL hasPerformedInitialReload = NO;
689
672
 
690
673
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
691
674
  if (httpResponse.statusCode != 200) {
692
- NSLog(@"[HotUpdates] Download failed: HTTP %ld", (long)httpResponse.statusCode);
675
+ NSLog(@"[HotUpdates] ERROR: HTTP %ld", (long)httpResponse.statusCode);
693
676
 
694
677
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
695
678
  messageAsDictionary:[self createError:kErrorHTTPError
@@ -698,9 +681,7 @@ static BOOL hasPerformedInitialReload = NO;
698
681
  return;
699
682
  }
700
683
 
701
- NSLog(@"[HotUpdates] Download completed successfully");
702
-
703
- // Сохраняем скачанное обновление во временную папку
684
+ NSLog(@"[HotUpdates] Download completed, verifying...");
704
685
  [self saveDownloadedUpdate:location callbackId:callbackId];
705
686
  }];
706
687
 
@@ -708,24 +689,25 @@ static BOOL hasPerformedInitialReload = NO;
708
689
  }
709
690
 
710
691
  - (void)saveDownloadedUpdate:(NSURL*)updateLocation callbackId:(NSString*)callbackId {
711
- NSLog(@"[HotUpdates] Saving downloaded update");
712
-
713
692
  NSFileManager *fileManager = [NSFileManager defaultManager];
714
693
  NSError *error;
715
694
 
695
+ // Используем временную папку для новой загрузки (не трогаем существующую temp_downloaded_update)
696
+ NSString *newDownloadPath = [documentsPath stringByAppendingPathComponent:@"temp_new_download"];
716
697
  NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
717
698
 
718
- if ([fileManager fileExistsAtPath:tempUpdatePath]) {
719
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
699
+ // Очищаем только папку для новой загрузки
700
+ if ([fileManager fileExistsAtPath:newDownloadPath]) {
701
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
720
702
  }
721
703
 
722
- [fileManager createDirectoryAtPath:tempUpdatePath
704
+ [fileManager createDirectoryAtPath:newDownloadPath
723
705
  withIntermediateDirectories:YES
724
706
  attributes:nil
725
707
  error:&error];
726
708
 
727
709
  if (error) {
728
- NSLog(@"[HotUpdates] Error creating temp directory: %@", error);
710
+ NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
729
711
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
730
712
  messageAsDictionary:[self createError:kErrorTempDirError
731
713
  message:@"Cannot create temp directory"]];
@@ -733,11 +715,10 @@ static BOOL hasPerformedInitialReload = NO;
733
715
  return;
734
716
  }
735
717
 
736
- BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:tempUpdatePath];
718
+ BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:newDownloadPath];
737
719
 
738
720
  if (!unzipSuccess) {
739
- NSLog(@"[HotUpdates] Failed to unzip update");
740
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
721
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
741
722
 
742
723
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
743
724
  messageAsDictionary:[self createError:kErrorExtractionFailed
@@ -746,10 +727,10 @@ static BOOL hasPerformedInitialReload = NO;
746
727
  return;
747
728
  }
748
729
 
749
- NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
750
- if (![fileManager fileExistsAtPath:tempWwwPath]) {
751
- NSLog(@"[HotUpdates] www folder not found in update package");
752
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
730
+ NSString *newWwwPath = [newDownloadPath stringByAppendingPathComponent:kWWWDirName];
731
+ if (![fileManager fileExistsAtPath:newWwwPath]) {
732
+ NSLog(@"[HotUpdates] ERROR: www folder not found in update package");
733
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
753
734
 
754
735
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
755
736
  messageAsDictionary:[self createError:kErrorWWWNotFound
@@ -758,6 +739,12 @@ static BOOL hasPerformedInitialReload = NO;
758
739
  return;
759
740
  }
760
741
 
742
+ // Успех! Теперь безопасно заменяем старую temp_downloaded_update на новую
743
+ if ([fileManager fileExistsAtPath:tempUpdatePath]) {
744
+ [fileManager removeItemAtPath:tempUpdatePath error:nil];
745
+ }
746
+ [fileManager moveItemAtPath:newDownloadPath toPath:tempUpdatePath error:nil];
747
+
761
748
  // Копируем в pending_update для автоустановки при следующем запуске
762
749
  NSString *pendingPath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
763
750
 
@@ -770,18 +757,18 @@ static BOOL hasPerformedInitialReload = NO;
770
757
  error:&error];
771
758
 
772
759
  if (!copySuccess) {
773
- NSLog(@"[HotUpdates] Failed to copy to pending_update: %@", error);
774
- } else {
775
- NSLog(@"[HotUpdates] Copied to pending_update for auto-install on next launch");
760
+ NSLog(@"[HotUpdates] WARNING: Failed to copy to pending_update (auto-install disabled): %@", error.localizedDescription);
776
761
  }
777
762
 
778
763
  isUpdateReadyToInstall = YES;
764
+ // Сохраняем URL и версию в UserDefaults только после успешной загрузки
765
+ [[NSUserDefaults standardUserDefaults] setObject:pendingUpdateURL forKey:kPendingUpdateURL];
766
+ [[NSUserDefaults standardUserDefaults] setObject:pendingUpdateVersion forKey:kPendingVersion];
779
767
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kPendingUpdateReady];
780
768
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kHasPending];
781
769
  [[NSUserDefaults standardUserDefaults] synchronize];
782
770
 
783
- NSLog(@"[HotUpdates] Update downloaded and ready to install");
784
- NSLog(@"[HotUpdates] If user ignores popup, update will install automatically on next launch");
771
+ NSLog(@"[HotUpdates] Update ready (v%@)", pendingUpdateVersion);
785
772
 
786
773
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
787
774
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
@@ -790,8 +777,6 @@ static BOOL hasPerformedInitialReload = NO;
790
777
  #pragma mark - Force Update (Install Only)
791
778
 
792
779
  - (void)forceUpdate:(CDVInvokedUrlCommand*)command {
793
- NSLog(@"[HotUpdates] forceUpdate() called - installing downloaded update");
794
-
795
780
  if (!isUpdateReadyToInstall) {
796
781
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
797
782
  messageAsDictionary:[self createError:kErrorNoUpdateReady
@@ -804,7 +789,7 @@ static BOOL hasPerformedInitialReload = NO;
804
789
  NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
805
790
 
806
791
  if (![[NSFileManager defaultManager] fileExistsAtPath:tempWwwPath]) {
807
- NSLog(@"[HotUpdates] Downloaded update files not found");
792
+ NSLog(@"[HotUpdates] ERROR: Downloaded update files not found");
808
793
 
809
794
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
810
795
  messageAsDictionary:[self createError:kErrorUpdateFilesNotFound
@@ -817,13 +802,13 @@ static BOOL hasPerformedInitialReload = NO;
817
802
  }
818
803
 
819
804
  - (void)installDownloadedUpdate:(NSString*)tempWwwPath callbackId:(NSString*)callbackId {
820
- NSLog(@"[HotUpdates] Installing update");
821
-
822
805
  NSString *versionToInstall = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
823
806
  if (!versionToInstall) {
824
807
  versionToInstall = @"unknown";
825
808
  }
826
809
 
810
+ NSLog(@"[HotUpdates] forceUpdate: installing v%@", versionToInstall);
811
+
827
812
  NSFileManager *fileManager = [NSFileManager defaultManager];
828
813
  NSError *error;
829
814
 
@@ -838,7 +823,7 @@ static BOOL hasPerformedInitialReload = NO;
838
823
  error:&error];
839
824
 
840
825
  if (!copySuccess) {
841
- NSLog(@"[HotUpdates] Failed to install update: %@", error);
826
+ NSLog(@"[HotUpdates] ERROR: Failed to install update: %@", error.localizedDescription);
842
827
 
843
828
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
844
829
  messageAsDictionary:[self createError:kErrorInstallFailed
@@ -867,18 +852,14 @@ static BOOL hasPerformedInitialReload = NO;
867
852
  pendingUpdateVersion = nil;
868
853
 
869
854
  NSLog(@"[HotUpdates] Update installed successfully");
855
+ NSLog(@"[HotUpdates] Starting canary timer for version %@", newVersion);
870
856
 
871
857
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
872
858
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
873
859
 
874
- // После reloadWebView pluginInitialize НЕ вызывается, поэтому canary timer запускаем вручную
875
- NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", newVersion);
876
-
877
860
  [self startCanaryTimer];
878
-
879
861
  hasPerformedInitialReload = NO;
880
862
 
881
- // Очищаем кэш WebView перед перезагрузкой, иначе может загрузиться старая версия
882
863
  [self clearWebViewCacheWithCompletion:^{
883
864
  [self reloadWebView];
884
865
  }];
@@ -901,8 +882,6 @@ static BOOL hasPerformedInitialReload = NO;
901
882
  return;
902
883
  }
903
884
 
904
- NSLog(@"[HotUpdates] Canary called for version: %@", canaryVersion);
905
-
906
885
  // Сохраняем canary версию
907
886
  [[NSUserDefaults standardUserDefaults] setObject:canaryVersion forKey:kCanaryVersion];
908
887
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -911,7 +890,7 @@ static BOOL hasPerformedInitialReload = NO;
911
890
  if (canaryTimer && [canaryTimer isValid]) {
912
891
  [canaryTimer invalidate];
913
892
  canaryTimer = nil;
914
- NSLog(@"[HotUpdates] Canary timer stopped - JS confirmed bundle is working");
893
+ NSLog(@"[HotUpdates] Canary confirmed: v%@", canaryVersion);
915
894
  }
916
895
 
917
896
  // ТЗ: при успехе callback возвращает null