cordova-plugin-hot-updates 2.1.2 → 2.2.1

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.
@@ -13,7 +13,7 @@
13
13
  * - Auto-install pending updates on next app launch
14
14
  *
15
15
  * @version 2.1.2
16
- * @date 2025-11-13
16
+ * @date 2025-11-26
17
17
  * @author Mustafin Vladimir
18
18
  * @copyright Copyright (c) 2025. All rights reserved.
19
19
  */
@@ -21,27 +21,10 @@
21
21
  #import <Cordova/CDV.h>
22
22
  #import <Cordova/CDVViewController.h>
23
23
  #import "HotUpdates.h"
24
+ #import "HotUpdates+Helpers.h"
25
+ #import "HotUpdatesConstants.h"
24
26
  #import <SSZipArchive/SSZipArchive.h>
25
27
 
26
- // Storage keys
27
- static NSString * const kInstalledVersion = @"hot_updates_installed_version";
28
- static NSString * const kPendingVersion = @"hot_updates_pending_version";
29
- static NSString * const kHasPending = @"hot_updates_has_pending";
30
- static NSString * const kPreviousVersion = @"hot_updates_previous_version";
31
- static NSString * const kIgnoreList = @"hot_updates_ignore_list";
32
- static NSString * const kCanaryVersion = @"hot_updates_canary_version";
33
- static NSString * const kDownloadInProgress = @"hot_updates_download_in_progress";
34
-
35
- // Constants for v2.1.0
36
- static NSString * const kPendingUpdateURL = @"hot_updates_pending_update_url";
37
- static NSString * const kPendingUpdateReady = @"hot_updates_pending_ready";
38
-
39
- // Directory names
40
- static NSString * const kWWWDirName = @"www";
41
- static NSString * const kPreviousWWWDirName = @"www_previous";
42
- static NSString * const kBackupWWWDirName = @"www_backup";
43
- static NSString * const kPendingUpdateDirName = @"pending_update";
44
-
45
28
  // Флаг для предотвращения повторных перезагрузок при навигации внутри WebView
46
29
  static BOOL hasPerformedInitialReload = NO;
47
30
 
@@ -65,24 +48,22 @@ static BOOL hasPerformedInitialReload = NO;
65
48
  - (void)pluginInitialize {
66
49
  [super pluginInitialize];
67
50
 
68
- // Получаем пути к директориям
69
51
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
70
52
  documentsPath = [paths objectAtIndex:0];
71
53
  wwwPath = [documentsPath stringByAppendingPathComponent:kWWWDirName];
72
54
  previousVersionPath = [documentsPath stringByAppendingPathComponent:kPreviousWWWDirName];
73
55
 
74
- // Загружаем конфигурацию
75
56
  [self loadConfiguration];
76
-
77
- // Загрузка ignoreList
78
57
  [self loadIgnoreList];
79
58
 
80
- // Инициализация переменных
59
+ // Сбрасываем флаг загрузки (если приложение было убито во время загрузки)
60
+ isDownloadingUpdate = NO;
61
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
62
+
81
63
  isUpdateReadyToInstall = NO;
82
64
  pendingUpdateURL = nil;
83
65
  pendingUpdateVersion = nil;
84
66
 
85
- // Загружаем информацию о pending update если есть
86
67
  pendingUpdateURL = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingUpdateURL];
87
68
  isUpdateReadyToInstall = [[NSUserDefaults standardUserDefaults] boolForKey:kPendingUpdateReady];
88
69
  if (isUpdateReadyToInstall) {
@@ -95,29 +76,18 @@ static BOOL hasPerformedInitialReload = NO;
95
76
  NSLog(@"[HotUpdates] Documents www path: %@", wwwPath);
96
77
  NSLog(@"[HotUpdates] Ignore list: %@", ignoreList);
97
78
 
98
- // 1. Проверяем и устанавливаем pending updates
99
79
  [self checkAndInstallPendingUpdate];
100
-
101
- // 2. Создаем папку www если её нет (копируем из bundle)
102
80
  [self initializeWWWFolder];
103
-
104
- // 3. Переключаем WebView на обновленный контент и перезагружаем
105
81
  [self switchToUpdatedContentWithReload];
106
82
 
107
- // 4. Запускаем canary timer на 20 секунд
108
83
  NSString *currentVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
109
84
  if (currentVersion) {
110
85
  NSString *canaryVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kCanaryVersion];
111
86
 
112
- // Если canary еще не был вызван для текущей версии
113
87
  if (!canaryVersion || ![canaryVersion isEqualToString:currentVersion]) {
114
88
  NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", currentVersion);
115
89
 
116
- canaryTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
117
- target:self
118
- selector:@selector(canaryTimeout)
119
- userInfo:nil
120
- repeats:NO];
90
+ [self startCanaryTimer];
121
91
  } else {
122
92
  NSLog(@"[HotUpdates] Canary already confirmed for version %@", currentVersion);
123
93
  }
@@ -127,7 +97,6 @@ static BOOL hasPerformedInitialReload = NO;
127
97
  }
128
98
 
