cordova-plugin-hot-updates 2.2.1 → 2.2.3

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,4 +1,4 @@
1
- # Cordova Hot Updates Plugin v2.2.1
1
+ # Cordova Hot Updates Plugin v2.2.3
2
2
 
3
3
  Frontend-controlled manual hot updates for Cordova iOS applications using WebView Reload approach.
4
4
 
@@ -14,6 +14,7 @@ This plugin enables **manual, JavaScript-controlled** web content updates for yo
14
14
  - **Auto-Install on Launch**: If user ignores update prompt, it installs on next app launch
15
15
  - **Canary System**: Automatic rollback if update fails to load (20-second timeout)
16
16
  - **IgnoreList**: Tracks problematic versions (information only, does NOT block installation)
17
+ - **Version History** *(new in v2.2.3)*: Tracks successful versions for progressive data migrations
17
18
  - **Instant Effect**: WebView Reload approach - no app restart needed
18
19
  - **Cache Management**: Clears WKWebView cache (disk, memory, Service Worker) before reload
19
20
 
@@ -268,6 +269,58 @@ window.hotUpdate.getIgnoreList(function(result) {
268
269
 
269
270
  ---
270
271
 
272
+ ### window.hotUpdate.getVersionHistory(callback)
273
+
274
+ Returns list of all successfully installed versions (excluding rolled back ones).
275
+
276
+ **New in v2.2.3** - Enables progressive data migrations.
277
+
278
+ When internal data structure changes, you may need to run migrations. This method returns the version history so your app can determine which migrations to run.
279
+
280
+ **Key behaviors:**
281
+ - **Automatically initialized** with `appBundleVersion` on first launch
282
+ - **Added to history** when update is successfully installed
283
+ - **Removed from history** when version is rolled back (failed canary)
284
+ - **Excludes ignoreList** - only contains successful versions
285
+
286
+ **Parameters:**
287
+ - `callback` (Function) - `callback(result)`
288
+ - `result`: `{versions: string[]}` - Array of successful version strings
289
+
290
+ **Example:**
291
+ ```javascript
292
+ window.hotUpdate.getVersionHistory(function(result) {
293
+ console.log('Version history:', result.versions);
294
+ // Example: ["2.7.7", "2.7.8", "2.7.9"]
295
+
296
+ // Run migrations based on version progression
297
+ result.versions.forEach((version, index) => {
298
+ if (index > 0) {
299
+ const from = result.versions[index - 1];
300
+ const to = version;
301
+ runMigration(from, to);
302
+ }
303
+ });
304
+ });
305
+ ```
306
+
307
+ **Use Case:**
308
+ ```javascript
309
+ // Check for missed critical versions
310
+ window.hotUpdate.getVersionHistory(function(result) {
311
+ const criticalVersions = ['2.8.0', '3.0.0']; // Versions with important migrations
312
+ const missed = criticalVersions.filter(v => !result.versions.includes(v));
313
+
314
+ if (missed.length > 0) {
315
+ console.warn('User skipped critical versions:', missed);
316
+ // Run all missed critical migrations
317
+ missed.forEach(v => runCriticalMigration(v));
318
+ }
319
+ });
320
+ ```
321
+
322
+ ---
323
+
271
324
  ### window.hotUpdate.getVersionInfo(callback)
272
325
 
273
326
  Returns version information (debug method).
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.3",
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.3"
4
4
  xmlns="http://apache.org/cordova/ns/plugins/1.0"
5
5
  xmlns:android="http://schemas.android.com/apk/res/android">
6
6
 
@@ -29,16 +29,18 @@
29
29
 
30
30
  // Settings
31
31
  NSMutableArray *ignoreList; // Список игнорируемых версий (управляется только native)
32
+ NSMutableArray *versionHistory; // История успешно установленных версий (исключая откаченные)
32
33
  NSString *previousVersionPath; // Путь к предыдущей версии
33
34
  }
34
35
 
35
- // JavaScript API methods (v2.1.0)
36
- - (void)getUpdate:(CDVInvokedUrlCommand*)command; // Download update
37
- - (void)forceUpdate:(CDVInvokedUrlCommand*)command; // Install downloaded update
38
- - (void)canary:(CDVInvokedUrlCommand*)command; // Confirm successful load
39
- - (void)getIgnoreList:(CDVInvokedUrlCommand*)command; // Get ignore list (JS reads only)
36
+ // JavaScript API methods (v2.2.2)
37
+ - (void)getUpdate:(CDVInvokedUrlCommand*)command; // Download update
38
+ - (void)forceUpdate:(CDVInvokedUrlCommand*)command; // Install downloaded update
39
+ - (void)canary:(CDVInvokedUrlCommand*)command; // Confirm successful load
40
+ - (void)getIgnoreList:(CDVInvokedUrlCommand*)command; // Get ignore list (JS reads only)
41
+ - (void)getVersionHistory:(CDVInvokedUrlCommand*)command; // Get version history (successful installs only)
40
42
 
41
43
  // Debug method
42
- - (void)getVersionInfo:(CDVInvokedUrlCommand*)command; // Get all version info for debugging
44
+ - (void)getVersionInfo:(CDVInvokedUrlCommand*)command; // Get all version info for debugging
43
45
 
44
46
  @end
@@ -55,26 +55,20 @@ static BOOL hasPerformedInitialReload = NO;
55
55
 
56
56
  [self loadConfiguration];
57
57
  [self loadIgnoreList];
58
+ [self loadVersionHistory];
58
59
 
59
60
  // Сбрасываем флаг загрузки (если приложение было убито во время загрузки)
60
61
  isDownloadingUpdate = NO;
61
62
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
62
63
 
63
- isUpdateReadyToInstall = NO;
64
- pendingUpdateURL = nil;
65
- pendingUpdateVersion = nil;
66
-
64
+ // Читаем состояние из UserDefaults
67
65
  pendingUpdateURL = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingUpdateURL];
