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.
- package/README.md +87 -42
- package/package.json +2 -2
- package/plugin.xml +5 -1
- package/src/ios/HotUpdates+Helpers.h +23 -0
- package/src/ios/HotUpdates+Helpers.m +24 -0
- package/src/ios/HotUpdates.h +6 -31
- package/src/ios/HotUpdates.m +128 -250
- package/src/ios/HotUpdatesConstants.h +51 -0
- package/src/ios/HotUpdatesConstants.m +46 -0
- package/www/HotUpdates.js +109 -116
package/src/ios/HotUpdates.m
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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:©Error];
|
|
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];
|
|
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
|
-
// Предотвращаем повторные перезагрузки при навигации между страницами
|
|
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
|
-
|
|
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
|
|
238
|
-
* @details
|
|
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
|
-
//
|
|
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:©Error];
|
|
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
|
-
//
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
718
|
-
config.timeoutIntervalForResource =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// Копируем
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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];
|
|
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
|
-
//
|
|
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
|
-
//
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
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 -
|
|
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
|