129
99
  - (void)loadConfiguration {
130
- // Получаем версию bundle приложения
131
100
  appBundleVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
132
101
  if (!appBundleVersion) {
133
102
  appBundleVersion = @"1.0.0";
@@ -144,16 +113,10 @@ static BOOL hasPerformedInitialReload = NO;
144
113
  - (void)checkAndInstallPendingUpdate {
145
114
  BOOL hasPendingUpdate = [[NSUserDefaults standardUserDefaults] boolForKey:kHasPending];
146
115
  NSString *pendingVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
147
- NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
148
116
 
149
- // ТЗ п.7: Если пользователь проигнорировал попап, обновление устанавливается автоматически при следующем запуске
150
117
  if (hasPendingUpdate && pendingVersion) {
151
- // ВАЖНО (ТЗ): НЕ проверяем ignoreList - JS сам решил загрузить эту версию
152
- // Если версия скачана (через getUpdate), значит JS одобрил её установку
153
-
154
118
  NSLog(@"[HotUpdates] Installing pending update %@ to Documents/www (auto-install on launch)", pendingVersion);
155
119
 
156
- // НОВОЕ: Создаем резервную копию текущей версии перед установкой
157
120
  [self backupCurrentVersion];
158
121
 
159
122
  NSString *pendingUpdatePath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
@@ -161,24 +124,20 @@ static BOOL hasPerformedInitialReload = NO;
161
124
  NSString *documentsWwwPath = [documentsPath stringByAppendingPathComponent:kWWWDirName];
162
125
 
163
126
  if ([[NSFileManager defaultManager] fileExistsAtPath:pendingWwwPath]) {
164
- // Удаляем старую Documents/www
165
127
  if ([[NSFileManager defaultManager] fileExistsAtPath:documentsWwwPath]) {
166
128
  [[NSFileManager defaultManager] removeItemAtPath:documentsWwwPath error:nil];
167
129
  }
168
130
 
169
- // Копируем pending_update/www в Documents/www
170
131
  NSError *copyError = nil;
171
132
  BOOL copySuccess = [[NSFileManager defaultManager] copyItemAtPath:pendingWwwPath toPath:documentsWwwPath error:&copyError];
172
133
 
173
134
  if (copySuccess) {
174
- // Помечаем как установленный
175
135
  [[NSUserDefaults standardUserDefaults] setObject:pendingVersion forKey:kInstalledVersion];
176
136
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kHasPending];
177
137
  [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPendingVersion];
178
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCanaryVersion]; // Сбрасываем canary для новой версии
138
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCanaryVersion];
179
139
  [[NSUserDefaults standardUserDefaults] synchronize];
180
140
 
181
- // Очищаем pending_update папку
182
141
  [[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
183
142
 
184
143
  NSLog(@"[HotUpdates] Update %@ installed successfully (canary timer will start)", pendingVersion);
@@ -195,7 +154,7 @@ static BOOL hasPerformedInitialReload = NO;
195
154
  * Uses static flag to prevent reload on every page navigation (only once per app launch)
196
155
  */
197
156
  - (void)switchToUpdatedContentWithReload {
198
- // Предотвращаем повторные перезагрузки при навигации между страницами (например, admin → index.html)
157
+ // Предотвращаем повторные перезагрузки при навигации между страницами
199
158
  if (hasPerformedInitialReload) {
200
159
  NSLog(@"[HotUpdates] Initial reload already performed, skipping");
201
160
  return;
@@ -207,37 +166,42 @@ static BOOL hasPerformedInitialReload = NO;
207
166
  NSString *documentsWwwPath = [documentsPath stringByAppendingPathComponent:kWWWDirName];
208
167
  NSString *indexPath = [documentsWwwPath stringByAppendingPathComponent:@"index.html"];
209
168
 
210
- // Проверяем, что файлы действительно существуют
211
169
  if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath]) {
212
170
  NSLog(@"[HotUpdates] Using WebView reload approach");
213
171
  NSLog(@"[HotUpdates] Found installed update version: %@", installedVersion);
214
172
 
215
- // Устанавливаем новый путь
216
173
  ((CDVViewController *)self.viewController).wwwFolderName = documentsWwwPath;
217
174
  NSLog(@"[HotUpdates] Changed wwwFolderName to: %@", documentsWwwPath);
218
175
 
219
- // Принудительно перезагружаем WebView для применения нового пути
220
- [self reloadWebView];
221
-
222
- // Устанавливаем флаг, чтобы больше не перезагружать при навигации
223
176
  hasPerformedInitialReload = YES;
224
177
 
225
- NSLog(@"[HotUpdates] WebView reloaded with updated content (version: %@)", installedVersion);
178
+ // Очищаем кэш перед перезагрузкой, иначе может загрузиться старая версия
179
+ [self clearWebViewCacheWithCompletion:^{
180
+ [self reloadWebView];
181
+ NSLog(@"[HotUpdates] WebView reloaded with updated content (version: %@)", installedVersion);
182
+ }];
226
183
  } else {
227
184
  NSLog(@"[HotUpdates] Documents/www/index.html not found, keeping bundle www");
228
185
  }
229
186
  } else {
230
187
  NSLog(@"[HotUpdates] No installed updates, using bundle www");
231
- // Устанавливаем флаг даже если нет обновлений, чтобы не проверять постоянно
232
188
  hasPerformedInitialReload = YES;
233
189
  }
234
190
  }
235
191
 
236
192
  /*!
237
- * @brief Force reload the WebView
238
- * @details Uses WKWebView loadFileURL with proper sandbox permissions
193
+ * @brief Clear WebView cache
194
+ * @details Clears disk cache, memory cache, offline storage and service workers
239
195
  */
240
196
  - (void)clearWebViewCache {
197
+ [self clearWebViewCacheWithCompletion:nil];
198
+ }
199
+
200
+ /*!
201
+ * @brief Clear WebView cache with completion handler
202
+ * @param completion Block called after cache is cleared (on main thread)
203
+ */
204
+ - (void)clearWebViewCacheWithCompletion:(void (^)(void))completion {
241
205
  NSLog(@"[HotUpdates] Clearing WebView cache");
242
206
 
243
207
  NSSet *websiteDataTypes = [NSSet setWithArray:@[
@@ -252,15 +216,16 @@ static BOOL hasPerformedInitialReload = NO;
252
216
  modifiedSince:dateFrom
253
217
  completionHandler:^{
254
218
  NSLog(@"[HotUpdates] WebView cache cleared");
219
+ if (completion) {
220
+ dispatch_async(dispatch_get_main_queue(), completion);
221
+ }
255
222
  }];
256
223
  }
257
224
 
258
225
  - (void)reloadWebView {
259
- // Приводим к типу CDVViewController для доступа к webViewEngine
260
226
  if ([self.viewController isKindOfClass:[CDVViewController class]]) {
261
227
  CDVViewController *cdvViewController = (CDVViewController *)self.viewController;
262
228
 
263
- // Строим новый URL для обновленного контента
264
229
  NSString *documentsWwwPath = [documentsPath stringByAppendingPathComponent:kWWWDirName];
265
230
  NSString *indexPath = [documentsWwwPath stringByAppendingPathComponent:@"index.html"];
266
231
  NSURL *fileURL = [NSURL fileURLWithPath:indexPath];
@@ -270,13 +235,11 @@ static BOOL hasPerformedInitialReload = NO;
270
235
 
271
236
  id webViewEngine = cdvViewController.webViewEngine;
272
237
  if (webViewEngine && [webViewEngine respondsToSelector:@selector(engineWebView)]) {
273
- // Получаем WKWebView
274
238
  WKWebView *webView = [webViewEngine performSelector:@selector(engineWebView)];
275
239
 
276
240
  if (webView && [webView isKindOfClass:[WKWebView class]]) {
277
- // Используем loadFileURL:allowingReadAccessToURL: для правильных sandbox permissions
241
+ // loadFileURL:allowingReadAccessToURL: правильно настраивает sandbox permissions для локальных файлов
278
242
  dispatch_async(dispatch_get_main_queue(), ^{
279
- // Этот метод правильно настраивает sandbox для локальных файлов
280
243
  [webView loadFileURL:fileURL allowingReadAccessToURL:allowReadAccessToURL];
281
244
  NSLog(@"[HotUpdates] WebView loadFileURL executed with sandbox permissions");
282
245
  });
@@ -294,12 +257,10 @@ static BOOL hasPerformedInitialReload = NO;
294
257
 
295
258
  - (void)initializeWWWFolder {
296
259
  NSFileManager *fileManager = [NSFileManager defaultManager];
297
-
298
- // Проверяем, существует ли папка www в Documents
260
+
299
261
  if (![fileManager fileExistsAtPath:wwwPath]) {
300
262
  NSLog(@"[HotUpdates] WWW folder not found in Documents. Creating and copying from bundle...");
301
-
302
- // Копируем содержимое www из bundle в Documents
263
+
303
264
  NSString *bundleWWWPath = [[NSBundle mainBundle] pathForResource:@"www" ofType:nil];
304
265
  if (bundleWWWPath) {
305
266
  NSError *error;
@@ -323,7 +284,6 @@ static BOOL hasPerformedInitialReload = NO;
323
284
  NSFileManager *fileManager = [NSFileManager defaultManager];
324
285
  NSError *error = nil;
325
286
 
326
- // Создаем папку назначения если её нет
327
287
  if (![fileManager fileExistsAtPath:destination]) {
328
288
  [fileManager createDirectoryAtPath:destination withIntermediateDirectories:YES attributes:nil error:&error];
329
289
  if (error) {
@@ -332,24 +292,19 @@ static BOOL hasPerformedInitialReload = NO;
332
292
  }
333
293
  }
334
294
 
335
- // Распаковка ZIP архива с SSZipArchive
336
295
  NSLog(@"[HotUpdates] Extracting ZIP archive using SSZipArchive library");
337
296
 
338
- // Простая проверка файла
339
297
  if (![[NSFileManager defaultManager] fileExistsAtPath:zipPath]) {
340
298
  NSLog(@"[HotUpdates] ZIP file does not exist: %@", zipPath);
341
299
  return NO;
342
300
  }
343
-
344
- // Создаем временную папку для распаковки
301
+
345
302
  NSString *tempExtractPath = [destination stringByAppendingPathComponent:@"temp_extract"];
346
-
347
- // Удаляем существующую временную папку
303
+
348
304
  if ([[NSFileManager defaultManager] fileExistsAtPath:tempExtractPath]) {
349
305
  [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
350
306
  }
351
-
352
- // Создаем временную папку
307
+
353
308
  if (![[NSFileManager defaultManager] createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
354
309
  NSLog(@"[HotUpdates] Failed to create temp extraction folder: %@", error.localizedDescription);
355
310
  return NO;
@@ -357,17 +312,15 @@ static BOOL hasPerformedInitialReload = NO;
357
312
 
358
313
  NSLog(@"[HotUpdates] Extracting to temp location: %@", tempExtractPath);
359
314
 
360
- // Распаковываем ZIP архив
361
315
  BOOL extractSuccess = [SSZipArchive unzipFileAtPath:zipPath toDestination:tempExtractPath];
362
316
 
363
317
  if (extractSuccess) {
364
318
  NSLog(@"[HotUpdates] ZIP extraction successful");
365
319
 
366
- // Проверяем содержимое распакованного архива
367
320
  NSArray *extractedContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tempExtractPath error:nil];
368
321
  NSLog(@"[HotUpdates] Extracted contents: %@", extractedContents);
369
-
370
- // Ищем папку www в распакованном содержимом
322
+
323
+ // Ищем папку www (может быть вложенной)
371
324
  NSString *wwwSourcePath = nil;
372
325
  for (NSString *item in extractedContents) {
373
326
  NSString *itemPath = [tempExtractPath stringByAppendingPathComponent:item];
@@ -377,7 +330,6 @@ static BOOL hasPerformedInitialReload = NO;
377
330
  wwwSourcePath = itemPath;
378
331
  break;
379
332
  }
380
- // Проверяем, есть ли www внутри папки
381
333
  NSString *nestedWwwPath = [itemPath stringByAppendingPathComponent:@"www"];
382
334
  if ([[NSFileManager defaultManager] fileExistsAtPath:nestedWwwPath]) {
383
335
  wwwSourcePath = nestedWwwPath;
@@ -385,26 +337,22 @@ static BOOL hasPerformedInitialReload = NO;
385
337
  }
386
338
  }
387
339
  }
388
-
340
+
389
341
  if (wwwSourcePath) {
390
342
  NSLog(@"[HotUpdates] Found www folder at: %@", wwwSourcePath);
391
343
 
392
- // Копируем www папку в финальное место
393
344
  NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
394
345
 
395
- // Удаляем существующую папку www если есть
396
346
  if ([[NSFileManager defaultManager] fileExistsAtPath:finalWwwPath]) {
397
347
  [[NSFileManager defaultManager] removeItemAtPath:finalWwwPath error:nil];
398
348
  }
399
349
 
400
- // Копируем новую www папку
401
350
  NSError *copyError = nil;
402
351
  BOOL copySuccess = [[NSFileManager defaultManager] copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:&copyError];
403
352
 
404
353
  if (copySuccess) {
405
354
  NSLog(@"[HotUpdates] www folder copied successfully to: %@", finalWwwPath);
406
355
 
407
- // Очищаем временную папку
408
356
  [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
409
357
 
410
358
  NSLog(@"[HotUpdates] ZIP extraction completed successfully");
@@ -417,11 +365,9 @@ static BOOL hasPerformedInitialReload = NO;
417
365
  NSLog(@"[HotUpdates] Available contents: %@", extractedContents);
418
366
  }
419
367
 
420
- // Очищаем временную папку при ошибке
421
368
  [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
422
369
  } else {
423
370
  NSLog(@"[HotUpdates] Failed to extract ZIP archive");
424
- // Очищаем временную папку при ошибке
425
371
  [[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
426
372
  }
427
373
 
@@ -463,6 +409,28 @@ static BOOL hasPerformedInitialReload = NO;
463
409
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
464
410
  }
465
411
 
412
+ #pragma mark - Canary Timer
413
+
414
+ /*!
415
+ * @brief Start canary timer with weak self to prevent retain cycle
416
+ * @details Uses block-based timer (iOS 10+) with weak reference
417
+ */
418
+ - (void)startCanaryTimer {
419
+ // Инвалидируем предыдущий таймер если есть
420
+ if (canaryTimer && [canaryTimer isValid]) {
421
+ [canaryTimer invalidate];
422
+ canaryTimer = nil;
423
+ }
424
+
425
+ // Используем weak self для предотвращения retain cycle
426
+ __weak __typeof__(self) weakSelf = self;
427
+ canaryTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
428
+ repeats:NO
429
+ block:^(NSTimer * _Nonnull timer) {
430
+ [weakSelf canaryTimeout];
431
+ }];
432
+ }
433
+
466
434
  #pragma mark - Canary Timeout Handler
467
435
 
468
436
  - (void)canaryTimeout {
@@ -471,7 +439,6 @@ static BOOL hasPerformedInitialReload = NO;
471
439
  NSString *currentVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
472
440
  NSString *previousVersion = [self getPreviousVersion];
473
441
 
474
- // Если некуда откатываться (свежая установка из Store) - ничего не делаем
475
442
  if (!previousVersion || previousVersion.length == 0) {
476
443
  NSLog(@"[HotUpdates] Fresh install from Store, rollback not possible");
477
444
  return;
@@ -479,23 +446,17 @@ static BOOL hasPerformedInitialReload = NO;
479
446
 
480
447
  NSLog(@"[HotUpdates] Version %@ considered faulty, performing rollback", currentVersion);
481
448
 
482
- // Добавляем в ignoreList
483
- if (currentVersion) {
484
- [self addVersionToIgnoreList:currentVersion];
485
- }
486
-
487
- // Выполняем rollback
449
+ // Примечание: версия добавляется в ignoreList внутри rollbackToPreviousVersion
488
450
  BOOL rollbackSuccess = [self rollbackToPreviousVersion];
489
451
 
490
452
  if (rollbackSuccess) {
491
453
  NSLog(@"[HotUpdates] Automatic rollback completed successfully");
492
454
 
493
- // Сбрасываем флаг для разрешения перезагрузки после rollback
494
455
  hasPerformedInitialReload = NO;
495
456
 
496
- // Очищаем кэш и перезагружаем WebView
497
- [self clearWebViewCache];
498
- [self reloadWebView];
457
+ [self clearWebViewCacheWithCompletion:^{
458
+ [self reloadWebView];
459
+ }];
499
460
  } else {
500
461
  NSLog(@"[HotUpdates] Automatic rollback failed");
501
462
  }
@@ -549,26 +510,23 @@ static BOOL hasPerformedInitialReload = NO;
549
510
 
550
511
  NSLog(@"[HotUpdates] Rollback: %@ -> %@", currentVersion ?: @"bundle", previousVersion ?: @"nil");
551
512
 
552
- // Проверка: нет previousVersion
553
513
  if (!previousVersion || previousVersion.length == 0) {
554
514
  NSLog(@"[HotUpdates] Rollback failed: no previous version");
555
515
  return NO;
556
516
  }
557
517
 
558
- // Проверка: папка не существует
559
518
  if (![fileManager fileExistsAtPath:previousVersionPath]) {
560
519
  NSLog(@"[HotUpdates] Rollback failed: previous version folder not found");
561
520
  return NO;
562
521
  }
563
522
 
564
- // Проверка: previous = current (защита от цикла)
523
+ // Защита от цикла rollback
565
524
  NSString *effectiveCurrentVersion = currentVersion ?: appBundleVersion;
566
525
  if ([previousVersion isEqualToString:effectiveCurrentVersion]) {
567
526
  NSLog(@"[HotUpdates] Rollback failed: cannot rollback to same version");
568
527
  return NO;
569
528
  }
570
529
 
571
- // Создаем временную резервную копию текущей
572
530
  NSString *tempBackupPath = [documentsPath stringByAppendingPathComponent:kBackupWWWDirName];
573
531
  if ([fileManager fileExistsAtPath:tempBackupPath]) {
574
532
  [fileManager removeItemAtPath:tempBackupPath error:nil];
@@ -576,7 +534,6 @@ static BOOL hasPerformedInitialReload = NO;
576
534
 
577
535
  NSError *error = nil;
578
536
 
579
- // Бэкапим текущую версию
580
537
  if ([fileManager fileExistsAtPath:wwwPath]) {
581
538
  [fileManager moveItemAtPath:wwwPath toPath:tempBackupPath error:&error];
582
539
  if (error) {
@@ -585,23 +542,19 @@ static BOOL hasPerformedInitialReload = NO;
585
542
  }
586
543
  }
587
544
 
588
- // Копируем предыдущую версию
589
545
  BOOL success = [fileManager copyItemAtPath:previousVersionPath
590
546
  toPath:wwwPath
591
547
  error:&error];
592
548
 
593
549
  if (success) {
594
- // Обновляем метаданные (previousVersion очищается для предотвращения циклов)
595
550
  [[NSUserDefaults standardUserDefaults] setObject:previousVersion forKey:kInstalledVersion];
596
551
  [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPreviousVersion];
597
552
  [[NSUserDefaults standardUserDefaults] synchronize];
598
553
 
599
- // Очищаем временный бэкап
600
554
  [fileManager removeItemAtPath:tempBackupPath error:nil];
601
555
 
602
556
  NSLog(@"[HotUpdates] Rollback successful: %@ -> %@", currentVersion, previousVersion);
603
557
 
604
- // Добавляем проблемную версию в ignoreList
605
558
  if (currentVersion) {
606
559
  [self addVersionToIgnoreList:currentVersion];
607
560
  }
@@ -610,7 +563,6 @@ static BOOL hasPerformedInitialReload = NO;
610
563
  } else {
611
564
  NSLog(@"[HotUpdates] Rollback failed: %@", error.localizedDescription);
612
565
 
613
- // Восстанавливаем текущую версию
614
566
  if ([fileManager fileExistsAtPath:tempBackupPath]) {
615
567
  [fileManager moveItemAtPath:tempBackupPath toPath:wwwPath error:nil];
616
568
  }
@@ -622,15 +574,16 @@ static BOOL hasPerformedInitialReload = NO;
622
574
  #pragma mark - Get Update (Download Only)
623
575
 
624
576
  - (void)getUpdate:(CDVInvokedUrlCommand*)command {
625
- NSDictionary *updateData = [command.arguments objectAtIndex:0];
577
+ // Безопасное получение первого аргумента
578
+ NSDictionary *updateData = nil;
579
+ if (command.arguments.count > 0 && [command.arguments[0] isKindOfClass:[NSDictionary class]]) {
580
+ updateData = command.arguments[0];
581
+ }
626
582
 
627
583
  if (!updateData) {
628
584
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
629
- messageAsDictionary:@{
630
- @"error": @{
631
- @"message": @"Update data required"
632
- }
633
- }];
585
+ messageAsDictionary:[self createError:kErrorUpdateDataRequired
586
+ message:@"Update data required"]];
634
587
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
635
588
  return;
636
589
  }
@@ -639,16 +592,12 @@ static BOOL hasPerformedInitialReload = NO;
639
592
 
640
593
  if (!downloadURL) {
641
594
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
642
- messageAsDictionary:@{
643
- @"error": @{
644
- @"message": @"URL required"
645
- }
646
- }];
595
+ messageAsDictionary:[self createError:kErrorURLRequired
596
+ message:@"URL is required"]];
647
597
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
648
598
  return;
649
599
  }
650
600
 
651
- // Опциональная версия (для автоустановки при следующем запуске)
652
601
  NSString *updateVersion = [updateData objectForKey:@"version"];
653
602
  if (!updateVersion) {
654
603
  updateVersion = @"pending";
@@ -657,50 +606,38 @@ static BOOL hasPerformedInitialReload = NO;
657
606
  NSLog(@"[HotUpdates] getUpdate() called - downloading update from: %@", downloadURL);
658
607
  NSLog(@"[HotUpdates] Version: %@", updateVersion);
659
608
 
660
- // ВАЖНО (ТЗ): НЕ проверяем ignoreList - JS сам контролирует что загружать
661
-
662
- // 1. Проверяем, не установлена ли уже эта версия
663
609
  NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
664
610
  if (installedVersion && [installedVersion isEqualToString:updateVersion]) {
665
611
  NSLog(@"[HotUpdates] Version %@ already installed, skipping download", updateVersion);
666
- // Возвращаем SUCCESS - версия уже установлена
667
612
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
668
613
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
669
614
  return;
670
615
  }
671
616
 
672
- // 2. Проверяем, не скачана ли уже эта версия (hasPending + та же версия)
673
617
  BOOL hasPending = [[NSUserDefaults standardUserDefaults] boolForKey:kHasPending];
674
618
  NSString *existingPendingVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
675
619
 
676
620
  if (hasPending && existingPendingVersion && [existingPendingVersion isEqualToString:updateVersion]) {
677
621
  NSLog(@"[HotUpdates] Version %@ already downloaded, skipping re-download", updateVersion);
678
- // Возвращаем SUCCESS - версия уже скачана, повторная загрузка не нужна
679
622
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
680
623
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
681
624
  return;
682
625
  }
683
626
 
684
- // 3. Проверяем, не скачивается ли уже обновление
685
627
  if (isDownloadingUpdate) {
686
628
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
687
- messageAsDictionary:@{
688
- @"error": @{
689
- @"message": @"Download already in progress"
690
- }
691
- }];
629
+ messageAsDictionary:[self createError:kErrorDownloadInProgress
630
+ message:@"Download already in progress"]];
692
631
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
693
632
  return;
694
633
  }
695
634
 
696
- // Сохраняем URL и версию для последующей установки и автоустановки
697
635
  pendingUpdateURL = downloadURL;
698
636
  pendingUpdateVersion = updateVersion;
699
637
  [[NSUserDefaults standardUserDefaults] setObject:downloadURL forKey:kPendingUpdateURL];
700
638
  [[NSUserDefaults standardUserDefaults] setObject:updateVersion forKey:kPendingVersion];
701
639
  [[NSUserDefaults standardUserDefaults] synchronize];
702
640
 
703
- // Запускаем загрузку
704
641
  [self downloadUpdateOnly:downloadURL callbackId:command.callbackId];
705
642
  }
706
643
 
@@ -712,15 +649,30 @@ static BOOL hasPerformedInitialReload = NO;
712
649
  NSLog(@"[HotUpdates] Starting download");
713
650
 
714
651
  NSURL *url = [NSURL URLWithString:downloadURL];
652
+ if (!url) {
653
+ NSLog(@"[HotUpdates] Invalid URL: %@", downloadURL);
654
+ isDownloadingUpdate = NO;
655
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
656
+ [[NSUserDefaults standardUserDefaults] synchronize];
657
+
658
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
659
+ messageAsDictionary:[self createError:kErrorURLRequired
660
+ message:@"Invalid URL format"]];
661
+ [self.commandDelegate sendPluginResult:result callbackId:callbackId];
662
+ return;
663
+ }
715
664
 
716
665
  NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
717
- config.timeoutIntervalForRequest = 60.0;
718
- config.timeoutIntervalForResource = 300.0;
666
+ config.timeoutIntervalForRequest = 30.0; // ТЗ: 30-60 секунд
667
+ config.timeoutIntervalForResource = 60.0; // ТЗ: максимум 60 секунд на всю загрузку
719
668
 
720
669
  NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
721
670
 
722
671
  NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url
723
672
  completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
673
+ // Инвалидируем сессию для предотвращения утечки памяти
674
+ [session finishTasksAndInvalidate];
675
+
724
676
  self->isDownloadingUpdate = NO;
725
677
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
726
678
  [[NSUserDefaults standardUserDefaults] synchronize];
@@ -729,11 +681,8 @@ static BOOL hasPerformedInitialReload = NO;
729
681
  NSLog(@"[HotUpdates] Download failed: %@", error.localizedDescription);
730
682
 
731
683
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
732
- messageAsDictionary:@{
733
- @"error": @{
734
- @"message": error.localizedDescription
735
- }
736
- }];
684
+ messageAsDictionary:[self createError:kErrorDownloadFailed
685
+ message:[NSString stringWithFormat:@"Download failed: %@", error.localizedDescription]]];
737
686
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
738
687
  return;
739
688
  }
@@ -743,11 +692,8 @@ static BOOL hasPerformedInitialReload = NO;
743
692
  NSLog(@"[HotUpdates] Download failed: HTTP %ld", (long)httpResponse.statusCode);
744
693
 
745
694
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
746
- messageAsDictionary:@{
747
- @"error": @{
748
- @"message": [NSString stringWithFormat:@"HTTP %ld", (long)httpResponse.statusCode]
749
- }
750
- }];
695
+ messageAsDictionary:[self createError:kErrorHTTPError
696
+ message:[NSString stringWithFormat:@"HTTP error: %ld", (long)httpResponse.statusCode]]];
751
697
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
752
698
  return;
753
699
  }
@@ -767,15 +713,12 @@ static BOOL hasPerformedInitialReload = NO;
767
713
  NSFileManager *fileManager = [NSFileManager defaultManager];
768
714
  NSError *error;
769
715
 
770
- // Создаем временную папку для распаковки
771
716
  NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
772
717
 
773
- // Удаляем старую временную папку
774
718
  if ([fileManager fileExistsAtPath:tempUpdatePath]) {
775
719
  [fileManager removeItemAtPath:tempUpdatePath error:nil];
776
720
  }
777
721
 
778
- // Создаем новую временную папку
779
722
  [fileManager createDirectoryAtPath:tempUpdatePath
780
723
  withIntermediateDirectories:YES
781
724
  attributes:nil
@@ -784,16 +727,12 @@ static BOOL hasPerformedInitialReload = NO;
784
727
  if (error) {
785
728
  NSLog(@"[HotUpdates] Error creating temp directory: %@", error);
786
729
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
787
- messageAsDictionary:@{
788
- @"error": @{
789
- @"message": @"Cannot create temp directory"
790
- }
791
- }];
730
+ messageAsDictionary:[self createError:kErrorTempDirError
731
+ message:@"Cannot create temp directory"]];
792
732
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
793
733
  return;
794
734
  }
795
735
 
796
- // Распаковываем обновление
797
736
  BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:tempUpdatePath];
798
737
 
799
738
  if (!unzipSuccess) {
@@ -801,63 +740,49 @@ static BOOL hasPerformedInitialReload = NO;
801
740
  [fileManager removeItemAtPath:tempUpdatePath error:nil];
802
741
 
803
742
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
804
- messageAsDictionary:@{
805
- @"error": @{
806
- @"message": @"Failed to extract update package"
807
- }
808
- }];
743
+ messageAsDictionary:[self createError:kErrorExtractionFailed
744
+ message:@"Failed to extract update package"]];
809
745
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
810
746
  return;
811
747
  }
812
748
 
813
- // Проверяем наличие www папки
814
749
  NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
815
750
  if (![fileManager fileExistsAtPath:tempWwwPath]) {
816
751
  NSLog(@"[HotUpdates] www folder not found in update package");
817
752
  [fileManager removeItemAtPath:tempUpdatePath error:nil];
818
753
 
819
754
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
820
- messageAsDictionary:@{
821
- @"error": @{
822
- @"message": @"www folder not found in package"
823
- }
824
- }];
755
+ messageAsDictionary:[self createError:kErrorWWWNotFound
756
+ message:@"www folder not found in package"]];
825
757
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
826
758
  return;
827
759
  }