68
66
  isUpdateReadyToInstall = [[NSUserDefaults standardUserDefaults] boolForKey:kPendingUpdateReady];
69
67
  if (isUpdateReadyToInstall) {
70
68
  pendingUpdateVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
71
- NSLog(@"[HotUpdates] Found pending update ready to install: %@", pendingUpdateVersion);
72
69
  }
73
70
 
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);
71
+ NSLog(@"[HotUpdates] Initializing plugin...");
78
72
 
79
73
  [self checkAndInstallPendingUpdate];
80
74
  [self initializeWWWFolder];
@@ -85,15 +79,12 @@ static BOOL hasPerformedInitialReload = NO;
85
79
  NSString *canaryVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kCanaryVersion];
86
80
 
87
81
  if (!canaryVersion || ![canaryVersion isEqualToString:currentVersion]) {
88
- NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", currentVersion);
89
-
82
+ NSLog(@"[HotUpdates] Starting canary timer for version %@", currentVersion);
90
83
  [self startCanaryTimer];
91
- } else {
92
- NSLog(@"[HotUpdates] Canary already confirmed for version %@", currentVersion);
93
84
  }
94
85
  }
95
86
 
96
- NSLog(@"[HotUpdates] Plugin initialized.");
87
+ NSLog(@"[HotUpdates] Plugin initialized (v%@)", appBundleVersion);
97
88
  }
98
89
 
99
90
  - (void)loadConfiguration {
@@ -101,9 +92,6 @@ static BOOL hasPerformedInitialReload = NO;
101
92
  if (!appBundleVersion) {
102
93
  appBundleVersion = @"1.0.0";
103
94
  }
104
-
105
- NSLog(@"[HotUpdates] Configuration loaded:");
106
- NSLog(@" App bundle version: %@", appBundleVersion);
107
95
  }
108
96
 
