cordova-plugin-hot-updates 2.2.0 → 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.0
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
 
@@ -125,11 +136,11 @@ callback(null)
125
136
  ### Error Handling Example
126
137
 
127
138
  ```javascript
128
- window.hotUpdate.getUpdate({url: 'http://...'}, function(error) {
129
- if (error) {
130
- console.error('[HotUpdates]', error.code, ':', error.message);
139
+ window.hotUpdate.getUpdate({url: 'http://...'}, function(result) {
140
+ if (result && result.error) {
141
+ console.error('[HotUpdates]', result.error.code, ':', result.error.message);
131
142
 
132
- switch(error.code) {
143
+ switch(result.error.code) {
133
144
  case 'HTTP_ERROR':
134
145
  // Handle HTTP errors
135
146
  break;
@@ -137,7 +148,7 @@ window.hotUpdate.getUpdate({url: 'http://...'}, function(error) {
137
148
  // Handle network errors
138
149
  break;
139
150
  default:
140
- console.error('Unknown error:', error);
151
+ console.error('Unknown error:', result.error);
141
152
  }
142
153
  } else {
143
154
  console.log('Update downloaded successfully');
@@ -268,6 +279,32 @@ window.hotUpdate.getIgnoreList(function(result) {
268
279
 
269
280
  ---
270
281
 
282
+ ### window.hotUpdate.getVersionInfo(callback)
283
+
284
+ Returns version information (debug method).
285
+
286
+ **Parameters:**
287
+ - `callback` (Function) - `callback(info)`
288
+ - `info.appBundleVersion` (string) - Native app version from Info.plist
289
+ - `info.installedVersion` (string|null) - Current hot update version
290
+ - `info.previousVersion` (string|null) - Last working version (for rollback)
291
+ - `info.canaryVersion` (string|null) - Version confirmed by canary
292
+ - `info.pendingVersion` (string|null) - Version pending installation
293
+ - `info.hasPendingUpdate` (boolean) - Whether pending update exists
294
+ - `info.ignoreList` (string[]) - Array of problematic versions
295
+
296
+ **Example:**
297
+ ```javascript
298
+ window.hotUpdate.getVersionInfo(function(info) {
299
+ console.log('App version:', info.appBundleVersion);
300
+ console.log('Installed:', info.installedVersion);
301
+ console.log('Previous:', info.previousVersion);
302
+ console.log('Pending:', info.hasPendingUpdate ? info.pendingVersion : 'none');
303
+ });
304
+ ```
305
+
306
+ ---
307
+
271
308
  ## Complete Update Flow
272
309
 
273
310
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-hot-updates",
3
- "version": "2.2.0",
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.0"
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
 
@@ -2,7 +2,7 @@
2
2
  * @file HotUpdates+Helpers.h
3
3
  * @brief Helper methods for Hot Updates Plugin
4
4
  * @details Category extension providing utility methods for error handling
5
- * @version 2.2.0
5
+ * @version 2.1.0
6
6
  * @date 2025-11-13
7
7
  * @author Mustafin Vladimir
8
8
  * @copyright Copyright (c) 2025. All rights reserved.
@@ -2,7 +2,7 @@
2
2
  * @file HotUpdates+Helpers.m
3
3
  * @brief Implementation of helper methods for Hot Updates Plugin
4
4
  * @details Provides utility methods for error handling and response formatting
5
- * @version 2.2.0
5
+ * @version 2.1.0
6
6
  * @date 2025-11-13
7
7
  * @author Mustafin Vladimir
8
8
  * @copyright Copyright (c) 2025. All rights reserved.
@@ -32,39 +32,13 @@
32
32
  NSString *previousVersionPath; // Путь к предыдущей версии
33
33
  }
34
34
 
35
- // Plugin lifecycle methods
36
- - (void)pluginInitialize;
37
- - (void)loadConfiguration;
38
- - (void)initializeWWWFolder;
39
- - (void)checkAndInstallPendingUpdate;
40
- - (void)switchToUpdatedContentWithReload;
41
- - (void)reloadWebView;
42
-
43
- // Update management methods (internal)
44
- - (void)installPendingUpdate:(NSString*)newVersion;
45
- - (BOOL)unzipFile:(NSString *)zipPath toDestination:(NSString *)destinationPath;
46
-
47
- // Version comparison utilities
48
- - (NSComparisonResult)compareVersion:(NSString*)version1 withVersion:(NSString*)version2;
49
-
50
- // JavaScript callable methods (minimal set for debugging)
51
- - (void)getCurrentVersion:(CDVInvokedUrlCommand*)command;
52
- - (void)getPendingUpdateInfo:(CDVInvokedUrlCommand*)command;
53
-
54
- // Ignore List management (JS can only read, native controls)
55
- - (void)getIgnoreList:(CDVInvokedUrlCommand*)command;
56
-
57
- // Debug methods (for manual testing only)
58
- - (void)addToIgnoreList:(CDVInvokedUrlCommand*)command;
59
- - (void)removeFromIgnoreList:(CDVInvokedUrlCommand*)command;
60
- - (void)clearIgnoreList:(CDVInvokedUrlCommand*)command;
61
-
62
- // Update methods (v2.1.0 - manual updates only)
35
+ // JavaScript API methods (v2.1.0)
63
36
  - (void)getUpdate:(CDVInvokedUrlCommand*)command; // Download update
64
37
  - (void)forceUpdate:(CDVInvokedUrlCommand*)command; // Install downloaded update
65
38
  - (void)canary:(CDVInvokedUrlCommand*)command; // Confirm successful load
39
+ - (void)getIgnoreList:(CDVInvokedUrlCommand*)command; // Get ignore list (JS reads only)
66
40
 
67
- // Debug methods
41
+ // Debug method
68
42
  - (void)getVersionInfo:(CDVInvokedUrlCommand*)command; // Get all version info for debugging
69
43
 
70
44
  @end
@@ -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.2.0
16
- * @date 2025-11-03
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
  */
@@ -56,21 +56,18 @@ static BOOL hasPerformedInitialReload = NO;
56
56
  [self loadConfiguration];
57
57
  [self loadIgnoreList];
58
58
 
59
- isUpdateReadyToInstall = NO;
60
- pendingUpdateURL = nil;
61
- pendingUpdateVersion = nil;
59
+ // Сбрасываем флаг загрузки (если приложение было убито во время загрузки)
60
+ isDownloadingUpdate = NO;
61
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
62
62
 
63
+ // Читаем состояние из UserDefaults
63
64
  pendingUpdateURL = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingUpdateURL];
64
65
  isUpdateReadyToInstall = [[NSUserDefaults standardUserDefaults] boolForKey:kPendingUpdateReady];
65
66
  if (isUpdateReadyToInstall) {
66
67
  pendingUpdateVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
67
- NSLog(@"[HotUpdates] Found pending update ready to install: %@", pendingUpdateVersion);
68
68
  }
69
69
 
70
- NSLog(@"[HotUpdates] Startup sequence initiated");
71
- NSLog(@"[HotUpdates] Bundle www path: %@", [[NSBundle mainBundle] pathForResource:kWWWDirName ofType:nil]);
72
- NSLog(@"[HotUpdates] Documents www path: %@", wwwPath);
73
- NSLog(@"[HotUpdates] Ignore list: %@", ignoreList);
70
+ NSLog(@"[HotUpdates] Initializing plugin...");
74
71
 
75
72
  [self checkAndInstallPendingUpdate];
76
73
  [self initializeWWWFolder];
@@ -81,19 +78,12 @@ static BOOL hasPerformedInitialReload = NO;
81
78
  NSString *canaryVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kCanaryVersion];
82
79
 
83
80
  if (!canaryVersion || ![canaryVersion isEqualToString:currentVersion]) {
84
- NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", currentVersion);
85
-
86
- canaryTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
87
- target:self
88
- selector:@selector(canaryTimeout)
89
- userInfo:nil
90
- repeats:NO];
91
- } else {
92
- NSLog(@"[HotUpdates] Canary already confirmed for version %@", currentVersion);
81
+ NSLog(@"[HotUpdates] Starting canary timer for version %@", currentVersion);
82
+ [self startCanaryTimer];
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
  /*!
@@ -113,10 +100,9 @@ static BOOL hasPerformedInitialReload = NO;
113
100
  - (void)checkAndInstallPendingUpdate {
114
101
  BOOL hasPendingUpdate = [[NSUserDefaults standardUserDefaults] boolForKey:kHasPending];
115
102
  NSString *pendingVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
116
- NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
117
103
 
118
104
  if (hasPendingUpdate && pendingVersion) {
119
- NSLog(@"[HotUpdates] Installing pending update %@ to Documents/www (auto-install on launch)", pendingVersion);
105
+ NSLog(@"[HotUpdates] Auto-installing pending update: %@", pendingVersion);
120
106
 
121
107
  [self backupCurrentVersion];
122
108
 
@@ -141,9 +127,15 @@ static BOOL hasPerformedInitialReload = NO;
141
127
 
142
128
  [[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
143
129
 
144
- NSLog(@"[HotUpdates] Update %@ installed successfully (canary timer will start)", pendingVersion);
130
+ NSLog(@"[HotUpdates] Update %@ installed successfully", pendingVersion);
145
131
  } else {
146
- 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];
147
139
  }
148
140
  }
149
141
  }
@@ -157,7 +149,6 @@ static BOOL hasPerformedInitialReload = NO;
157
149
  - (void)switchToUpdatedContentWithReload {
158
150
  // Предотвращаем повторные перезагрузки при навигации между страницами
159
151
  if (hasPerformedInitialReload) {
160
- NSLog(@"[HotUpdates] Initial reload already performed, skipping");
161
152
  return;
162
153
  }
163
154
 
@@ -168,33 +159,36 @@ static BOOL hasPerformedInitialReload = NO;
168
159
  NSString *indexPath = [documentsWwwPath stringByAppendingPathComponent:@"index.html"];
169
160
 
170
161
  if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath]) {
171
- NSLog(@"[HotUpdates] Using WebView reload approach");
172
- NSLog(@"[HotUpdates] Found installed update version: %@", installedVersion);
162
+ NSLog(@"[HotUpdates] Loading installed version: %@", installedVersion);
173
163
 
174
164
  ((CDVViewController *)self.viewController).wwwFolderName = documentsWwwPath;
175
- NSLog(@"[HotUpdates] Changed wwwFolderName to: %@", documentsWwwPath);
176
-
177
- [self reloadWebView];
178
-
179
165
  hasPerformedInitialReload = YES;
180
166
 
181
- NSLog(@"[HotUpdates] WebView reloaded with updated content (version: %@)", installedVersion);
167
+ // Очищаем кэш перед перезагрузкой, иначе может загрузиться старая версия
168
+ [self clearWebViewCacheWithCompletion:^{
169
+ [self reloadWebView];
170
+ }];
182
171
  } else {
183
- NSLog(@"[HotUpdates] Documents/www/index.html not found, keeping bundle www");
172
+ NSLog(@"[HotUpdates] WARNING: Documents/www/index.html not found, using bundle");
184
173
  }
185
174
  } else {
186
- NSLog(@"[HotUpdates] No installed updates, using bundle www");
187
175
  hasPerformedInitialReload = YES;
188
176
  }
189
177
  }
190
178
 
191
179
  /*!
192
- * @brief Force reload the WebView
193
- * @details Uses WKWebView loadFileURL with proper sandbox permissions
180
+ * @brief Clear WebView cache
181
+ * @details Clears disk cache, memory cache, offline storage and service workers
194
182
  */
195
183
  - (void)clearWebViewCache {
196
- NSLog(@"[HotUpdates] Clearing WebView cache");
184
+ [self clearWebViewCacheWithCompletion:nil];
185
+ }
197
186
 
187
+ /*!
188
+ * @brief Clear WebView cache with completion handler
189
+ * @param completion Block called after cache is cleared (on main thread)
190
+ */
191
+ - (void)clearWebViewCacheWithCompletion:(void (^)(void))completion {
198
192
  NSSet *websiteDataTypes = [NSSet setWithArray:@[
199
193
  WKWebsiteDataTypeDiskCache,
200
194
  WKWebsiteDataTypeMemoryCache,
@@ -206,7 +200,9 @@ static BOOL hasPerformedInitialReload = NO;
206
200
  [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes
207
201
  modifiedSince:dateFrom
208
202
  completionHandler:^{
209
- NSLog(@"[HotUpdates] WebView cache cleared");
203
+ if (completion) {
204
+ dispatch_async(dispatch_get_main_queue(), completion);
205
+ }
210
206
  }];
211
207
  }
212
208
 
@@ -219,26 +215,22 @@ static BOOL hasPerformedInitialReload = NO;
219
215
  NSURL *fileURL = [NSURL fileURLWithPath:indexPath];
220
216
  NSURL *allowReadAccessToURL = [NSURL fileURLWithPath:documentsWwwPath];
221
217
 
222
- NSLog(@"[HotUpdates] Loading WebView with new URL: %@", fileURL.absoluteString);
223
-
224
218
  id webViewEngine = cdvViewController.webViewEngine;
225
219
  if (webViewEngine && [webViewEngine respondsToSelector:@selector(engineWebView)]) {
226
220
  WKWebView *webView = [webViewEngine performSelector:@selector(engineWebView)];
227
221
 
228
222
  if (webView && [webView isKindOfClass:[WKWebView class]]) {
229
- // loadFileURL:allowingReadAccessToURL: правильно настраивает sandbox permissions для локальных файлов
230
223
  dispatch_async(dispatch_get_main_queue(), ^{
231
224
  [webView loadFileURL:fileURL allowingReadAccessToURL:allowReadAccessToURL];
232
- NSLog(@"[HotUpdates] WebView loadFileURL executed with sandbox permissions");
233
225
  });
234
226
  } else {
235
- NSLog(@"[HotUpdates] Could not access WKWebView for reload");
227
+ NSLog(@"[HotUpdates] ERROR: Could not access WKWebView for reload");
236
228
  }
237
229
  } else {
238
- NSLog(@"[HotUpdates] WebView engine not available for reload");
230
+ NSLog(@"[HotUpdates] ERROR: WebView engine not available for reload");
239
231
  }
240
232
  } else {
241
- NSLog(@"[HotUpdates] ViewController is not CDVViewController type");
233
+ NSLog(@"[HotUpdates] ERROR: ViewController is not CDVViewController type");
242
234
  }
243
235
  }
244
236
 
@@ -247,120 +239,126 @@ static BOOL hasPerformedInitialReload = NO;
247
239
  NSFileManager *fileManager = [NSFileManager defaultManager];
248
240
 
249
241
  if (![fileManager fileExistsAtPath:wwwPath]) {
250
- NSLog(@"[HotUpdates] WWW folder not found in Documents. Creating and copying from bundle...");
251
-
252
242
  NSString *bundleWWWPath = [[NSBundle mainBundle] pathForResource:@"www" ofType:nil];
253
243
  if (bundleWWWPath) {
254
244
  NSError *error;
255
245
  [fileManager copyItemAtPath:bundleWWWPath toPath:wwwPath error:&error];
256
246
  if (error) {
257
- NSLog(@"[HotUpdates] Error copying www folder: %@", error.localizedDescription);
247
+ NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", error.localizedDescription);
258
248
  } else {
259
- NSLog(@"[HotUpdates] WWW folder copied successfully to Documents");
249
+ NSLog(@"[HotUpdates] Initialized www folder from bundle");
260
250
  }
261
251
  } else {
262
- NSLog(@"[HotUpdates] Error: Bundle www folder not found");
252
+ NSLog(@"[HotUpdates] ERROR: Bundle www folder not found");
263
253
  }
264
- } else {
265
- NSLog(@"[HotUpdates] WWW folder already exists in Documents");
266
254
  }
267
255
  }
268
256
 
269
257
  - (BOOL)unzipFile:(NSString*)zipPath toDestination:(NSString*)destination {
270
- NSLog(@"[HotUpdates] Unzipping %@ to %@", zipPath, destination);
271
-
272
258
  NSFileManager *fileManager = [NSFileManager defaultManager];
273
259
  NSError *error = nil;
274
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
+ // Создаём директорию назначения если не существует
275
276
  if (![fileManager fileExistsAtPath:destination]) {
276
277
  [fileManager createDirectoryAtPath:destination withIntermediateDirectories:YES attributes:nil error:&error];
277
278
  if (error) {
278
- NSLog(@"[HotUpdates] Error creating destination directory: %@", error.localizedDescription);
279
+ NSLog(@"[HotUpdates] ERROR: Failed to create destination directory: %@", error.localizedDescription);
279
280
  return NO;
280
281
  }
281
282
  }
282
283
 
283
- NSLog(@"[HotUpdates] Extracting ZIP archive using SSZipArchive library");
284
-
285
- if (![[NSFileManager defaultManager] fileExistsAtPath:zipPath]) {
286
- NSLog(@"[HotUpdates] ZIP file does not exist: %@", zipPath);
287
- return NO;
288
- }
289
-
290
284
  NSString *tempExtractPath = [destination stringByAppendingPathComponent:@"temp_extract"];
291
285
 
292
- if ([[NSFileManager defaultManager] fileExistsAtPath:tempExtractPath]) {
293
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
286
+ // Очищаем временную директорию
287
+ if ([fileManager fileExistsAtPath:tempExtractPath]) {
288
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
294
289
  }
295
290
 
296
- if (![[NSFileManager defaultManager] createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
297
- 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);
298
293
  return NO;
299
294
  }
300
295
 
301
- NSLog(@"[HotUpdates] Extracting to temp location: %@", tempExtractPath);
302
-
303
296
  BOOL extractSuccess = [SSZipArchive unzipFileAtPath:zipPath toDestination:tempExtractPath];
304
297
 
305
- if (extractSuccess) {
306
- NSLog(@"[HotUpdates] ZIP extraction successful");
307
-
308
- NSArray *extractedContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tempExtractPath error:nil];
309
- NSLog(@"[HotUpdates] Extracted contents: %@", extractedContents);
310
-
311
- // Ищем папку www (может быть вложенной)
312
- NSString *wwwSourcePath = nil;
313
- for (NSString *item in extractedContents) {
314
- NSString *itemPath = [tempExtractPath stringByAppendingPathComponent:item];
315
- BOOL isDirectory;
316
- if ([[NSFileManager defaultManager] fileExistsAtPath:itemPath isDirectory:&isDirectory] && isDirectory) {
317
- if ([item isEqualToString:@"www"]) {
318
- wwwSourcePath = itemPath;
319
- break;
320
- }
321
- NSString *nestedWwwPath = [itemPath stringByAppendingPathComponent:@"www"];
322
- if ([[NSFileManager defaultManager] fileExistsAtPath:nestedWwwPath]) {
323
- wwwSourcePath = nestedWwwPath;
324
- break;
325
- }
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;
326
320
  }
327
321
  }
322
+ }
328
323
 
329
- if (wwwSourcePath) {
330
- NSLog(@"[HotUpdates] Found www folder at: %@", wwwSourcePath);
324
+ if (!wwwSourcePath) {
325
+ NSLog(@"[HotUpdates] ERROR: www folder not found in ZIP archive");
326
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
327
+ return NO;
328
+ }
331
329
 
332
- NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
330
+ NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
333
331
 
334
- if ([[NSFileManager defaultManager] fileExistsAtPath:finalWwwPath]) {
335
- [[NSFileManager defaultManager] removeItemAtPath:finalWwwPath error:nil];
336
- }
332
+ if ([fileManager fileExistsAtPath:finalWwwPath]) {
333
+ [fileManager removeItemAtPath:finalWwwPath error:nil];
334
+ }
337
335
 
338
- NSError *copyError = nil;
339
- BOOL copySuccess = [[NSFileManager defaultManager] copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
336
+ NSError *copyError = nil;
337
+ BOOL copySuccess = [fileManager copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
340
338
 
341
- if (copySuccess) {
342
- NSLog(@"[HotUpdates] www folder copied successfully to: %@", finalWwwPath);
339
+ [fileManager removeItemAtPath:tempExtractPath error:nil];
343
340
 
344
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
341
+ if (!copySuccess) {
342
+ NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", copyError.localizedDescription);
343
+ return NO;
344
+ }
345
345
 
346
- NSLog(@"[HotUpdates] ZIP extraction completed successfully");
347
- return YES;
348
- } else {
349
- NSLog(@"[HotUpdates] Error copying www folder: %@", copyError.localizedDescription);
350
- }
351
- } else {
352
- NSLog(@"[HotUpdates] www folder not found in ZIP archive");
353
- NSLog(@"[HotUpdates] Available contents: %@", extractedContents);
354
- }
346
+ NSLog(@"[HotUpdates] Extraction completed successfully");
347
+ return YES;
348
+ }
355
349
 
356
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
357
- } else {
358
- NSLog(@"[HotUpdates] Failed to extract ZIP archive");
359
- [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
360
- }
350
+ - (BOOL)isValidZipFile:(NSString*)filePath {
351
+ NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:filePath];
352
+ if (!file) return NO;
361
353
 
362
- NSLog(@"[HotUpdates] ZIP extraction failed");
363
- return NO;
354
+ NSData *header = [file readDataOfLength:4];
355
+ [file closeFile];
356
+
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);
364
362
  }
365
363
 
366
364
  #pragma mark - Settings Management
@@ -397,6 +395,28 @@ static BOOL hasPerformedInitialReload = NO;
397
395
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
398
396
  }
399
397
 
398
+ #pragma mark - Canary Timer
399
+
400
+ /*!
401
+ * @brief Start canary timer with weak self to prevent retain cycle
402
+ * @details Uses block-based timer (iOS 10+) with weak reference
403
+ */
404
+ - (void)startCanaryTimer {
405
+ // Инвалидируем предыдущий таймер если есть
406
+ if (canaryTimer && [canaryTimer isValid]) {
407
+ [canaryTimer invalidate];
408
+ canaryTimer = nil;
409
+ }
410
+
411
+ // Используем weak self для предотвращения retain cycle
412
+ __weak __typeof__(self) weakSelf = self;
413
+ canaryTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
414
+ repeats:NO
415
+ block:^(NSTimer * _Nonnull timer) {
416
+ [weakSelf canaryTimeout];
417
+ }];
418
+ }
419
+
400
420
  #pragma mark - Canary Timeout Handler
401
421
 
402
422
  - (void)canaryTimeout {
@@ -412,10 +432,7 @@ static BOOL hasPerformedInitialReload = NO;
412
432
 
413
433
  NSLog(@"[HotUpdates] Version %@ considered faulty, performing rollback", currentVersion);
414
434
 
415
- if (currentVersion) {
416
- [self addVersionToIgnoreList:currentVersion];
417
- }
418
-
435
+ // Примечание: версия добавляется в ignoreList внутри rollbackToPreviousVersion
419
436
  BOOL rollbackSuccess = [self rollbackToPreviousVersion];
420
437
 
421
438
  if (rollbackSuccess) {
@@ -423,8 +440,9 @@ static BOOL hasPerformedInitialReload = NO;
423
440
 
424
441
  hasPerformedInitialReload = NO;
425
442
 
426
- [self clearWebViewCache];
427
- [self reloadWebView];
443
+ [self clearWebViewCacheWithCompletion:^{
444
+ [self reloadWebView];
445
+ }];
428
446
  } else {
429
447
  NSLog(@"[HotUpdates] Automatic rollback failed");
430
448
  }
@@ -542,7 +560,11 @@ static BOOL hasPerformedInitialReload = NO;
542
560
  #pragma mark - Get Update (Download Only)
543
561
 
544
562
  - (void)getUpdate:(CDVInvokedUrlCommand*)command {
545
- NSDictionary *updateData = [command.arguments objectAtIndex:0];
563
+ // Безопасное получение первого аргумента
564
+ NSDictionary *updateData = nil;
565
+ if (command.arguments.count > 0 && [command.arguments[0] isKindOfClass:[NSDictionary class]]) {
566
+ updateData = command.arguments[0];
567
+ }
546
568
 
547
569
  if (!updateData) {
548
570
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
@@ -567,8 +589,7 @@ static BOOL hasPerformedInitialReload = NO;
567
589
  updateVersion = @"pending";
568
590
  }
569
591
 
570
- NSLog(@"[HotUpdates] getUpdate() called - downloading update from: %@", downloadURL);
571
- NSLog(@"[HotUpdates] Version: %@", updateVersion);
592
+ NSLog(@"[HotUpdates] getUpdate: v%@ from %@", updateVersion, downloadURL);
572
593
 
573
594
  NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
574
595
  if (installedVersion && [installedVersion isEqualToString:updateVersion]) {
@@ -596,11 +617,9 @@ static BOOL hasPerformedInitialReload = NO;
596
617
  return;
597
618
  }
598
619
 
620
+ // Сохраняем только в память, в UserDefaults запишем после успешной загрузки
599
621
  pendingUpdateURL = downloadURL;
600
622
  pendingUpdateVersion = updateVersion;
601
- [[NSUserDefaults standardUserDefaults] setObject:downloadURL forKey:kPendingUpdateURL];
602
- [[NSUserDefaults standardUserDefaults] setObject:updateVersion forKey:kPendingVersion];
603
- [[NSUserDefaults standardUserDefaults] synchronize];
604
623
 
605
624
  [self downloadUpdateOnly:downloadURL callbackId:command.callbackId];
606
625
  }
@@ -613,21 +632,36 @@ static BOOL hasPerformedInitialReload = NO;
613
632
  NSLog(@"[HotUpdates] Starting download");
614
633
 
615
634
  NSURL *url = [NSURL URLWithString:downloadURL];
635
+ if (!url) {
636
+ NSLog(@"[HotUpdates] ERROR: Invalid URL format");
637
+ isDownloadingUpdate = NO;
638
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
639
+ [[NSUserDefaults standardUserDefaults] synchronize];
640
+
641
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
642
+ messageAsDictionary:[self createError:kErrorURLRequired
643
+ message:@"Invalid URL format"]];
644
+ [self.commandDelegate sendPluginResult:result callbackId:callbackId];
645
+ return;
646
+ }
616
647
 
617
648
  NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
618
- config.timeoutIntervalForRequest = 60.0;
619
- config.timeoutIntervalForResource = 300.0;
649
+ config.timeoutIntervalForRequest = 30.0; // ТЗ: 30-60 секунд
650
+ config.timeoutIntervalForResource = 60.0; // ТЗ: максимум 60 секунд на всю загрузку
620
651
 
621
652
  NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
622
653
 
623
654
  NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url
624
655
  completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
656
+ // Инвалидируем сессию для предотвращения утечки памяти
657
+ [session finishTasksAndInvalidate];
658
+
625
659
  self->isDownloadingUpdate = NO;
626
660
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
627
661
  [[NSUserDefaults standardUserDefaults] synchronize];
628
662
 
629
663
  if (error) {
630
- NSLog(@"[HotUpdates] Download failed: %@", error.localizedDescription);
664
+ NSLog(@"[HotUpdates] ERROR: Download failed: %@", error.localizedDescription);
631
665
 
632
666
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
633
667
  messageAsDictionary:[self createError:kErrorDownloadFailed
@@ -638,7 +672,7 @@ static BOOL hasPerformedInitialReload = NO;
638
672
 
639
673
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
640
674
  if (httpResponse.statusCode != 200) {
641
- NSLog(@"[HotUpdates] Download failed: HTTP %ld", (long)httpResponse.statusCode);
675
+ NSLog(@"[HotUpdates] ERROR: HTTP %ld", (long)httpResponse.statusCode);
642
676
 
643
677
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
644
678
  messageAsDictionary:[self createError:kErrorHTTPError
@@ -647,9 +681,7 @@ static BOOL hasPerformedInitialReload = NO;
647
681
  return;
648
682
  }
649
683
 
650
- NSLog(@"[HotUpdates] Download completed successfully");
651
-
652
- // Сохраняем скачанное обновление во временную папку
684
+ NSLog(@"[HotUpdates] Download completed, verifying...");
653
685
  [self saveDownloadedUpdate:location callbackId:callbackId];
654
686
  }];
655
687
 
@@ -657,24 +689,25 @@ static BOOL hasPerformedInitialReload = NO;
657
689
  }
658
690
 
659
691
  - (void)saveDownloadedUpdate:(NSURL*)updateLocation callbackId:(NSString*)callbackId {
660
- NSLog(@"[HotUpdates] Saving downloaded update");
661
-
662
692
  NSFileManager *fileManager = [NSFileManager defaultManager];
663
693
  NSError *error;
664
694
 
695
+ // Используем временную папку для новой загрузки (не трогаем существующую temp_downloaded_update)
696
+ NSString *newDownloadPath = [documentsPath stringByAppendingPathComponent:@"temp_new_download"];
665
697
  NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
666
698
 
667
- if ([fileManager fileExistsAtPath:tempUpdatePath]) {
668
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
699
+ // Очищаем только папку для новой загрузки
700
+ if ([fileManager fileExistsAtPath:newDownloadPath]) {
701
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
669
702
  }
670
703
 
671
- [fileManager createDirectoryAtPath:tempUpdatePath
704
+ [fileManager createDirectoryAtPath:newDownloadPath
672
705
  withIntermediateDirectories:YES
673
706
  attributes:nil
674
707
  error:&error];
675
708
 
676
709
  if (error) {
677
- NSLog(@"[HotUpdates] Error creating temp directory: %@", error);
710
+ NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
678
711
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
679
712
  messageAsDictionary:[self createError:kErrorTempDirError
680
713
  message:@"Cannot create temp directory"]];
@@ -682,11 +715,10 @@ static BOOL hasPerformedInitialReload = NO;
682
715
  return;
683
716
  }
684
717
 
685
- BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:tempUpdatePath];
718
+ BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:newDownloadPath];
686
719
 
687
720
  if (!unzipSuccess) {
688
- NSLog(@"[HotUpdates] Failed to unzip update");
689
- [fileManager removeItemAtPath:tempUpdatePath error:nil];
721
+ [fileManager removeItemAtPath:newDownloadPath error:nil];
690
722
 
691
723
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
692
724
  messageAsDictionary:[self createError:kErrorExtractionFailed
@@ -695,10 +727,10 @@ static BOOL hasPerformedInitialReload = NO;
695
727
  return;
696
728
  }
697
729
 
698
- NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
699
- if (![fileManager fileExistsAtPath:tempWwwPath]) {
700
- NSLog(@"[HotUpdates] www folder not found in update package");
701
- [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];
702
734
 
703
735
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
704
736
  messageAsDictionary:[self createError:kErrorWWWNotFound
@@ -707,6 +739,12 @@ static BOOL hasPerformedInitialReload = NO;
707
739
  return;
708
740
  }
709
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
+
710
748
  // Копируем в pending_update для автоустановки при следующем запуске
711
749
  NSString *pendingPath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
712
750
 
@@ -719,18 +757,18 @@ static BOOL hasPerformedInitialReload = NO;
719
757
  error:&error];
720
758
 
721
759
  if (!copySuccess) {
722
- NSLog(@"[HotUpdates] Failed to copy to pending_update: %@", error);
723
- } else {
724
- 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);
725
761
  }
726
762
 
727
763
  isUpdateReadyToInstall = YES;
764
+ // Сохраняем URL и версию в UserDefaults только после успешной загрузки
765
+ [[NSUserDefaults standardUserDefaults] setObject:pendingUpdateURL forKey:kPendingUpdateURL];
766
+ [[NSUserDefaults standardUserDefaults] setObject:pendingUpdateVersion forKey:kPendingVersion];
728
767
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kPendingUpdateReady];
729
768
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kHasPending];
730
769
  [[NSUserDefaults standardUserDefaults] synchronize];
731
770
 
732
- NSLog(@"[HotUpdates] Update downloaded and ready to install");
733
- NSLog(@"[HotUpdates] If user ignores popup, update will install automatically on next launch");
771
+ NSLog(@"[HotUpdates] Update ready (v%@)", pendingUpdateVersion);
734
772
 
735
773
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
736
774
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
@@ -739,8 +777,6 @@ static BOOL hasPerformedInitialReload = NO;
739
777
  #pragma mark - Force Update (Install Only)
740
778
 
741
779
  - (void)forceUpdate:(CDVInvokedUrlCommand*)command {
742
- NSLog(@"[HotUpdates] forceUpdate() called - installing downloaded update");
743
-
744
780
  if (!isUpdateReadyToInstall) {
745
781
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
746
782
  messageAsDictionary:[self createError:kErrorNoUpdateReady
@@ -753,7 +789,7 @@ static BOOL hasPerformedInitialReload = NO;
753
789
  NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
754
790
 
755
791
  if (![[NSFileManager defaultManager] fileExistsAtPath:tempWwwPath]) {
756
- NSLog(@"[HotUpdates] Downloaded update files not found");
792
+ NSLog(@"[HotUpdates] ERROR: Downloaded update files not found");
757
793
 
758
794
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
759
795
  messageAsDictionary:[self createError:kErrorUpdateFilesNotFound
@@ -766,13 +802,13 @@ static BOOL hasPerformedInitialReload = NO;
766
802
  }
767
803
 
768
804
  - (void)installDownloadedUpdate:(NSString*)tempWwwPath callbackId:(NSString*)callbackId {
769
- NSLog(@"[HotUpdates] Installing update");
770
-
771
805
  NSString *versionToInstall = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
772
806
  if (!versionToInstall) {
773
807
  versionToInstall = @"unknown";
774
808
  }
775
809
 
810
+ NSLog(@"[HotUpdates] forceUpdate: installing v%@", versionToInstall);
811
+
776
812
  NSFileManager *fileManager = [NSFileManager defaultManager];
777
813
  NSError *error;
778
814
 
@@ -787,7 +823,7 @@ static BOOL hasPerformedInitialReload = NO;
787
823
  error:&error];
788
824
 
789
825
  if (!copySuccess) {
790
- NSLog(@"[HotUpdates] Failed to install update: %@", error);
826
+ NSLog(@"[HotUpdates] ERROR: Failed to install update: %@", error.localizedDescription);
791
827
 
792
828
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
793
829
  messageAsDictionary:[self createError:kErrorInstallFailed
@@ -813,37 +849,30 @@ static BOOL hasPerformedInitialReload = NO;
813
849
 
814
850
  isUpdateReadyToInstall = NO;
815
851
  pendingUpdateURL = nil;
852
+ pendingUpdateVersion = nil;
816
853
 
817
854
  NSLog(@"[HotUpdates] Update installed successfully");
855
+ NSLog(@"[HotUpdates] Starting canary timer for version %@", newVersion);
818
856
 
819
857
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
820
858
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
821
859
 
822
- // После reloadWebView pluginInitialize НЕ вызывается, поэтому canary timer запускаем вручную
823
- NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", newVersion);
824
-
825
- if (canaryTimer && [canaryTimer isValid]) {
826
- [canaryTimer invalidate];
827
- }
828
-
829
- canaryTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
830
- target:self
831
- selector:@selector(canaryTimeout)
832
- userInfo:nil
833
- repeats:NO];
834
-
860
+ [self startCanaryTimer];
835
861
  hasPerformedInitialReload = NO;
836
862
 
837
- // Очищаем кэш WebView перед перезагрузкой, иначе может загрузиться старая версия
838
- [self clearWebViewCache];
839
-
840
- [self reloadWebView];
863
+ [self clearWebViewCacheWithCompletion:^{
864
+ [self reloadWebView];
865
+ }];
841
866
  }
842
867
 
843
868
  #pragma mark - Canary
844
869
 
845
870
  - (void)canary:(CDVInvokedUrlCommand*)command {
846
- NSString *canaryVersion = [command.arguments objectAtIndex:0];
871
+ // Безопасное получение первого аргумента
872
+ NSString *canaryVersion = nil;
873
+ if (command.arguments.count > 0 && [command.arguments[0] isKindOfClass:[NSString class]]) {
874
+ canaryVersion = command.arguments[0];
875
+ }
847
876
 
848
877
  if (!canaryVersion || canaryVersion.length == 0) {
849
878
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
@@ -853,8 +882,6 @@ static BOOL hasPerformedInitialReload = NO;
853
882
  return;
854
883
  }
855
884
 
856
- NSLog(@"[HotUpdates] Canary called for version: %@", canaryVersion);
857
-
858
885
  // Сохраняем canary версию
859
886
  [[NSUserDefaults standardUserDefaults] setObject:canaryVersion forKey:kCanaryVersion];
860
887
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -863,15 +890,11 @@ static BOOL hasPerformedInitialReload = NO;
863
890
  if (canaryTimer && [canaryTimer isValid]) {
864
891
  [canaryTimer invalidate];
865
892
  canaryTimer = nil;
866
- NSLog(@"[HotUpdates] Canary timer stopped - JS confirmed bundle is working");
893
+ NSLog(@"[HotUpdates] Canary confirmed: v%@", canaryVersion);
867
894
  }
868
895
 
869
- CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
870
- messageAsDictionary:@{
871
- @"success": @YES,
872
- @"canaryVersion": canaryVersion,
873
- @"message": @"Canary version confirmed"
874
- }];
896
+ // ТЗ: при успехе callback возвращает null
897
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
875
898
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
876
899
  }
877
900
 
@@ -1,17 +1,19 @@
1
1
  /*!
2
2
  * @file HotUpdatesConstants.h
3
3
  * @brief Constants for Hot Updates Plugin
4
- * @details Defines error codes, storage keys, and directory names
5
- * @version 2.2.0
4
+ * @details Contains error codes, storage keys, and directory names used by the plugin
5
+ * @version 2.1.0
6
6
  * @date 2025-11-13
7
7
  * @author Mustafin Vladimir
8
8
  * @copyright Copyright (c) 2025. All rights reserved.
9
9
  */
10
10
 
11
- #import <Foundation/Foundation.h>
11
+ #ifndef HotUpdatesConstants_h
12
+ #define HotUpdatesConstants_h
12
13
 
13
14
  #pragma mark - Error Codes
14
15
 
16
+ // Error codes returned to JavaScript
15
17
  extern NSString * const kErrorUpdateDataRequired;
16
18
  extern NSString * const kErrorURLRequired;
17
19
  extern NSString * const kErrorDownloadInProgress;
@@ -27,6 +29,7 @@ extern NSString * const kErrorVersionRequired;
27
29
 
28
30
  #pragma mark - Storage Keys
29
31
 
32
+ // NSUserDefaults keys
30
33
  extern NSString * const kInstalledVersion;
31
34
  extern NSString * const kPendingVersion;
32
35
  extern NSString * const kHasPending;
@@ -39,7 +42,10 @@ extern NSString * const kPendingUpdateReady;
39
42
 
40
43
  #pragma mark - Directory Names
41
44
 
45
+ // Directory names
42
46
  extern NSString * const kWWWDirName;
43
47
  extern NSString * const kPreviousWWWDirName;
44
48
  extern NSString * const kBackupWWWDirName;
45
49
  extern NSString * const kPendingUpdateDirName;
50
+
51
+ #endif /* HotUpdatesConstants_h */
@@ -2,7 +2,7 @@
2
2
  * @file HotUpdatesConstants.m
3
3
  * @brief Implementation of constants for Hot Updates Plugin
4
4
  * @details Defines all constant values used throughout the plugin
5
- * @version 2.2.0
5
+ * @version 2.1.0
6
6
  * @date 2025-11-13
7
7
  * @author Mustafin Vladimir
8
8
  * @copyright Copyright (c) 2025. All rights reserved.
package/www/HotUpdates.js CHANGED
@@ -1,73 +1,80 @@
1
1
  var exec = require('cordova/exec');
2
2
 
3
3
  /**
4
- * Cordova Hot Updates Plugin v2.1.2
4
+ * Cordova Hot Updates Plugin v2.2.1
5
5
  * Frontend-controlled manual hot updates for iOS
6
6
  *
7
- * Provides manual over-the-air (OTA) updates for Cordova applications
8
- * using the WebView Reload approach. All update decisions are controlled
9
- * by JavaScript - the native plugin only executes commands.
10
- *
11
- * Features:
12
- * - Frontend-controlled manual updates (no automatic checking)
13
- * - Two-step update flow: getUpdate() downloads, forceUpdate() installs
14
- * - Automatic rollback with 20-second canary timer
15
- * - IgnoreList system for tracking problematic versions (information only)
16
- * - Auto-install pending updates on next app launch
17
- * - WebView reload approach for instant updates without app restart
18
- * - No App Store approval required for web content updates
19
- *
20
- * @version 2.1.2
7
+ * @version 2.2.1
21
8
  * @author Mustafin Vladimir
22
9
  */
10
+
11
+ /**
12
+ * Error codes returned by the plugin
13
+ * @readonly
14
+ * @enum {string}
15
+ */
16
+ var ErrorCodes = {
17
+ // getUpdate errors
18
+ UPDATE_DATA_REQUIRED: 'UPDATE_DATA_REQUIRED',
19
+ URL_REQUIRED: 'URL_REQUIRED',
20
+ DOWNLOAD_IN_PROGRESS: 'DOWNLOAD_IN_PROGRESS',
21
+ DOWNLOAD_FAILED: 'DOWNLOAD_FAILED',
22
+ HTTP_ERROR: 'HTTP_ERROR',
23
+ TEMP_DIR_ERROR: 'TEMP_DIR_ERROR',
24
+ EXTRACTION_FAILED: 'EXTRACTION_FAILED',
25
+ WWW_NOT_FOUND: 'WWW_NOT_FOUND',
26
+ // forceUpdate errors
27
+ NO_UPDATE_READY: 'NO_UPDATE_READY',
28
+ UPDATE_FILES_NOT_FOUND: 'UPDATE_FILES_NOT_FOUND',
29
+ INSTALL_FAILED: 'INSTALL_FAILED',
30
+ // canary errors
31
+ VERSION_REQUIRED: 'VERSION_REQUIRED'
32
+ };
33
+
23
34
  var HotUpdates = {
24
35
 
36
+ /**
37
+ * Error codes enum
38
+ * @type {Object}
39
+ */
40
+ ErrorCodes: ErrorCodes,
41
+
25
42
  /**
26
43
  * Download update from server
27
44
  *
28
- * Downloads update ZIP archive from the provided URL and saves to two locations:
29
- * - temp_downloaded_update (for immediate installation via forceUpdate)
30
- * - pending_update (for auto-installation on next app launch)
31
- *
32
- * If the specified version is already downloaded, returns success without re-downloading.
33
- * Does NOT check ignoreList - JavaScript controls all installation decisions.
34
- *
35
45
  * @param {Object} options - Update options
36
46
  * @param {string} options.url - URL to download ZIP archive (required)
37
47
  * @param {string} [options.version] - Version string (optional)
38
- * @param {Function} callback - Callback function
39
- * - Called with null on success
40
- * - Called with {error: {message?: string}} on error
48
+ * @param {Function} callback - Callback(error)
49
+ * - null on success
50
+ * - {error: {code: string, message: string}} on error
41
51
  *
42
52
  * @example
43
- * window.hotUpdate.getUpdate({
44
- * url: 'https://your-server.com/updates/2.0.0.zip',
45
- * version: '2.0.0'
46
- * }, function(error) {
47
- * if (error) {
48
- * console.error('Download failed:', error);
49
- * } else {
50
- * console.log('Update downloaded successfully');
51
- * // Can now call forceUpdate() to install immediately
52
- * // Or user can ignore and it will auto-install on next launch
53
- * }
53
+ * hotUpdate.getUpdate({url: 'https://server.com/update.zip', version: '2.0.0'}, function(err) {
54
+ * if (err) console.error(err.error.code, err.error.message);
55
+ * else console.log('Downloaded');
54
56
  * });
55
57
  */
56
58
  getUpdate: function(options, callback) {
57
- if (!options || !options.url) {
59
+ if (!options) {
60
+ if (callback) {
61
+ callback({error: {code: ErrorCodes.UPDATE_DATA_REQUIRED, message: 'Update data required'}});
62
+ }
63
+ return;
64
+ }
65
+
66
+ if (!options.url) {
58
67
  if (callback) {
59
- callback({error: {message: 'URL is required'}});
68
+ callback({error: {code: ErrorCodes.URL_REQUIRED, message: 'URL is required'}});
60
69
  }
61
70
  return;
62
71
  }
63
72
 
64
73
  exec(
65
74
  function() {
66
- // Success
67
75
  if (callback) callback(null);
68
76
  },
69
77
  function(error) {
70
- // Error
71
78
  if (callback) callback({error: error});
72
79
  },
73
80
  'HotUpdates',
@@ -79,42 +86,22 @@ var HotUpdates = {
79
86
  /**
80
87
  * Install downloaded update immediately
81
88
  *
82
- * Installs the update that was downloaded via getUpdate().
83
- * This will:
84
- * 1. Backup current version to www_previous
85
- * 2. Copy downloaded update to Documents/www
86
- * 3. Clear WebView cache (disk, memory, Service Worker)
87
- * 4. Reload WebView
88
- * 5. Start 20-second canary timer
89
- *
90
- * IMPORTANT: JavaScript MUST call canary(version) within 20 seconds
91
- * after reload to confirm successful bundle load. Otherwise automatic
92
- * rollback will occur.
93
- *
94
- * Does NOT check ignoreList - JavaScript decides what to install.
95
- *
96
- * @param {Function} callback - Callback function
97
- * - Called with null on success (before WebView reload)
98
- * - Called with {error: {message?: string}} on error
89
+ * @param {Function} callback - Callback(error)
90
+ * - null on success (WebView will reload)
91
+ * - {error: {code: string, message: string}} on error
99
92
  *
100
93
  * @example
101
- * window.hotUpdate.forceUpdate(function(error) {
102
- * if (error) {
103
- * console.error('Install failed:', error);
104
- * } else {
105
- * console.log('Update installing, WebView will reload...');
106
- * // After reload, MUST call canary() within 20 seconds!
107
- * }
94
+ * hotUpdate.forceUpdate(function(err) {
95
+ * if (err) console.error(err.error.code);
96
+ * // WebView reloads, call canary() within 20 sec
108
97
  * });
109
98
  */
110
99
  forceUpdate: function(callback) {
111
100
  exec(
112
101
  function() {
113
- // Success
114
102
  if (callback) callback(null);
115
103
  },
116
104
  function(error) {
117
- // Error
118
105
  if (callback) callback({error: error});
119
106
  },
120
107
  'HotUpdates',
@@ -124,36 +111,30 @@ var HotUpdates = {
124
111
  },
125
112
 
126
113
  /**
127
- * Confirm successful bundle load (canary check)
128
- *
129
- * MUST be called within 20 seconds after forceUpdate() to confirm
130
- * that the new bundle loaded successfully. This stops the canary timer
131
- * and prevents automatic rollback.
132
- *
133
- * If not called within 20 seconds:
134
- * - Automatic rollback to previous version
135
- * - Failed version added to ignoreList
136
- * - WebView reloaded with previous version
137
- *
138
- * Call this immediately after your app initialization completes.
114
+ * Confirm successful bundle load (MUST call within 20 sec after forceUpdate)
139
115
  *
140
- * @param {string} version - Version that loaded successfully
141
- * @param {Function} [callback] - Optional callback (not used, method is synchronous)
116
+ * @param {string} version - Current version
117
+ * @param {Function} [callback] - Optional callback
142
118
  *
143
119
  * @example
144
- * // Call as early as possible after app loads
145
120
  * document.addEventListener('deviceready', function() {
146
- * window.hotUpdate.canary('2.0.0');
147
- * console.log('Canary confirmed, update successful');
148
- * }, false);
121
+ * hotUpdate.canary('2.0.0');
122
+ * });
149
123
  */
150
124
  canary: function(version, callback) {
125
+ if (!version) {
126
+ if (callback) {
127
+ callback({error: {code: ErrorCodes.VERSION_REQUIRED, message: 'Version is required'}});
128
+ }
129
+ return;
130
+ }
131
+
151
132
  exec(
152
133
  function() {
153
- if (callback) callback();
134
+ if (callback) callback(null);
154
135
  },
155
- function() {
156
- if (callback) callback();
136
+ function(error) {
137
+ if (callback) callback({error: error});
157
138
  },
158
139
  'HotUpdates',
159
140
  'canary',
@@ -162,50 +143,62 @@ var HotUpdates = {
162
143
  },
163
144
 
164
145
  /**
165
- * Get list of problematic versions (information only)
146
+ * Get list of problematic versions
166
147
  *
167
- * Returns array of version strings that failed to load (triggered rollback).
168
- * This is an INFORMATION-ONLY system - native does NOT block installation
169
- * of versions in this list.
170
- *
171
- * JavaScript should read this list and decide whether to skip downloading/
172
- * installing these versions. If JS decides to install a version from the
173
- * ignoreList, that's allowed (per TS requirements).
174
- *
175
- * Native automatically adds versions to this list when rollback occurs.
176
- * JavaScript cannot modify the list (no add/remove/clear methods per TS v2.1.0).
177
- *
178
- * @param {Function} callback - Callback function
179
- * - Called with {versions: string[]} - Array of problematic version strings
148
+ * @param {Function} callback - Callback({versions: string[]})
180
149
  *
181
150
  * @example
182
- * window.hotUpdate.getIgnoreList(function(result) {
183
- * console.log('Problematic versions:', result.versions);
184
- * // Example: {versions: ['1.9.0', '2.0.1']}
185
- *
186
- * // JavaScript decides what to do with this information
187
- * var shouldSkip = result.versions.includes(availableVersion);
188
- * if (shouldSkip) {
189
- * console.log('Skipping known problematic version');
190
- * } else {
191
- * // Download and install
151
+ * hotUpdate.getIgnoreList(function(result) {
152
+ * if (result.versions.includes(newVersion)) {
153
+ * console.log('Version is blacklisted');
192
154
  * }
193
155
  * });
194
156
  */
195
157
  getIgnoreList: function(callback) {
196
158
  exec(
197
- function(versions) {
198
- // Success - native returns array of version strings
199
- if (callback) callback({versions: versions || []});
159
+ function(result) {
160
+ if (callback) callback(result || {versions: []});
200
161
  },
201
- function(error) {
202
- // Error - return empty list
162
+ function() {
203
163
  if (callback) callback({versions: []});
204
164
  },
205
165
  'HotUpdates',
206
166
  'getIgnoreList',
207
167
  []
208
168
  );
169
+ },
170
+
171
+ /**
172
+ * Get version info (debug method)
173
+ *
174
+ * @param {Function} callback - Callback with version info
175
+ * {
176
+ * appBundleVersion: string,
177
+ * installedVersion: string|null,
178
+ * previousVersion: string|null,
179
+ * canaryVersion: string|null,
180
+ * pendingVersion: string|null,
181
+ * hasPendingUpdate: boolean,
182
+ * ignoreList: string[]
183
+ * }
184
+ *
185
+ * @example
186
+ * hotUpdate.getVersionInfo(function(info) {
187
+ * console.log('Current:', info.installedVersion || info.appBundleVersion);
188
+ * });
189
+ */
190
+ getVersionInfo: function(callback) {
191
+ exec(
192
+ function(info) {
193
+ if (callback) callback(info);
194
+ },
195
+ function(error) {
196
+ if (callback) callback({error: error});
197
+ },
198
+ 'HotUpdates',
199
+ 'getVersionInfo',
200
+ []
201
+ );
209
202
  }
210
203
  };
211
204