828
760
 
829
- // Копируем также в pending_update для автоустановки при следующем запуске (ТЗ п.7)
761
+ // Копируем в pending_update для автоустановки при следующем запуске
830
762
  NSString *pendingPath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
831
763
 
832
- // Удаляем старую pending_update папку
833
764
  if ([fileManager fileExistsAtPath:pendingPath]) {
834
765
  [fileManager removeItemAtPath:pendingPath error:nil];
835
766
  }
836
767
 
837
- // Копируем temp_downloaded_update → pending_update
838
768
  BOOL copySuccess = [fileManager copyItemAtPath:tempUpdatePath
839
769
  toPath:pendingPath
840
770
  error:&error];
841
771
 
842
772
  if (!copySuccess) {
843
773
  NSLog(@"[HotUpdates] Failed to copy to pending_update: %@", error);
844
- // Не критично - forceUpdate всё равно сработает из temp_downloaded_update
845
774
  } else {
846
775
  NSLog(@"[HotUpdates] Copied to pending_update for auto-install on next launch");
847
776
  }
848
777
 
849
- // Помечаем обновление как готовое к установке (для forceUpdate)
850
778
  isUpdateReadyToInstall = YES;
851
779
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kPendingUpdateReady];
852
-
853
- // Устанавливаем флаг для автоустановки при следующем запуске (ТЗ п.7)
854
780
  [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kHasPending];