109
97
  /*!
@@ -115,7 +103,7 @@ static BOOL hasPerformedInitialReload = NO;
115
103
  NSString *pendingVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
116
104
 
117
105
  if (hasPendingUpdate && pendingVersion) {
118
- NSLog(@"[HotUpdates] Installing pending update %@ to Documents/www (auto-install on launch)", pendingVersion);
106
+ NSLog(@"[HotUpdates] Auto-installing pending update: %@", pendingVersion);
119
107
 
120
108
  [self backupCurrentVersion];
121
109
 
@@ -140,9 +128,18 @@ static BOOL hasPerformedInitialReload = NO;
140
128
 
141
129
  [[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
142
130
 
143
- NSLog(@"[HotUpdates] Update %@ installed successfully (canary timer will start)", pendingVersion);
131
+ // Добавляем версию в историю при успешной установке
132
+ [self addVersionToHistory:pendingVersion];
133
+
134
+ NSLog(@"[HotUpdates] Update %@ installed successfully", pendingVersion);
144
135
  } else {
145
- NSLog(@"[HotUpdates] Failed to install update: %@", copyError.localizedDescription);
136
+ NSLog(@"[HotUpdates] Failed to install pending update: %@", copyError.localizedDescription);
137
+ // Очищаем флаги чтобы не пытаться снова при следующем запуске
138
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kHasPending];
139
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPendingVersion];
140
+ [[NSUserDefaults standardUserDefaults] synchronize];
141
+ // Удаляем битое обновление
142
+ [[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
146
143
  }
147
144
  }
148
145
  }
@@ -156,7 +153,6 @@ static BOOL hasPerformedInitialReload = NO;
156
153
  - (void)switchToUpdatedContentWithReload {
157
154
  // Предотвращаем повторные перезагрузки при навигации между страницами
158
155
  if (hasPerformedInitialReload) {
159
- NSLog(@"[HotUpdates] Initial reload already performed, skipping");
160
156
  return;
161
157
  }
162
158
 
@@ -167,24 +163,19 @@ static BOOL hasPerformedInitialReload = NO;
167
163
  NSString *indexPath = [documentsWwwPath stringByAppendingPathComponent:@"index.html"];
168
164
 
169
165
  if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath]) {
170
- NSLog(@"[HotUpdates] Using WebView reload approach");
171
- NSLog(@"[HotUpdates] Found installed update version: %@", installedVersion);
166
+ NSLog(@"[HotUpdates] Loading installed version: %@", installedVersion);
172
167
 
173
168
  ((CDVViewController *)self.viewController).wwwFolderName = documentsWwwPath;
174
- NSLog(@"[HotUpdates] Changed wwwFolderName to: %@", documentsWwwPath);
175
-
176
169
  hasPerformedInitialReload = YES;
177
170
 
178
171
  // Очищаем кэш перед перезагрузкой, иначе может загрузиться старая версия
179
172
  [self clearWebViewCacheWithCompletion:^{
180
173
  [self reloadWebView];
181
- NSLog(@"[HotUpdates] WebView reloaded with updated content (version: %@)", installedVersion);
182
174
  }];
183
175
  } else {
184
- NSLog(@"[HotUpdates] Documents/www/index.html not found, keeping bundle www");
176
+ NSLog(@"[HotUpdates] WARNING: Documents/www/index.html not found, using bundle");
185
177
  }
186
178
  } else {
187
- NSLog(@"[HotUpdates] No installed updates, using bundle www");
188
179
  hasPerformedInitialReload = YES;
189
180
  }
190
181
  }
@@ -202,8 +193,6 @@ static BOOL hasPerformedInitialReload = NO;
202
193
  * @param completion Block called after cache is cleared (on main thread)
203
194
  */
