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