855
781
  [[NSUserDefaults standardUserDefaults] synchronize];
856
782
 
857
783
  NSLog(@"[HotUpdates] Update downloaded and ready to install");
858
784
  NSLog(@"[HotUpdates] If user ignores popup, update will install automatically on next launch");
859
785
 
860
- // Возвращаем успех (callback без ошибки) - ТЗ: возвращаем null при успехе
861
786
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
862
787
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
863
788
  }
@@ -867,16 +792,10 @@ static BOOL hasPerformedInitialReload = NO;
867
792
  - (void)forceUpdate:(CDVInvokedUrlCommand*)command {
868
793
  NSLog(@"[HotUpdates] forceUpdate() called - installing downloaded update");
869
794
 
870
- // ВАЖНО: Не проверяем ignoreList - это контролирует JS
871
-
872
- // Проверяем, что обновление было скачано
873
795
  if (!isUpdateReadyToInstall) {
874
796
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
875
- messageAsDictionary:@{
876
- @"error": @{
877
- @"message": @"No update ready to install. Call getUpdate() first."
878
- }
879
- }];
797
+ messageAsDictionary:[self createError:kErrorNoUpdateReady
798
+ message:@"No update ready to install"]];
880
799
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
881
800
  return;
882
801
  }
@@ -884,47 +803,36 @@ static BOOL hasPerformedInitialReload = NO;
884
803
  NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