204
195
  - (void)clearWebViewCacheWithCompletion:(void (^)(void))completion {
205
- NSLog(@"[HotUpdates] Clearing WebView cache");
206
-
207
196
  NSSet *websiteDataTypes = [NSSet setWithArray:@[
208
197
  WKWebsiteDataTypeDiskCache,
209
198
  WKWebsiteDataTypeMemoryCache,
@@ -215,7 +204,6 @@ static BOOL hasPerformedInitialReload = NO;
215
204
  [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes
216
205
  modifiedSince:dateFrom
217
206
  completionHandler:^{
218
- NSLog(@"[HotUpdates] WebView cache cleared");
219
207
  if (completion) {
220
208
  dispatch_async(dispatch_get_main_queue(), completion);
221
209
  }
@@ -231,26 +219,22 @@ static BOOL hasPerformedInitialReload = NO;
231
219
  NSURL *fileURL = [NSURL fileURLWithPath:indexPath];
232
220
  NSURL *allowReadAccessToURL = [NSURL fileURLWithPath:documentsWwwPath];
233
221
 
234
- NSLog(@"[HotUpdates] Loading WebView with new URL: %@", fileURL.absoluteString);
235
-
236
222
  id webViewEngine = cdvViewController.webViewEngine;
237
223
  if (webViewEngine && [webViewEngine respondsToSelector:@selector(engineWebView)]) {
238
224
  WKWebView *webView = [webViewEngine performSelector:@selector(engineWebView)];
239
225
 
240
226
  if (webView && [webView isKindOfClass:[WKWebView class]]) {
241
- // loadFileURL:allowingReadAccessToURL: правильно настраивает sandbox permissions для локальных файлов
242
227
  dispatch_async(dispatch_get_main_queue(), ^{
243
228
  [webView loadFileURL:fileURL allowingReadAccessToURL:allowReadAccessToURL];
244
- NSLog(@"[HotUpdates] WebView loadFileURL executed with sandbox permissions");
245
229
  });
246
230
  } else {
247
- NSLog(@"[HotUpdates] Could not access WKWebView for reload");
231
+ NSLog(@"[HotUpdates] ERROR: Could not access WKWebView for reload");
248
232
  }
249
233
  } else {
250
- NSLog(@"[HotUpdates] WebView engine not available for reload");
234
+ NSLog(@"[HotUpdates] ERROR: WebView engine not available for reload");
251
235
  }
252
236
  } else {
253
- NSLog(@"[HotUpdates] ViewController is not CDVViewController type");
237
+ NSLog(@"[HotUpdates] ERROR: ViewController is not CDVViewController type");
254
238
  }
255
239
  }
256
240
 
@@ -259,120 +243,126 @@ static BOOL hasPerformedInitialReload = NO;
259
243
  NSFileManager *fileManager = [NSFileManager defaultManager];
260
244
 
261
245
  if (![fileManager fileExistsAtPath:wwwPath]) {
262
- NSLog(@"[HotUpdates] WWW folder not found in Documents. Creating and copying from bundle...");
263
-
264
246
  NSString *bundleWWWPath = [[NSBundle mainBundle] pathForResource:@"www" ofType:nil];
265
247
  if (bundleWWWPath) {
266
248
  NSError *error;
267
249
  [fileManager copyItemAtPath:bundleWWWPath toPath:wwwPath error:&error];
268
250
  if (error) {
269
- NSLog(@"[HotUpdates] Error copying www folder: %@", error.localizedDescription);
251
+ NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", error.localizedDescription);
270
252
  } else {
271
- NSLog(@"[HotUpdates] WWW folder copied successfully to Documents");
253
+ NSLog(@"[HotUpdates] Initialized www folder from bundle");
272
254
  }
273
255
  } else {
274
- NSLog(@"[HotUpdates] Error: Bundle www folder not found");
256
+ NSLog(@"[HotUpdates] ERROR: Bundle www folder not found");
275
257
  }
276
- } else {
277
- NSLog(@"[HotUpdates] WWW folder already exists in Documents");
278
258
  }
279
259
  }
280
260
 
281
261
  - (BOOL)unzipFile:(NSString*)zipPath toDestination:(NSString*)destination {
282
- NSLog(@"[HotUpdates] Unzipping %@ to %@", zipPath, destination);
283
-
284
262
  NSFileManager *fileManager = [NSFileManager defaultManager];
285
263
  NSError *error = nil;
286
264
 
265
+ // Проверяем существование ZIP файла
266
+ if (![fileManager fileExistsAtPath:zipPath]) {
267
+ NSLog(@"[HotUpdates] ERROR: ZIP file does not exist");
268
+ return NO;
269
+ }
270
+
271
+ // Проверяем magic bytes (PK\x03\x04)
272
+ if (![self isValidZipFile:zipPath]) {
273
+ NSLog(@"[HotUpdates] ERROR: Invalid file format (not a ZIP archive)");
274
+ return NO;
275
+ }
276
+
277
+ NSLog(@"[HotUpdates] Extracting update package...");
278
+
279
+ // Создаём директорию назначения если не существует
287
280
  if (![fileManager fileExistsAtPath:destination]) {
288
281
  [fileManager createDirectoryAtPath:destination withIntermediateDirectories:YES attributes:nil error:&error];
289
282
  if (error) {
290
- NSLog(@"[HotUpdates] Error creating destination directory: %@", error.localizedDescription);
283
+ NSLog(@"[HotUpdates] ERROR: Failed to create destination directory: %@", error.localizedDescription);
291
284
  return NO;
292
285
  }
293
286
  }
294
287
 
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
288
  NSString *tempExtractPath = [destination stringByAppendingPathComponent:@"temp_extract"];
303
289
 
304
- if ([[NSFileManager defaultManager] fileExistsAtPath:tempExtractPath]) {
305
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
290
+ // Очищаем временную директорию
291
+ if ([fileManager fileExistsAtPath:tempExtractPath]) {
292
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
306
293
  }
307
294
 
308
- if (![[NSFileManager defaultManager] createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
309
- NSLog(@"[HotUpdates] Failed to create temp extraction folder: %@", error.localizedDescription);
295
+ if (![fileManager createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
296
+ NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
310
297
  return NO;
311
298
  }
312
299
 
313
- NSLog(@"[HotUpdates] Extracting to temp location: %@", tempExtractPath);
314
-
315
300
  BOOL extractSuccess = [SSZipArchive unzipFileAtPath:zipPath toDestination:tempExtractPath];
316
301
 
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
- }
302
+ if (!extractSuccess) {
303
+ NSLog(@"[HotUpdates] ERROR: Failed to extract ZIP archive");
304
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
305
+ return NO;
306
+ }
307
+
308
+ NSArray *extractedContents = [fileManager contentsOfDirectoryAtPath:tempExtractPath error:nil];
309
+
310
+ // Ищем папку www (может быть вложенной)
311
+ NSString *wwwSourcePath = nil;
312
+ for (NSString *item in extractedContents) {
313
+ NSString *itemPath = [tempExtractPath stringByAppendingPathComponent:item];
314
+ BOOL isDirectory;
315
+ if ([fileManager fileExistsAtPath:itemPath isDirectory:&isDirectory] && isDirectory) {
316
+ if ([item isEqualToString:@"www"]) {
317
+ wwwSourcePath = itemPath;
318
+ break;
319
+ }
320
+ NSString *nestedWwwPath = [itemPath stringByAppendingPathComponent:@"www"];
321
+ if ([fileManager fileExistsAtPath:nestedWwwPath]) {
322
+ wwwSourcePath = nestedWwwPath;
323
+ break;
338
324
  }
339
325
  }
326
+ }
340
327
 
341
- if (wwwSourcePath) {
342
- NSLog(@"[HotUpdates] Found www folder at: %@", wwwSourcePath);
328
+ if (!wwwSourcePath) {
329
+ NSLog(@"[HotUpdates] ERROR: www folder not found in ZIP archive");
330
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
331
+ return NO;
332
+ }
343
333
 
344
- NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
334
+ NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
345
335
 
346
- if ([[NSFileManager defaultManager] fileExistsAtPath:finalWwwPath]) {
347
- [[NSFileManager defaultManager] removeItemAtPath:finalWwwPath error:nil];
348
- }
336
+ if ([fileManager fileExistsAtPath:finalWwwPath]) {
337
+ [fileManager removeItemAtPath:finalWwwPath error:nil];
338
+ }
349
339
 
350
- NSError *copyError = nil;
351
- BOOL copySuccess = [[NSFileManager defaultManager] copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
340
+ NSError *copyError = nil;
341
+ BOOL copySuccess = [fileManager copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
352
342
 
353
- if (copySuccess) {
354
- NSLog(@"[HotUpdates] www folder copied successfully to: %@", finalWwwPath);
343
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
355
344
 
356
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
345
+ if (!copySuccess) {
346
+ NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", copyError.localizedDescription);
347
+ return NO;
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
+ NSLog(@"[HotUpdates] Extraction completed successfully");
351
+ return YES;
352
+ }
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
+ - (BOOL)isValidZipFile:(NSString*)filePath {
355
+ NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:filePath];
356
+ if (!file) return NO;
357
+
358
+ NSData *header = [file readDataOfLength:4];
359
+ [file closeFile];
373
360
 
374
- NSLog(@"[HotUpdates] ZIP extraction failed");
375
- return NO;
361
+ if (header.length < 4) return NO;
362
+
363
+ const uint8_t *bytes = header.bytes;
364
+ // ZIP magic: PK\x03\x04 (0x504B0304)
365
+ return (bytes[0] == 0x50 && bytes[1] == 0x4B && bytes[2] == 0x03 && bytes[3] == 0x04);
376
366
  }
377
367
 
378
368
  #pragma mark - Settings Management
@@ -409,6 +399,58 @@ static BOOL hasPerformedInitialReload = NO;
409
399
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
410
400
  }
411
401
 
402
+ #pragma mark - Version History Management
403
+
404
+ - (void)loadVersionHistory {
405
+ NSArray *savedHistory = [[NSUserDefaults standardUserDefaults] arrayForKey:kVersionHistory];
406
+ if (savedHistory) {
407
+ versionHistory = [savedHistory mutableCopy];
408
+ } else {
409
+ versionHistory = [NSMutableArray array];
410
+ // При первом запуске добавляем исходную версию приложения
411
+ if (appBundleVersion) {
412
+ [versionHistory addObject:appBundleVersion];
413
+ [self saveVersionHistory];
414
+ NSLog(@"[HotUpdates] Initial version history created with app version: %@", appBundleVersion);
415
+ }
416
+ }
417
+ }
418
+
419
+ - (NSArray*)getVersionHistoryInternal {
420
+ return [versionHistory copy];
421
+ }
422
+
423
+ - (void)saveVersionHistory {
424
+ [[NSUserDefaults standardUserDefaults] setObject:versionHistory forKey:kVersionHistory];
425
+ [[NSUserDefaults standardUserDefaults] synchronize];
426
+ }
427
+
428
+ - (void)addVersionToHistory:(NSString*)version {
429
+ if (version && ![versionHistory containsObject:version]) {
430
+ [versionHistory addObject:version];
431
+ [self saveVersionHistory];
432
+ NSLog(@"[HotUpdates] Added version %@ to version history", version);
433
+ }
434
+ }
435
+
436
+ - (void)removeVersionFromHistory:(NSString*)version {
437
+ if (version && [versionHistory containsObject:version]) {
438
+ [versionHistory removeObject:version];
439
+ [self saveVersionHistory];
440
+ NSLog(@"[HotUpdates] Removed version %@ from version history", version);
441
+ }
442
+ }
443
+
444
+ - (void)getVersionHistory:(CDVInvokedUrlCommand*)command {
445
+ NSArray *history = [self getVersionHistoryInternal];
446
+
447
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
448
+ messageAsDictionary:@{
449
+ @"versions": history
450
+ }];
451
+ [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
452
+ }
453
+
412
454
  #pragma mark - Canary Timer
413
455
 
414
456
  /*!
@@ -557,6 +599,8 @@ static BOOL hasPerformedInitialReload = NO;
557
599
 
558
600
  if (currentVersion) {
559
601
  [self addVersionToIgnoreList:currentVersion];
602
+ // Удаляем откаченную версию из истории (она не прошла canary)
603
+ [self removeVersionFromHistory:currentVersion];
560
604
  }
561
605
 
562
606
  return YES;
@@ -603,8 +647,7 @@ static BOOL hasPerformedInitialReload = NO;
603
647
  updateVersion = @"pending";
604
648
  }
605
649
 
606
- NSLog(@"[HotUpdates] getUpdate() called - downloading update from: %@", downloadURL);
607
- NSLog(@"[HotUpdates] Version: %@", updateVersion);
650
+ NSLog(@"[HotUpdates] getUpdate: v%@ from %@", updateVersion, downloadURL);
608
651
 
609
652
  NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
610
653
  if (installedVersion && [installedVersion isEqualToString:updateVersion]) {
@@ -632,11 +675,9 @@ static BOOL hasPerformedInitialReload = NO;
632
675
  return;
633
676
  }
634
677
 
678
+ // Сохраняем только в память, в UserDefaults запишем после успешной загрузки
635
679
  pendingUpdateURL = downloadURL;
636
680
  pendingUpdateVersion = updateVersion;
637
- [[NSUserDefaults standardUserDefaults] setObject:downloadURL forKey:kPendingUpdateURL];
638
- [[NSUserDefaults standardUserDefaults] setObject:updateVersion forKey:kPendingVersion];
639
- [[NSUserDefaults standardUserDefaults] synchronize];
640
681
 
641
682
  [self downloadUpdateOnly:downloadURL callbackId:command.callbackId];
642
683
  }
@@ -650,7 +691,7 @@ static BOOL hasPerformedInitialReload = NO;
650
691
 
651
692
  NSURL *url = [NSURL URLWithString:downloadURL];
652
693
  if (!url) {
653
- NSLog(@"[HotUpdates] Invalid URL: %@", downloadURL);
694
+ NSLog(@"[HotUpdates] ERROR: Invalid URL format");
654
695
  isDownloadingUpdate = NO;
655
696
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
656
697
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -678,7 +719,7 @@ static BOOL hasPerformedInitialReload = NO;
678
719
  [[NSUserDefaults standardUserDefaults] synchronize];
679
720
 
680
721
  if (error) {
681
- NSLog(@"[HotUpdates] Download failed: %@", error.localizedDescription);
722
+ NSLog(@"[HotUpdates] ERROR: Download failed: %@", error.localizedDescription);
682
723
 
683
724
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
684
725
  messageAsDictionary:[self createError:kErrorDownloadFailed
@@ -689,7 +730,7 @@ static BOOL hasPerformedInitialReload = NO;
689
730
 
690
731
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
691
732
  if (httpResponse.statusCode != 200) {
692
- NSLog(@"[HotUpdates] Download failed: HTTP %ld", (long)httpResponse.statusCode);
733
+ NSLog(@"[HotUpdates] ERROR: HTTP %ld", (long)httpResponse.statusCode);
693
734
 
694
735
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
695
736
  messageAsDictionary:[self createError:kErrorHTTPError
@@ -698,9 +739,7 @@ static BOOL hasPerformedInitialReload = NO;
698
739
  return;
699
740
  }
700
741
 
701
- NSLog(@"[HotUpdates] Download completed successfully");
702
-
703
- // Сохраняем скачанное обновление во временную папку
742
+ NSLog(@"[HotUpdates] Download completed, verifying...");
704
743
  [self saveDownloadedUpdate:location callbackId:callbackId];
705
744
  }];
706
745
 
@@ -708,24 +747,25 @@ static BOOL hasPerformedInitialReload = NO;
708
747
  }
709
748
 
710
749
  - (void)saveDownloadedUpdate:(NSURL*)updateLocation callbackId:(NSString*)callbackId {
711
- NSLog(@"[HotUpdates] Saving downloaded update");
712
-
713
750
  NSFileManager *fileManager = [NSFileManager defaultManager];
714
751
  NSError *error;
715
752
 
753
+ // Используем временную папку для новой загрузки (не трогаем существующую temp_downloaded_update)
754
+ NSString *newDownloadPath = [documentsPath stringByAppendingPathComponent:@"temp_new_download"];
716
755
  NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
717
756
 
718
- if ([fileManager fileExistsAtPath:tempUpdatePath]) {
719
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
757
+ // Очищаем только папку для новой загрузки
758
+ if ([fileManager fileExistsAtPath:newDownloadPath]) {
759
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
720
760
  }
721
761
 
722
- [fileManager createDirectoryAtPath:tempUpdatePath
762
+ [fileManager createDirectoryAtPath:newDownloadPath
723
763
  withIntermediateDirectories:YES
724
764
  attributes:nil
725
765
  error:&error];
726
766
 
727
767
  if (error) {
728
- NSLog(@"[HotUpdates] Error creating temp directory: %@", error);
768
+ NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
729
769
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
730
770
  messageAsDictionary:[self createError:kErrorTempDirError
731
771
  message:@"Cannot create temp directory"]];
@@ -733,11 +773,10 @@ static BOOL hasPerformedInitialReload = NO;
733
773
  return;
734
774
  }
735
775
 
736
- BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:tempUpdatePath];
776
+ BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:newDownloadPath];
737
777
 
738
778
  if (!unzipSuccess) {
739
- NSLog(@"[HotUpdates] Failed to unzip update");
740
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
779
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
741
780
 
742
781
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
743
782
  messageAsDictionary:[self createError:kErrorExtractionFailed
@@ -746,10 +785,10 @@ static BOOL hasPerformedInitialReload = NO;
746
785
  return;
747
786
  }
748
787
 
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];
788
+ NSString *newWwwPath = [newDownloadPath stringByAppendingPathComponent:kWWWDirName];
789
+ if (![fileManager fileExistsAtPath:newWwwPath]) {
790
+ NSLog(@"[HotUpdates] ERROR: www folder not found in update package");
791
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
753
792
 
754
793
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
755
794
  messageAsDictionary:[self createError:kErrorWWWNotFound
@@ -758,6 +797,12 @@ static BOOL hasPerformedInitialReload = NO;
758
797
  return;
759
798
  }
760
799
 
800
+ // Успех! Теперь безопасно заменяем старую temp_downloaded_update на новую
801
+ if ([fileManager fileExistsAtPath:tempUpdatePath]) {
802
+ [fileManager removeItemAtPath:tempUpdatePath error:nil];
803
+ }
804
+ [fileManager moveItemAtPath:newDownloadPath toPath:tempUpdatePath error:nil];
805
+
761
806
  // Копируем в pending_update для автоустановки при следующем запуске
762
807
  NSString *pendingPath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
763
808
 
@@ -770,18 +815,18 @@ static BOOL hasPerformedInitialReload = NO;
770
815
  error:&error];
771
816
 
772
817
  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");
818
+ NSLog(@"[HotUpdates] WARNING: Failed to copy to pending_update (auto-install disabled): %@", error.localizedDescription);
776
819
  }
777
820
 
778
821
  isUpdateReadyToInstall = YES;
822
+ // Сохраняем URL и версию в UserDefaults только после успешной загрузки
823
+ [[NSUserDefaults standardUserDefaults] setObject:pendingUpdateURL forKey:kPendingUpdateURL];
824
+ [[NSUserDefaults standardUserDefaults] setObject:pendingUpdateVersion forKey:kPendingVersion];
779
825
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kPendingUpdateReady];
780
826
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kHasPending];
781
827
  [[NSUserDefaults standardUserDefaults] synchronize];
782
828
 
783
- NSLog(@"[HotUpdates] Update downloaded and ready to install");
784
- NSLog(@"[HotUpdates] If user ignores popup, update will install automatically on next launch");
829
+ NSLog(@"[HotUpdates] Update ready (v%@)", pendingUpdateVersion);
785
830
 
786
831
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
787
832
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
@@ -790,8 +835,6 @@ static BOOL hasPerformedInitialReload = NO;
790
835
  #pragma mark - Force Update (Install Only)
791
836
 
792
837
  - (void)forceUpdate:(CDVInvokedUrlCommand*)command {
793
- NSLog(@"[HotUpdates] forceUpdate() called - installing downloaded update");
794
-
795
838
  if (!isUpdateReadyToInstall) {
796
839
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
797
840
  messageAsDictionary:[self createError:kErrorNoUpdateReady
@@ -804,7 +847,7 @@ static BOOL hasPerformedInitialReload = NO;
804
847
  NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
805
848
 
806
849
  if (![[NSFileManager defaultManager] fileExistsAtPath:tempWwwPath]) {
807
- NSLog(@"[HotUpdates] Downloaded update files not found");
850
+ NSLog(@"[HotUpdates] ERROR: Downloaded update files not found");
808
851
 
809
852
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
810
853
  messageAsDictionary:[self createError:kErrorUpdateFilesNotFound
@@ -817,13 +860,13 @@ static BOOL hasPerformedInitialReload = NO;
817
860
  }
818
861
 
819
862
  - (void)installDownloadedUpdate:(NSString*)tempWwwPath callbackId:(NSString*)callbackId {
820
- NSLog(@"[HotUpdates] Installing update");
821
-
822
863
  NSString *versionToInstall = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
823
864
  if (!versionToInstall) {
824
865
  versionToInstall = @"unknown";
825
866
  }
826
867
 
868
+ NSLog(@"[HotUpdates] forceUpdate: installing v%@", versionToInstall);
869
+
827
870
  NSFileManager *fileManager = [NSFileManager defaultManager];
828
871
  NSError *error;
829
872
 
@@ -838,7 +881,7 @@ static BOOL hasPerformedInitialReload = NO;
838
881
  error:&error];
839
882
 
840
883
  if (!copySuccess) {
841
- NSLog(@"[HotUpdates] Failed to install update: %@", error);
884
+ NSLog(@"[HotUpdates] ERROR: Failed to install update: %@", error.localizedDescription);
842
885
 
843
886
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
844
887
  messageAsDictionary:[self createError:kErrorInstallFailed
@@ -866,19 +909,18 @@ static BOOL hasPerformedInitialReload = NO;
866
909
  pendingUpdateURL = nil;
867
910
  pendingUpdateVersion = nil;
868
911
 
912
+ // Добавляем версию в историю при успешной установке
913
+ [self addVersionToHistory:newVersion];
914
+
869
915
  NSLog(@"[HotUpdates] Update installed successfully");
916
+ NSLog(@"[HotUpdates] Starting canary timer for version %@", newVersion);
870
917
 
871
918
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
872
919
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
873
920
 
874
- // После reloadWebView pluginInitialize НЕ вызывается, поэтому canary timer запускаем вручную
875
- NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", newVersion);
876
-
877
921
  [self startCanaryTimer];
878
-
879
922
  hasPerformedInitialReload = NO;
880
923
 
881
- // Очищаем кэш WebView перед перезагрузкой, иначе может загрузиться старая версия
882
924
  [self clearWebViewCacheWithCompletion:^{
883
925
  [self reloadWebView];
884
926
  }];
@@ -901,8 +943,6 @@ static BOOL hasPerformedInitialReload = NO;
901
943
  return;
902
944
  }
903
945
 
904
- NSLog(@"[HotUpdates] Canary called for version: %@", canaryVersion);
905
-
906
946
  // Сохраняем canary версию
907
947
  [[NSUserDefaults standardUserDefaults] setObject:canaryVersion forKey:kCanaryVersion];
908
948
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -911,7 +951,7 @@ static BOOL hasPerformedInitialReload = NO;
911
951
  if (canaryTimer && [canaryTimer isValid]) {
912
952
  [canaryTimer invalidate];
913
953
  canaryTimer = nil;
914
- NSLog(@"[HotUpdates] Canary timer stopped - JS confirmed bundle is working");
954
+ NSLog(@"[HotUpdates] Canary confirmed: v%@", canaryVersion);
915
955
  }
916
956
 
917
957
  // ТЗ: при успехе callback возвращает null
@@ -39,6 +39,7 @@ extern NSString * const kCanaryVersion;
39
39
  extern NSString * const kDownloadInProgress;
40
40
  extern NSString * const kPendingUpdateURL;
41
41
  extern NSString * const kPendingUpdateReady;
42
+ extern NSString * const kVersionHistory;
42
43
 
43
44
  #pragma mark - Directory Names
44
45
 
@@ -37,6 +37,7 @@ NSString * const kCanaryVersion = @"hot_updates_canary_version";
37
37
  NSString * const kDownloadInProgress = @"hot_updates_download_in_progress";
38
38
  NSString * const kPendingUpdateURL = @"hot_updates_pending_update_url";
39
39
  NSString * const kPendingUpdateReady = @"hot_updates_pending_ready";
40
+ NSString * const kVersionHistory = @"hot_updates_version_history";
40
41
 
41
42
  #pragma mark - Directory Names
42
43