885
804
  NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
886
805
 
887
- // Проверяем наличие скачанных файлов
888
806
  if (![[NSFileManager defaultManager] fileExistsAtPath:tempWwwPath]) {
889
807
  NSLog(@"[HotUpdates] Downloaded update files not found");
890
808
 
891
809
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
892
- messageAsDictionary:@{
893
- @"error": @{
894
- @"message": @"Downloaded update files not found"
895
- }
896
- }];
810
+ messageAsDictionary:[self createError:kErrorUpdateFilesNotFound
811
+ message:@"Downloaded update files not found"]];
897
812
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
898
813
  return;
899
814
  }
900
815
 
901
- // Устанавливаем обновление
902
816
  [self installDownloadedUpdate:tempWwwPath callbackId:command.callbackId];
903
817
  }
904
818
 
905
819
  - (void)installDownloadedUpdate:(NSString*)tempWwwPath callbackId:(NSString*)callbackId {
906
820
  NSLog(@"[HotUpdates] Installing update");
907
821
 
908
- // Определяем версию ДО установки
909
822
  NSString *versionToInstall = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
910
823
  if (!versionToInstall) {
911
824
  versionToInstall = @"unknown";
912
825
  }
913
826
 
914
- // ВАЖНО (ТЗ): НЕ проверяем ignoreList - JS сам контролирует что устанавливать
915
-
916
827
  NSFileManager *fileManager = [NSFileManager defaultManager];
917
828
  NSError *error;
918
829
 
919
- // ВАЖНО: Создаем резервную копию текущей версии
920
830
  [self backupCurrentVersion];
921
831
 
922
- // Удаляем текущую www
923
832
  if ([fileManager fileExistsAtPath:wwwPath]) {
924
833
  [fileManager removeItemAtPath:wwwPath error:nil];
925
834
  }
926
835
 
927
- // Копируем новую версию
928
836
  BOOL copySuccess = [fileManager copyItemAtPath:tempWwwPath
929
837
  toPath:wwwPath
930
838
  error:&error];
@@ -933,78 +841,62 @@ static BOOL hasPerformedInitialReload = NO;
933
841
  NSLog(@"[HotUpdates] Failed to install update: %@", error);
934
842
 
935
843
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
936
- messageAsDictionary:@{
937
- @"error": @{
938
- @"message": error.localizedDescription
939
- }
940
- }];
844
+ messageAsDictionary:[self createError:kErrorInstallFailed
845
+ message:[NSString stringWithFormat:@"Install failed: %@", error.localizedDescription]]];
941
846
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
942
847
  return;
943
848
  }
944
849
 
945
- // Используем версию определенную ранее
946
850
  NSString *newVersion = versionToInstall;
947
851
 
948
- // Обновляем метаданные
949
852
  [[NSUserDefaults standardUserDefaults] setObject:newVersion forKey:kInstalledVersion];
950
853
  [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kPendingUpdateReady];
951
- [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kHasPending]; // Очищаем флаг автоустановки
854
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kHasPending];
952
855
  [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPendingUpdateURL];
953
856
  [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPendingVersion];
954
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCanaryVersion]; // Сбрасываем canary
857
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCanaryVersion];
955
858
  [[NSUserDefaults standardUserDefaults] synchronize];
956
859
 
957
- // Очищаем временные папки
958
860
  NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
959
861
  NSString *pendingPath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
960
862
  [fileManager removeItemAtPath:tempUpdatePath error:nil];
961
863
  [fileManager removeItemAtPath:pendingPath error:nil];
962
864
 
963
- // Сбрасываем флаги
964
865
  isUpdateReadyToInstall = NO;
965
866
  pendingUpdateURL = nil;
867
+ pendingUpdateVersion = nil;
966
868
 
967
869
  NSLog(@"[HotUpdates] Update installed successfully");
968
870
 
969
- // Возвращаем успех ПЕРЕД перезагрузкой - ТЗ: возвращаем null при успехе
970
871
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
971
872
  [self.commandDelegate sendPluginResult:result callbackId:callbackId];
972
873
 
973
- // КРИТИЧЕСКИ ВАЖНО: Запускаем canary timer ПЕРЕД перезагрузкой
974
- // После reloadWebView pluginInitialize НЕ вызывается, поэтому таймер нужно запустить вручную
874
+ // После reloadWebView pluginInitialize НЕ вызывается, поэтому canary timer запускаем вручную
975
875
  NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", newVersion);
976
876
 
977
- // Останавливаем предыдущий таймер если был
978
- if (canaryTimer && [canaryTimer isValid]) {
979
- [canaryTimer invalidate];
980
- }
981
-
982
- // Запускаем новый таймер на 20 секунд
983
- canaryTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
984
- target:self
985
- selector:@selector(canaryTimeout)
986
- userInfo:nil
987
- repeats:NO];
877
+ [self startCanaryTimer];
988
878
 
989
- // Сбрасываем флаг для разрешения перезагрузки после установки обновления
990
879
  hasPerformedInitialReload = NO;
991
880
 
992
- // КРИТИЧЕСКИ ВАЖНО: Очищаем кэш WebView перед перезагрузкой
993
- // Без этого может загрузиться старая закэшированная версия
994
- [self clearWebViewCache];
995
-
996
- // Перезагружаем WebView с новым контентом
997
- [self reloadWebView];
881
+ // Очищаем кэш WebView перед перезагрузкой, иначе может загрузиться старая версия
882
+ [self clearWebViewCacheWithCompletion:^{
883
+ [self reloadWebView];
884
+ }];
998
885
  }
999
886
 
1000
887
  #pragma mark - Canary
1001
888
 
1002
889
  - (void)canary:(CDVInvokedUrlCommand*)command {
1003
- NSString *canaryVersion = [command.arguments objectAtIndex:0];
890
+ // Безопасное получение первого аргумента
891
+ NSString *canaryVersion = nil;
892
+ if (command.arguments.count > 0 && [command.arguments[0] isKindOfClass:[NSString class]]) {
893
+ canaryVersion = command.arguments[0];
894
+ }
1004
895
 
1005
896
  if (!canaryVersion || canaryVersion.length == 0) {
1006
897
  CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
1007
- messageAsString:@"Version required"];
898
+ messageAsDictionary:[self createError:kErrorVersionRequired
899
+ message:@"Version is required"]];
1008
900
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
1009
901
  return;
1010
902
  }
@@ -1022,16 +914,12 @@ static BOOL hasPerformedInitialReload = NO;
1022
914
  NSLog(@"[HotUpdates] Canary timer stopped - JS confirmed bundle is working");
1023
915
  }
1024
916
 
1025
- CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
1026
- messageAsDictionary:@{
1027
- @"success": @YES,
1028
- @"canaryVersion": canaryVersion,
1029
- @"message": @"Canary version confirmed"
1030
- }];
917
+ // ТЗ: при успехе callback возвращает null
918
+ CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
1031
919
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
1032
920
  }
1033
921
 
1034
- #pragma mark - Information Methods
922
+ #pragma mark - Debug Methods
1035
923
 
1036
924
  - (void)getVersionInfo:(CDVInvokedUrlCommand*)command {
1037
925
  NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
@@ -1055,14 +943,4 @@ static BOOL hasPerformedInitialReload = NO;
1055
943
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
1056
944
  }
1057
945
 
1058
- - (void)checkForUpdates:(CDVInvokedUrlCommand*)command {
1059
- CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
1060
- messageAsDictionary:@{
1061
- @"error": @{
1062
- @"message": @"checkForUpdates() removed in v2.1.0. Use fetch() in JS to check your server for updates, then call getUpdate({url}) to download."
1063
- }
1064
- }];
1065
- [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
1066
- }
1067
-
1068
946
  @end