react-native-update 10.45.0 → 10.46.0
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/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +24 -0
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +2 -1
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +2 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +13 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +15 -15
- package/cpp/patch_core/error_codes.h +43 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +20 -2
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +83 -23
- package/ios/RCTPushy/RCTPushyDownloader.mm +22 -1
- package/package.json +2 -1
- package/src/client.ts +120 -27
- package/src/core.ts +3 -1
- package/src/endpoint.ts +4 -2
- package/src/error.ts +68 -0
- package/src/index.ts +3 -0
- package/src/locales/en.ts +5 -1
- package/src/locales/zh.ts +3 -1
- package/src/provider.tsx +50 -24
- package/src/type.ts +6 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable, machine-readable error codes used as the promise rejection code so
|
|
5
|
+
* the JS layer and user loggers can aggregate errors across platforms.
|
|
6
|
+
*
|
|
7
|
+
* MUST stay in sync with cpp/patch_core/error_codes.h (the single source of
|
|
8
|
+
* truth) and src/error.ts (UpdateErrorCode). Messages are free-form; only the
|
|
9
|
+
* codes are part of the contract.
|
|
10
|
+
*/
|
|
11
|
+
final class ErrorCodes {
|
|
12
|
+
static final String INVALID_OPTIONS = "INVALID_OPTIONS";
|
|
13
|
+
static final String DOWNLOAD_FAILED = "DOWNLOAD_FAILED";
|
|
14
|
+
static final String PATCH_FAILED = "PATCH_FAILED";
|
|
15
|
+
static final String FILE_OPERATION_FAILED = "FILE_OPERATION_FAILED";
|
|
16
|
+
static final String SWITCH_VERSION_FAILED = "SWITCH_VERSION_FAILED";
|
|
17
|
+
static final String MARK_SUCCESS_FAILED = "MARK_SUCCESS_FAILED";
|
|
18
|
+
static final String RESTART_FAILED = "RESTART_FAILED";
|
|
19
|
+
static final String INVALID_HASH_INFO = "INVALID_HASH_INFO";
|
|
20
|
+
static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM";
|
|
21
|
+
|
|
22
|
+
private ErrorCodes() {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -43,6 +43,7 @@ final class StateSerialRunner {
|
|
|
43
43
|
|
|
44
44
|
static void run(
|
|
45
45
|
@Nullable final Promise promise,
|
|
46
|
+
final String errorCode,
|
|
46
47
|
final String operationName,
|
|
47
48
|
final Operation operation
|
|
48
49
|
) {
|
|
@@ -53,7 +54,7 @@ final class StateSerialRunner {
|
|
|
53
54
|
operation.run();
|
|
54
55
|
} catch (Throwable error) {
|
|
55
56
|
if (promise != null) {
|
|
56
|
-
promise.reject(operationName + " failed", error);
|
|
57
|
+
promise.reject(errorCode, operationName + " failed", error);
|
|
57
58
|
} else {
|
|
58
59
|
Log.e(UpdateContext.TAG, operationName + " failed", error);
|
|
59
60
|
}
|
|
@@ -15,6 +15,7 @@ final class UiThreadRunner {
|
|
|
15
15
|
|
|
16
16
|
static void run(
|
|
17
17
|
@Nullable final Promise promise,
|
|
18
|
+
final String errorCode,
|
|
18
19
|
final String operationName,
|
|
19
20
|
final Operation operation
|
|
20
21
|
) {
|
|
@@ -25,7 +26,7 @@ final class UiThreadRunner {
|
|
|
25
26
|
operation.run();
|
|
26
27
|
} catch (Throwable error) {
|
|
27
28
|
if (promise != null) {
|
|
28
|
-
promise.reject(operationName + " failed", error);
|
|
29
|
+
promise.reject(errorCode, operationName + " failed", error);
|
|
29
30
|
} else {
|
|
30
31
|
Log.e(UpdateContext.TAG, operationName + " failed", error);
|
|
31
32
|
}
|
|
@@ -207,8 +207,10 @@ public class UpdateContext {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
private void persistEditor(SharedPreferences.Editor editor, String reason) {
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// A lost state write can mean a missed rollback or a version switch
|
|
211
|
+
// that silently never happens, so this must be visible in release too.
|
|
212
|
+
if (!editor.commit()) {
|
|
213
|
+
Log.e(TAG, "Failed to persist update state for " + reason);
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -367,6 +369,13 @@ public class UpdateContext {
|
|
|
367
369
|
ignoreRollback,
|
|
368
370
|
true
|
|
369
371
|
);
|
|
372
|
+
if (launchState.didRollback) {
|
|
373
|
+
// The crash-protection rollback: the new version never called
|
|
374
|
+
// markSuccess. Keep this visible in release logs.
|
|
375
|
+
Log.e(TAG, "Version " + currentState.currentVersion
|
|
376
|
+
+ " was not marked as successful, rolling back to "
|
|
377
|
+
+ launchState.currentVersion);
|
|
378
|
+
}
|
|
370
379
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
371
380
|
SharedPreferences.Editor editor = sp.edit();
|
|
372
381
|
applyState(editor, launchState);
|
|
@@ -411,6 +420,8 @@ public class UpdateContext {
|
|
|
411
420
|
false,
|
|
412
421
|
false
|
|
413
422
|
);
|
|
423
|
+
Log.e(TAG, "Rolling back version " + currentState.currentVersion
|
|
424
|
+
+ " to " + nextState.currentVersion);
|
|
414
425
|
SharedPreferences.Editor editor = sp.edit();
|
|
415
426
|
applyState(editor, nextState);
|
|
416
427
|
persistEditor(editor, "rollback");
|
|
@@ -41,7 +41,7 @@ public class UpdateModuleImpl {
|
|
|
41
41
|
|
|
42
42
|
@Override
|
|
43
43
|
public void onDownloadFailed(Throwable error) {
|
|
44
|
-
promise.reject(error);
|
|
44
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -64,7 +64,7 @@ public class UpdateModuleImpl {
|
|
|
64
64
|
|
|
65
65
|
@Override
|
|
66
66
|
public void onDownloadFailed(Throwable error) {
|
|
67
|
-
promise.reject(error);
|
|
67
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
@@ -84,7 +84,7 @@ public class UpdateModuleImpl {
|
|
|
84
84
|
|
|
85
85
|
@Override
|
|
86
86
|
public void onDownloadFailed(Throwable error) {
|
|
87
|
-
promise.reject(error);
|
|
87
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
}
|
|
@@ -107,11 +107,11 @@ public class UpdateModuleImpl {
|
|
|
107
107
|
|
|
108
108
|
@Override
|
|
109
109
|
public void onDownloadFailed(Throwable error) {
|
|
110
|
-
promise.reject(error);
|
|
110
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
} catch (Exception e) {
|
|
114
|
-
promise.reject("downloadPatchFromPpk failed: " + e.getMessage());
|
|
114
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "downloadPatchFromPpk failed: " + e.getMessage(), e);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -130,7 +130,7 @@ public class UpdateModuleImpl {
|
|
|
130
130
|
@Nullable final String hash,
|
|
131
131
|
final Promise promise
|
|
132
132
|
) {
|
|
133
|
-
UiThreadRunner.run(promise, "restartApp", new UiThreadRunner.Operation() {
|
|
133
|
+
UiThreadRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new UiThreadRunner.Operation() {
|
|
134
134
|
@Override
|
|
135
135
|
public void run() throws Throwable {
|
|
136
136
|
ReactReloadManager.restartApp(updateContext, reactContext, hash);
|
|
@@ -149,7 +149,7 @@ public class UpdateModuleImpl {
|
|
|
149
149
|
final Promise promise
|
|
150
150
|
) {
|
|
151
151
|
final String hash = options.getString("hash");
|
|
152
|
-
StateSerialRunner.run(promise, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
152
|
+
StateSerialRunner.run(promise, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
153
153
|
@Override
|
|
154
154
|
public void run() {
|
|
155
155
|
setNeedUpdateInternal(updateContext, hash);
|
|
@@ -160,7 +160,7 @@ public class UpdateModuleImpl {
|
|
|
160
160
|
|
|
161
161
|
public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
|
|
162
162
|
final String hash = options.getString("hash");
|
|
163
|
-
StateSerialRunner.run(null, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
163
|
+
StateSerialRunner.run(null, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
164
164
|
@Override
|
|
165
165
|
public void run() {
|
|
166
166
|
setNeedUpdateInternal(updateContext, hash);
|
|
@@ -173,7 +173,7 @@ public class UpdateModuleImpl {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
|
|
176
|
-
StateSerialRunner.run(promise, "markSuccess", new StateSerialRunner.Operation() {
|
|
176
|
+
StateSerialRunner.run(promise, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
|
|
177
177
|
@Override
|
|
178
178
|
public void run() {
|
|
179
179
|
markSuccessInternal(updateContext);
|
|
@@ -183,7 +183,7 @@ public class UpdateModuleImpl {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
public static void markSuccess(final UpdateContext updateContext) {
|
|
186
|
-
StateSerialRunner.run(null, "markSuccess", new StateSerialRunner.Operation() {
|
|
186
|
+
StateSerialRunner.run(null, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
|
|
187
187
|
@Override
|
|
188
188
|
public void run() {
|
|
189
189
|
markSuccessInternal(updateContext);
|
|
@@ -200,7 +200,7 @@ public class UpdateModuleImpl {
|
|
|
200
200
|
final String uuid,
|
|
201
201
|
final Promise promise
|
|
202
202
|
) {
|
|
203
|
-
StateSerialRunner.run(promise, "setUuid", new StateSerialRunner.Operation() {
|
|
203
|
+
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
|
|
204
204
|
@Override
|
|
205
205
|
public void run() {
|
|
206
206
|
setUuidInternal(updateContext, uuid);
|
|
@@ -210,7 +210,7 @@ public class UpdateModuleImpl {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
public static void setUuid(final UpdateContext updateContext, final String uuid) {
|
|
213
|
-
StateSerialRunner.run(null, "setUuid", new StateSerialRunner.Operation() {
|
|
213
|
+
StateSerialRunner.run(null, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
|
|
214
214
|
@Override
|
|
215
215
|
public void run() {
|
|
216
216
|
setUuidInternal(updateContext, uuid);
|
|
@@ -235,7 +235,7 @@ public class UpdateModuleImpl {
|
|
|
235
235
|
final String info,
|
|
236
236
|
final Promise promise
|
|
237
237
|
) {
|
|
238
|
-
StateSerialRunner.run(promise, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
238
|
+
StateSerialRunner.run(promise, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
239
239
|
@Override
|
|
240
240
|
public void run() {
|
|
241
241
|
setLocalHashInfoInternal(updateContext, hash, info);
|
|
@@ -249,7 +249,7 @@ public class UpdateModuleImpl {
|
|
|
249
249
|
final String hash,
|
|
250
250
|
final String info
|
|
251
251
|
) {
|
|
252
|
-
StateSerialRunner.run(null, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
252
|
+
StateSerialRunner.run(null, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
253
253
|
@Override
|
|
254
254
|
public void run() {
|
|
255
255
|
setLocalHashInfoInternal(updateContext, hash, info);
|
|
@@ -264,7 +264,7 @@ public class UpdateModuleImpl {
|
|
|
264
264
|
) {
|
|
265
265
|
String value = updateContext.getKv("hash_" + hash);
|
|
266
266
|
if (!isValidHashInfo(value)) {
|
|
267
|
-
promise.reject("getLocalHashInfo failed: invalid json string");
|
|
267
|
+
promise.reject(ErrorCodes.INVALID_HASH_INFO, "getLocalHashInfo failed: invalid json string");
|
|
268
268
|
return;
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#ifndef PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
2
|
+
#define PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
3
|
+
|
|
4
|
+
// Single source of truth for the stable, machine-readable error codes shared
|
|
5
|
+
// by every platform. Native modules reject promises with one of these codes
|
|
6
|
+
// so the JS layer (src/error.ts UpdateErrorCode) and user loggers can
|
|
7
|
+
// aggregate errors across platforms and locales.
|
|
8
|
+
//
|
|
9
|
+
// Mirrors that cannot include this header MUST stay in sync by hand:
|
|
10
|
+
// - src/error.ts (UpdateErrorCode union, JS layer)
|
|
11
|
+
// - android/.../ErrorCodes.java (Java constants)
|
|
12
|
+
// iOS (RCTPushy.mm) includes this header directly.
|
|
13
|
+
//
|
|
14
|
+
// Human-readable messages are NOT part of this contract: they may differ per
|
|
15
|
+
// platform and locale. Only the codes are stable.
|
|
16
|
+
|
|
17
|
+
namespace pushy {
|
|
18
|
+
namespace error_codes {
|
|
19
|
+
|
|
20
|
+
// Method options missing or malformed (blank hash/url, wrong types).
|
|
21
|
+
constexpr const char* kInvalidOptions = "INVALID_OPTIONS";
|
|
22
|
+
// Native download failed (network error, bad HTTP status, truncated body).
|
|
23
|
+
constexpr const char* kDownloadFailed = "DOWNLOAD_FAILED";
|
|
24
|
+
// Unzip or hdiff patch application failed.
|
|
25
|
+
constexpr const char* kPatchFailed = "PATCH_FAILED";
|
|
26
|
+
// Local file or state persistence operation failed.
|
|
27
|
+
constexpr const char* kFileOperationFailed = "FILE_OPERATION_FAILED";
|
|
28
|
+
// switchVersion / setNeedUpdate state transition failed.
|
|
29
|
+
constexpr const char* kSwitchVersionFailed = "SWITCH_VERSION_FAILED";
|
|
30
|
+
// markSuccess state transition failed.
|
|
31
|
+
constexpr const char* kMarkSuccessFailed = "MARK_SUCCESS_FAILED";
|
|
32
|
+
// reloadUpdate / restartApp failed.
|
|
33
|
+
constexpr const char* kRestartFailed = "RESTART_FAILED";
|
|
34
|
+
// Stored or provided hash info is not a valid JSON object.
|
|
35
|
+
constexpr const char* kInvalidHashInfo = "INVALID_HASH_INFO";
|
|
36
|
+
// The method is not supported on this platform (e.g. downloadAndInstallApk
|
|
37
|
+
// outside Android).
|
|
38
|
+
constexpr const char* kUnsupportedPlatform = "UNSUPPORTED_PLATFORM";
|
|
39
|
+
|
|
40
|
+
} // namespace error_codes
|
|
41
|
+
} // namespace pushy
|
|
42
|
+
|
|
43
|
+
#endif // PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
@@ -468,13 +468,22 @@ export class UpdateContext {
|
|
|
468
468
|
public getBundleUrl() {
|
|
469
469
|
UpdateContext.isUsingBundleUrl = true;
|
|
470
470
|
this.trace('getBundleUrl:enter');
|
|
471
|
+
const stateBeforeLaunch = this.getStateSnapshot();
|
|
471
472
|
const launchState = NativePatchCore.runStateCore(
|
|
472
473
|
STATE_OP_RESOLVE_LAUNCH,
|
|
473
|
-
|
|
474
|
+
stateBeforeLaunch,
|
|
474
475
|
'',
|
|
475
476
|
UpdateContext.ignoreRollback,
|
|
476
477
|
true,
|
|
477
478
|
);
|
|
479
|
+
if (launchState.didRollback) {
|
|
480
|
+
// The crash-protection rollback: the new version never called
|
|
481
|
+
// markSuccess. Keep this visible in release logs.
|
|
482
|
+
console.error(
|
|
483
|
+
`Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
|
|
484
|
+
` rolled back to ${launchState.currentVersion}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
478
487
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
479
488
|
this.persistState(launchState, {
|
|
480
489
|
markFirstLoadMarker: launchState.consumedFirstTime,
|
|
@@ -490,7 +499,12 @@ export class UpdateContext {
|
|
|
490
499
|
);
|
|
491
500
|
|
|
492
501
|
let version = launchState.loadVersion || '';
|
|
493
|
-
|
|
502
|
+
// Guard the rollback chain against cycles: a corrupted state returning an
|
|
503
|
+
// already-visited version would otherwise spin this loop forever during
|
|
504
|
+
// startup (Android has the same guard).
|
|
505
|
+
const visitedVersions = new Set<string>();
|
|
506
|
+
while (version && !visitedVersions.has(version)) {
|
|
507
|
+
visitedVersions.add(version);
|
|
494
508
|
const bundleFile = this.getBundlePath(version);
|
|
495
509
|
try {
|
|
496
510
|
if (!fileIo.accessSync(bundleFile)) {
|
|
@@ -514,7 +528,11 @@ export class UpdateContext {
|
|
|
514
528
|
}
|
|
515
529
|
|
|
516
530
|
private rollBack(): string {
|
|
531
|
+
const stateBefore = this.getStateSnapshot();
|
|
517
532
|
const nextState = this.runStateOperation(STATE_OP_ROLLBACK);
|
|
533
|
+
console.error(
|
|
534
|
+
`Rolling back version ${stateBefore.currentVersion} to ${nextState.currentVersion}`,
|
|
535
|
+
);
|
|
518
536
|
return nextState.currentVersion || '';
|
|
519
537
|
}
|
|
520
538
|
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#import "RCTPushyDownloader.h"
|
|
3
3
|
#import "ZipArchive.h"
|
|
4
4
|
#include "../../cpp/patch_core/archive_patch_core.h"
|
|
5
|
+
#include "../../cpp/patch_core/error_codes.h"
|
|
5
6
|
#include "../../cpp/patch_core/patch_core.h"
|
|
6
7
|
#include "../../cpp/patch_core/state_core.h"
|
|
7
8
|
|
|
@@ -37,9 +38,15 @@ static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs";
|
|
|
37
38
|
static NSString * const SOURCE_PATCH_NAME = @"__diff.json";
|
|
38
39
|
static NSString * const BUNDLE_PATCH_NAME = @"index.bundlejs.patch";
|
|
39
40
|
|
|
40
|
-
// error def
|
|
41
|
+
// error def — messages are human-readable; the stable cross-platform codes
|
|
42
|
+
// live in cpp/patch_core/error_codes.h and travel in PushyErrorCodeKey.
|
|
41
43
|
static NSString * const ERROR_OPTIONS = @"options error";
|
|
42
44
|
static NSString * const ERROR_FILE_OPERATION = @"file operation error";
|
|
45
|
+
static NSString * const PushyErrorCodeKey = @"PushyErrorCode";
|
|
46
|
+
|
|
47
|
+
static NSString *PushyCode(const char *code) {
|
|
48
|
+
return [NSString stringWithUTF8String:code];
|
|
49
|
+
}
|
|
43
50
|
|
|
44
51
|
// event def
|
|
45
52
|
static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTPushyDownloadProgress";
|
|
@@ -80,7 +87,10 @@ static std::string PushyToStdString(NSString *value) {
|
|
|
80
87
|
static NSError *PushyNSErrorFromStatus(const pushy::patch::Status &status) {
|
|
81
88
|
return [NSError errorWithDomain:PushyErrorDomain
|
|
82
89
|
code:-1
|
|
83
|
-
userInfo:@{
|
|
90
|
+
userInfo:@{
|
|
91
|
+
NSLocalizedDescriptionKey: [NSString stringWithUTF8String:status.message.c_str()],
|
|
92
|
+
PushyErrorCodeKey: PushyCode(pushy::error_codes::kPatchFailed),
|
|
93
|
+
}];
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
static NSUserDefaults *PushyDefaults(void) {
|
|
@@ -118,21 +128,24 @@ static BOOL PushyStringIsBlank(NSString *value) {
|
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
static void PushyRejectError(RCTPromiseRejectBlock reject, NSError *error) {
|
|
121
|
-
|
|
131
|
+
// Prefer the stable cross-platform code (error_codes.h); fall back to the
|
|
132
|
+
// numeric NSError code for system errors that were not classified.
|
|
133
|
+
NSString *code = error.userInfo[PushyErrorCodeKey];
|
|
134
|
+
if (code == nil) {
|
|
135
|
+
code = [NSString stringWithFormat:@"%ld", (long)error.code];
|
|
136
|
+
}
|
|
137
|
+
reject(code, error.localizedDescription, error);
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
static NSError *
|
|
140
|
+
static NSError *PushyErrorWithCode(const char *code, NSString *message) {
|
|
125
141
|
return [NSError errorWithDomain:PushyErrorDomain
|
|
126
142
|
code:-1
|
|
127
143
|
userInfo:@{
|
|
128
144
|
NSLocalizedDescriptionKey: message ?: @"unknown error",
|
|
145
|
+
PushyErrorCodeKey: PushyCode(code),
|
|
129
146
|
}];
|
|
130
147
|
}
|
|
131
148
|
|
|
132
|
-
static void PushyRejectMessage(RCTPromiseRejectBlock reject, NSString *message) {
|
|
133
|
-
PushyRejectError(reject, PushyErrorWithMessage(message));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
149
|
static pushy::patch::PatchManifest PushyPatchManifestFromJson(NSDictionary *json) {
|
|
137
150
|
pushy::patch::PatchManifest manifest;
|
|
138
151
|
|
|
@@ -257,6 +270,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
257
270
|
}
|
|
258
271
|
|
|
259
272
|
if (!state.current_version.empty()) {
|
|
273
|
+
std::string const versionBeforeLaunch = state.current_version;
|
|
260
274
|
pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
|
|
261
275
|
state,
|
|
262
276
|
ignoreRollback.load(),
|
|
@@ -264,6 +278,13 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
264
278
|
);
|
|
265
279
|
state = decision.state;
|
|
266
280
|
|
|
281
|
+
if (decision.did_rollback) {
|
|
282
|
+
// The crash-protection rollback: the new version never called
|
|
283
|
+
// markSuccess. Keep this visible in release logs.
|
|
284
|
+
RCTLogWarn(@"RCTPushy -- version %@ was not marked as successful, rolled back to %@",
|
|
285
|
+
PushyFromStdString(versionBeforeLaunch),
|
|
286
|
+
PushyFromStdString(state.current_version));
|
|
287
|
+
}
|
|
267
288
|
if (decision.did_rollback || decision.consumed_first_time) {
|
|
268
289
|
PushyApplyStateToDefaults(defaults, state);
|
|
269
290
|
}
|
|
@@ -275,13 +296,18 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
275
296
|
|
|
276
297
|
NSString *loadVersion = PushyFromStdString(decision.load_version);
|
|
277
298
|
NSString *downloadDir = [RCTPushy downloadDir];
|
|
278
|
-
|
|
299
|
+
// Guard the rollback chain against cycles: a corrupted state
|
|
300
|
+
// returning an already-visited version would otherwise spin this
|
|
301
|
+
// loop forever during startup (Android has the same guard).
|
|
302
|
+
NSMutableSet<NSString *> *visitedVersions = [NSMutableSet set];
|
|
303
|
+
while (loadVersion.length && ![visitedVersions containsObject:loadVersion]) {
|
|
304
|
+
[visitedVersions addObject:loadVersion];
|
|
279
305
|
NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
280
306
|
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
281
307
|
resolvedURL = [NSURL fileURLWithPath:bundlePath];
|
|
282
308
|
return;
|
|
283
309
|
} else {
|
|
284
|
-
RCTLogError(@"RCTPushy -- bundle version %@ not found", loadVersion);
|
|
310
|
+
RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
|
|
285
311
|
state = pushy::state::Rollback(state);
|
|
286
312
|
PushyApplyStateToDefaults(defaults, state);
|
|
287
313
|
loadVersion = PushyFromStdString(state.current_version);
|
|
@@ -359,7 +385,7 @@ RCT_EXPORT_METHOD(setUuid:(NSString *)uuid resolver:(RCTPromiseResolveBlock)res
|
|
|
359
385
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
360
386
|
{
|
|
361
387
|
if (PushyStringIsBlank(uuid)) {
|
|
362
|
-
PushyRejectError(reject,
|
|
388
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
363
389
|
return;
|
|
364
390
|
}
|
|
365
391
|
|
|
@@ -373,7 +399,7 @@ RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
|
|
|
373
399
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
374
400
|
{
|
|
375
401
|
if (PushyStringIsBlank(hash) || PushyStringIsBlank(value)) {
|
|
376
|
-
|
|
402
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
377
403
|
return;
|
|
378
404
|
}
|
|
379
405
|
|
|
@@ -386,7 +412,9 @@ RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
|
|
|
386
412
|
|
|
387
413
|
resolve(@true);
|
|
388
414
|
} else {
|
|
389
|
-
PushyRejectError(reject,
|
|
415
|
+
PushyRejectError(reject, PushyErrorWithCode(
|
|
416
|
+
pushy::error_codes::kInvalidHashInfo,
|
|
417
|
+
error != nil ? error.localizedDescription : @"invalid json string"));
|
|
390
418
|
}
|
|
391
419
|
}
|
|
392
420
|
|
|
@@ -421,6 +449,15 @@ RCT_EXPORT_METHOD(downloadPatchFromPpk:(NSDictionary *)options
|
|
|
421
449
|
[self downloadUpdate:PushyTypePatchFromPpk options:options resolver:resolve rejecter:reject];
|
|
422
450
|
}
|
|
423
451
|
|
|
452
|
+
RCT_EXPORT_METHOD(downloadAndInstallApk:(NSDictionary *)options
|
|
453
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
454
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
455
|
+
{
|
|
456
|
+
PushyRejectError(reject, PushyErrorWithCode(
|
|
457
|
+
pushy::error_codes::kUnsupportedPlatform,
|
|
458
|
+
@"downloadAndInstallApk is only supported on Android"));
|
|
459
|
+
}
|
|
460
|
+
|
|
424
461
|
RCT_EXPORT_METHOD(setNeedUpdate:(NSDictionary *)options
|
|
425
462
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
426
463
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -502,6 +539,12 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
502
539
|
{
|
|
503
540
|
[self performUpdate:type options:options callback:^(NSError *error) {
|
|
504
541
|
if (error != nil) {
|
|
542
|
+
if (error.userInfo[PushyErrorCodeKey] == nil) {
|
|
543
|
+
// Unclassified (system/network) errors from the download
|
|
544
|
+
// pipeline; keep the original message.
|
|
545
|
+
error = PushyErrorWithCode(pushy::error_codes::kDownloadFailed,
|
|
546
|
+
error.localizedDescription);
|
|
547
|
+
}
|
|
505
548
|
PushyRejectError(reject, error);
|
|
506
549
|
return;
|
|
507
550
|
}
|
|
@@ -527,24 +570,38 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
527
570
|
NSString *hash = PushyOptionString(options, @"hash");
|
|
528
571
|
|
|
529
572
|
if (PushyStringIsBlank(updateUrl) || PushyStringIsBlank(hash)) {
|
|
530
|
-
callback(
|
|
573
|
+
callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
531
574
|
return;
|
|
532
575
|
}
|
|
533
576
|
NSString *originHash = PushyOptionString(options, @"originHash");
|
|
534
577
|
if (type == PushyTypePatchFromPpk && PushyStringIsBlank(originHash)) {
|
|
535
|
-
callback(
|
|
578
|
+
callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
536
579
|
return;
|
|
537
580
|
}
|
|
538
581
|
|
|
539
582
|
NSString *dir = [RCTPushy downloadDir];
|
|
540
583
|
BOOL success = [self ensureDirectoryExistsAtPath:dir];
|
|
541
584
|
if (!success) {
|
|
542
|
-
callback(
|
|
585
|
+
callback(PushyErrorWithCode(pushy::error_codes::kFileOperationFailed, ERROR_FILE_OPERATION));
|
|
543
586
|
return;
|
|
544
587
|
}
|
|
545
588
|
|
|
546
589
|
NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hash, [self zipExtension:type]]];
|
|
547
590
|
|
|
591
|
+
// On failure, remove the partial version directory like Android/Harmony
|
|
592
|
+
// do: a half-unzipped/half-patched dir leaks disk and could later be
|
|
593
|
+
// mistaken for a complete version. hash is validated non-blank above, so
|
|
594
|
+
// this can never resolve to the download root itself.
|
|
595
|
+
NSString *unzipDir = [dir stringByAppendingPathComponent:hash];
|
|
596
|
+
void (^completion)(NSError *) = ^(NSError *error) {
|
|
597
|
+
if (error != nil) {
|
|
598
|
+
dispatch_async(self->_fileQueue, ^{
|
|
599
|
+
[[NSFileManager defaultManager] removeItemAtPath:unzipDir error:nil];
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
callback(error);
|
|
603
|
+
};
|
|
604
|
+
|
|
548
605
|
RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
|
|
549
606
|
[RCTPushyDownloader download:updateUrl savePath:zipFilePath progressHandler:^(long long receivedBytes, long long totalBytes) {
|
|
550
607
|
if (self->hasListeners) {
|
|
@@ -556,14 +613,14 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
556
613
|
}
|
|
557
614
|
} completionHandler:^(NSString *path, NSError *error) {
|
|
558
615
|
if (error != nil) {
|
|
559
|
-
|
|
616
|
+
completion(error);
|
|
560
617
|
return;
|
|
561
618
|
}
|
|
562
619
|
[self unzipDownloadedPackage:zipFilePath
|
|
563
620
|
hash:hash
|
|
564
621
|
type:type
|
|
565
622
|
originHash:originHash
|
|
566
|
-
callback:
|
|
623
|
+
callback:completion];
|
|
567
624
|
}];
|
|
568
625
|
}
|
|
569
626
|
|
|
@@ -629,18 +686,21 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
629
686
|
NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
630
687
|
NSData *data = [NSData dataWithContentsOfFile:sourcePatch];
|
|
631
688
|
if (data == nil) {
|
|
632
|
-
callback(
|
|
689
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"missing patch manifest"));
|
|
633
690
|
return;
|
|
634
691
|
}
|
|
635
692
|
|
|
636
693
|
NSError *error = nil;
|
|
637
694
|
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
|
|
638
695
|
if (error != nil) {
|
|
639
|
-
|
|
696
|
+
// Classify as a patch failure like the sibling manifest branches;
|
|
697
|
+
// unclassified errors would otherwise be tagged DOWNLOAD_FAILED by the
|
|
698
|
+
// downloadUpdate fallback even though the download itself succeeded.
|
|
699
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, error.localizedDescription));
|
|
640
700
|
return;
|
|
641
701
|
}
|
|
642
702
|
if (![jsonObject isKindOfClass:[NSDictionary class]]) {
|
|
643
|
-
callback(
|
|
703
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"invalid patch manifest"));
|
|
644
704
|
return;
|
|
645
705
|
}
|
|
646
706
|
NSDictionary *json = (NSDictionary *)jsonObject;
|
|
@@ -695,7 +755,7 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
695
755
|
{
|
|
696
756
|
if (PushyStringIsBlank(hash)) {
|
|
697
757
|
if (error != NULL) {
|
|
698
|
-
*error =
|
|
758
|
+
*error = PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS);
|
|
699
759
|
}
|
|
700
760
|
return NO;
|
|
701
761
|
}
|
|
@@ -758,7 +818,7 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
758
818
|
|
|
759
819
|
NSError *unzipError = error;
|
|
760
820
|
if (!succeeded && unzipError == nil) {
|
|
761
|
-
unzipError =
|
|
821
|
+
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
|
|
762
822
|
}
|
|
763
823
|
completionHandler(unzipError);
|
|
764
824
|
}];
|
|
@@ -113,7 +113,8 @@ didFinishDownloadingToURL:(NSURL *)location
|
|
|
113
113
|
// before touching savePath so an existing valid package is not destroyed.
|
|
114
114
|
NSURLResponse *response = downloadTask.response;
|
|
115
115
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
116
|
-
|
|
116
|
+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
117
|
+
NSInteger statusCode = httpResponse.statusCode;
|
|
117
118
|
if (statusCode < 200 || statusCode >= 300) {
|
|
118
119
|
self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
119
120
|
code:statusCode
|
|
@@ -122,6 +123,26 @@ didFinishDownloadingToURL:(NSURL *)location
|
|
|
122
123
|
}];
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
126
|
+
|
|
127
|
+
// Reject truncated transfers like Android/Harmony do. Skip the check
|
|
128
|
+
// for encoded responses (e.g. gzip): NSURLSession decompresses them
|
|
129
|
+
// transparently, so the on-disk size legitimately differs from the
|
|
130
|
+
// Content-Length of the encoded body.
|
|
131
|
+
NSString *contentEncoding = [httpResponse.allHeaderFields[@"Content-Encoding"] lowercaseString];
|
|
132
|
+
BOOL isEncodedBody = contentEncoding.length > 0 && ![contentEncoding isEqualToString:@"identity"];
|
|
133
|
+
long long expectedLength = httpResponse.expectedContentLength;
|
|
134
|
+
if (!isEncodedBody && expectedLength > 0) {
|
|
135
|
+
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:location.path
|
|
136
|
+
error:nil][NSFileSize];
|
|
137
|
+
if (fileSize != nil && fileSize.longLongValue != expectedLength) {
|
|
138
|
+
self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
139
|
+
code:-1
|
|
140
|
+
userInfo:@{
|
|
141
|
+
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"download incomplete: expected %lld bytes, got %lld", expectedLength, fileSize.longLongValue],
|
|
142
|
+
}];
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-update",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.46.0",
|
|
4
4
|
"description": "react-native hot update",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"test": "bun test src/__tests__",
|
|
16
16
|
"test:patch-core": "./scripts/test-patch-core.sh",
|
|
17
17
|
"build:harmony-har": "node scripts/build-harmony-har.js",
|
|
18
|
+
"build:harmony-e2e": "bash Example/e2etest/scripts/build-harmony-e2e.sh",
|
|
18
19
|
"build:so": "bun submodule && bash scripts/build-android-so.sh",
|
|
19
20
|
"build:ios-debug": "cd Example/e2etest && bun && detox build --configuration ios.sim.debug",
|
|
20
21
|
"build:ios-release": "cd Example/e2etest && bun && detox build --configuration ios.sim.release",
|
package/src/client.ts
CHANGED
|
@@ -39,8 +39,19 @@ import {
|
|
|
39
39
|
testUrls,
|
|
40
40
|
} from './utils';
|
|
41
41
|
import i18n from './i18n';
|
|
42
|
+
import { toUpdateError, UpdateError, UpdateErrorCode } from './error';
|
|
42
43
|
import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Receives every error the client reports, alongside the report event type.
|
|
47
|
+
* The UpdateProvider subscribes to surface errors as lastError/Alert; user
|
|
48
|
+
* code can subscribe too. Listeners run before any throwError rethrow.
|
|
49
|
+
*/
|
|
50
|
+
export type UpdateErrorListener = (
|
|
51
|
+
error: UpdateError,
|
|
52
|
+
eventType: EventType,
|
|
53
|
+
) => void;
|
|
54
|
+
|
|
44
55
|
const SERVER_PRESETS = {
|
|
45
56
|
// cn
|
|
46
57
|
Pushy: {
|
|
@@ -142,7 +153,10 @@ export class Pushy {
|
|
|
142
153
|
|
|
143
154
|
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
144
155
|
if (!options.appKey) {
|
|
145
|
-
throw
|
|
156
|
+
throw new UpdateError(
|
|
157
|
+
i18n.t('error_appkey_required'),
|
|
158
|
+
'APPKEY_REQUIRED',
|
|
159
|
+
);
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
162
|
|
|
@@ -184,13 +198,15 @@ export class Pushy {
|
|
|
184
198
|
report = async ({
|
|
185
199
|
type,
|
|
186
200
|
message = '',
|
|
201
|
+
code,
|
|
187
202
|
data = {},
|
|
188
203
|
}: {
|
|
189
204
|
type: EventType;
|
|
190
205
|
message?: string;
|
|
206
|
+
code?: UpdateErrorCode;
|
|
191
207
|
data?: Record<string, string | number>;
|
|
192
208
|
}) => {
|
|
193
|
-
log(`${type} ${message}`);
|
|
209
|
+
log(`${type} ${code ? `[${code}] ` : ''}${message}`);
|
|
194
210
|
if (this.options.logger === noop) {
|
|
195
211
|
// Wait briefly for a logger to arrive via setOptions (e.g. the rollback
|
|
196
212
|
// report fires in the constructor before the user configures one), but
|
|
@@ -214,6 +230,7 @@ export class Pushy {
|
|
|
214
230
|
overridePackageVersion,
|
|
215
231
|
buildTime,
|
|
216
232
|
message,
|
|
233
|
+
code,
|
|
217
234
|
...currentVersionInfo,
|
|
218
235
|
...data,
|
|
219
236
|
},
|
|
@@ -230,6 +247,39 @@ export class Pushy {
|
|
|
230
247
|
throw e;
|
|
231
248
|
}
|
|
232
249
|
};
|
|
250
|
+
private errorListeners = new Set<UpdateErrorListener>();
|
|
251
|
+
/**
|
|
252
|
+
* Subscribe to every error the client reports (regardless of throwError).
|
|
253
|
+
* Returns an unsubscribe function.
|
|
254
|
+
*/
|
|
255
|
+
onError = (listener: UpdateErrorListener) => {
|
|
256
|
+
this.errorListeners.add(listener);
|
|
257
|
+
return () => {
|
|
258
|
+
this.errorListeners.delete(listener);
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Single exit point for errors: reports to the logger (with the stable
|
|
263
|
+
* code) and notifies onError listeners. Whether to also throw stays with
|
|
264
|
+
* the caller (throwIfEnabled or an unconditional rethrow).
|
|
265
|
+
*/
|
|
266
|
+
private emitError = (
|
|
267
|
+
error: UpdateError,
|
|
268
|
+
type: EventType,
|
|
269
|
+
{
|
|
270
|
+
message = error.message,
|
|
271
|
+
data,
|
|
272
|
+
}: { message?: string; data?: Record<string, string | number> } = {},
|
|
273
|
+
) => {
|
|
274
|
+
this.report({ type, message, code: error.code, data });
|
|
275
|
+
for (const listener of this.errorListeners) {
|
|
276
|
+
try {
|
|
277
|
+
listener(error, type);
|
|
278
|
+
} catch (e: any) {
|
|
279
|
+
log('onError listener error:', e?.message || e);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
233
283
|
notifyAfterCheckUpdate = (state: UpdateCheckState) => {
|
|
234
284
|
const { afterCheckUpdate } = this.options;
|
|
235
285
|
if (!afterCheckUpdate) {
|
|
@@ -302,11 +352,13 @@ export class Pushy {
|
|
|
302
352
|
|
|
303
353
|
if (!resp.ok) {
|
|
304
354
|
const respText = await resp.text();
|
|
305
|
-
throw
|
|
355
|
+
throw new UpdateError(
|
|
306
356
|
this.t('error_http_status', {
|
|
307
357
|
status: resp.status,
|
|
308
358
|
statusText: respText,
|
|
309
359
|
}),
|
|
360
|
+
'HTTP_STATUS',
|
|
361
|
+
{ extra: { status: resp.status } },
|
|
310
362
|
);
|
|
311
363
|
}
|
|
312
364
|
|
|
@@ -348,7 +400,13 @@ export class Pushy {
|
|
|
348
400
|
if (sharedState.marked || __DEV__ || !isFirstTime) {
|
|
349
401
|
return;
|
|
350
402
|
}
|
|
351
|
-
|
|
403
|
+
try {
|
|
404
|
+
await Promise.resolve(PushyModule.markSuccess());
|
|
405
|
+
} catch (e) {
|
|
406
|
+
const err = toUpdateError(e, 'MARK_SUCCESS_FAILED');
|
|
407
|
+
this.emitError(err, 'errorMarkSuccess');
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
352
410
|
sharedState.marked = true;
|
|
353
411
|
this.report({ type: 'markSuccess' });
|
|
354
412
|
};
|
|
@@ -366,7 +424,11 @@ export class Pushy {
|
|
|
366
424
|
}
|
|
367
425
|
} catch (e) {
|
|
368
426
|
sharedState.applyingUpdate = false;
|
|
369
|
-
|
|
427
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
428
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
429
|
+
data: { newVersion: hash },
|
|
430
|
+
});
|
|
431
|
+
throw err;
|
|
370
432
|
}
|
|
371
433
|
try {
|
|
372
434
|
return await PushyModule.reloadUpdate({ hash });
|
|
@@ -374,7 +436,11 @@ export class Pushy {
|
|
|
374
436
|
// reloadUpdate can reject (e.g. bundle missing); reset the flag so a
|
|
375
437
|
// later retry is not permanently blocked by a stuck applyingUpdate.
|
|
376
438
|
sharedState.applyingUpdate = false;
|
|
377
|
-
|
|
439
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
440
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
441
|
+
data: { newVersion: hash },
|
|
442
|
+
});
|
|
443
|
+
throw err;
|
|
378
444
|
}
|
|
379
445
|
}
|
|
380
446
|
};
|
|
@@ -385,7 +451,15 @@ export class Pushy {
|
|
|
385
451
|
}
|
|
386
452
|
if (assertHash(hash)) {
|
|
387
453
|
log(`switchVersionLater: ${hash}`);
|
|
388
|
-
|
|
454
|
+
try {
|
|
455
|
+
return await PushyModule.setNeedUpdate({ hash });
|
|
456
|
+
} catch (e) {
|
|
457
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
458
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
459
|
+
data: { newVersion: hash },
|
|
460
|
+
});
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
389
463
|
}
|
|
390
464
|
};
|
|
391
465
|
checkUpdate = async (extra?: Record<string, any>) => {
|
|
@@ -456,14 +530,12 @@ export class Pushy {
|
|
|
456
530
|
return result;
|
|
457
531
|
} catch (e: any) {
|
|
458
532
|
this.lastRespJson = previousRespJson;
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
type: 'errorChecking',
|
|
463
|
-
message: errorMessage,
|
|
533
|
+
const err = toUpdateError(e, 'CHECK_FAILED');
|
|
534
|
+
this.emitError(err, 'errorChecking', {
|
|
535
|
+
message: err.message || this.t('error_cannot_connect_server'),
|
|
464
536
|
});
|
|
465
|
-
this.notifyAfterCheckUpdate({ status: 'error', error:
|
|
466
|
-
this.throwIfEnabled(
|
|
537
|
+
this.notifyAfterCheckUpdate({ status: 'error', error: err });
|
|
538
|
+
this.throwIfEnabled(err);
|
|
467
539
|
// Fall back to the previous successful response if we have one; otherwise
|
|
468
540
|
// return undefined so callers can distinguish "check failed" from a real
|
|
469
541
|
// empty result and avoid overwriting the last good updateInfo.
|
|
@@ -669,14 +741,22 @@ export class Pushy {
|
|
|
669
741
|
delete sharedState.progressHandlers[hash];
|
|
670
742
|
}
|
|
671
743
|
if (!succeeded) {
|
|
744
|
+
const message = errorMessages.join(';');
|
|
745
|
+
if (lastError) {
|
|
746
|
+
const err = toUpdateError(lastError, 'DOWNLOAD_FAILED');
|
|
747
|
+
this.emitError(err, 'errorUpdate', {
|
|
748
|
+
message,
|
|
749
|
+
data: { newVersion: hash },
|
|
750
|
+
});
|
|
751
|
+
throw err;
|
|
752
|
+
}
|
|
753
|
+
// No download URL was even attempted (e.g. dev without a full URL):
|
|
754
|
+
// report for diagnostics but there is no error object to surface.
|
|
672
755
|
this.report({
|
|
673
756
|
type: 'errorUpdate',
|
|
674
757
|
data: { newVersion: hash },
|
|
675
|
-
message
|
|
758
|
+
message,
|
|
676
759
|
});
|
|
677
|
-
if (lastError) {
|
|
678
|
-
throw lastError;
|
|
679
|
-
}
|
|
680
760
|
return;
|
|
681
761
|
} else {
|
|
682
762
|
const duration = Date.now() - patchStartTime;
|
|
@@ -717,8 +797,12 @@ export class Pushy {
|
|
|
717
797
|
return;
|
|
718
798
|
}
|
|
719
799
|
if (sharedState.apkStatus === 'downloaded') {
|
|
720
|
-
|
|
721
|
-
|
|
800
|
+
const err = new UpdateError(
|
|
801
|
+
this.t('error_apk_pending_install'),
|
|
802
|
+
'APK_INSTALL_PENDING',
|
|
803
|
+
);
|
|
804
|
+
this.emitError(err, 'errorInstallApk');
|
|
805
|
+
this.throwIfEnabled(err);
|
|
722
806
|
return;
|
|
723
807
|
}
|
|
724
808
|
if (Platform.Version <= 23) {
|
|
@@ -727,13 +811,18 @@ export class Pushy {
|
|
|
727
811
|
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
728
812
|
);
|
|
729
813
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
730
|
-
|
|
731
|
-
|
|
814
|
+
const err = new UpdateError(
|
|
815
|
+
this.t('error_storage_permission_rejected'),
|
|
816
|
+
'STORAGE_PERMISSION_REJECTED',
|
|
817
|
+
);
|
|
818
|
+
this.emitError(err, 'rejectStoragePermission');
|
|
819
|
+
this.throwIfEnabled(err);
|
|
732
820
|
return;
|
|
733
821
|
}
|
|
734
822
|
} catch (e: any) {
|
|
735
|
-
|
|
736
|
-
this.
|
|
823
|
+
const err = toUpdateError(e, 'STORAGE_PERMISSION_ERROR');
|
|
824
|
+
this.emitError(err, 'errorStoragePermission');
|
|
825
|
+
this.throwIfEnabled(err);
|
|
737
826
|
return;
|
|
738
827
|
}
|
|
739
828
|
}
|
|
@@ -761,10 +850,14 @@ export class Pushy {
|
|
|
761
850
|
hash: progressKey,
|
|
762
851
|
});
|
|
763
852
|
sharedState.apkStatus = 'downloaded';
|
|
764
|
-
} catch {
|
|
853
|
+
} catch (e) {
|
|
765
854
|
sharedState.apkStatus = null;
|
|
766
|
-
|
|
767
|
-
|
|
855
|
+
// Keep the native error (message/stack) instead of discarding it.
|
|
856
|
+
const err = toUpdateError(e, 'APK_DOWNLOAD_FAILED');
|
|
857
|
+
this.emitError(err, 'errorDownloadAndInstallApk', {
|
|
858
|
+
message: err.message || this.t('error_apk_download_failed'),
|
|
859
|
+
});
|
|
860
|
+
this.throwIfEnabled(err);
|
|
768
861
|
} finally {
|
|
769
862
|
if (sharedState.progressHandlers[progressKey]) {
|
|
770
863
|
sharedState.progressHandlers[progressKey].remove();
|
package/src/core.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
|
2
2
|
import i18n from './i18n';
|
|
3
|
+
import { UpdateError } from './error';
|
|
3
4
|
import { emptyModule, error, log } from './utils';
|
|
4
5
|
|
|
5
6
|
/* eslint-disable @react-native/no-deep-imports */
|
|
@@ -21,8 +22,9 @@ export const PushyModule =
|
|
|
21
22
|
export const UpdateModule = PushyModule;
|
|
22
23
|
|
|
23
24
|
if (!PushyModule) {
|
|
24
|
-
throw
|
|
25
|
+
throw new UpdateError(
|
|
25
26
|
'Failed to load react-native-update native module, please try to recompile',
|
|
27
|
+
'MODULE_NOT_LOADED',
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
package/src/endpoint.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { UpdateError } from './error';
|
|
2
|
+
|
|
1
3
|
export interface EndpointAttemptSuccess<T> {
|
|
2
4
|
endpoint: string;
|
|
3
5
|
value: T;
|
|
@@ -47,7 +49,7 @@ export const pickRandomEndpoint = (
|
|
|
47
49
|
random: () => number = Math.random,
|
|
48
50
|
) => {
|
|
49
51
|
if (!endpoints.length) {
|
|
50
|
-
throw new
|
|
52
|
+
throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
|
|
51
53
|
}
|
|
52
54
|
return endpoints[Math.floor(random() * endpoints.length)];
|
|
53
55
|
};
|
|
@@ -120,7 +122,7 @@ export async function executeEndpointFallback<T>({
|
|
|
120
122
|
let candidates = dedupeEndpoints(configuredEndpoints);
|
|
121
123
|
|
|
122
124
|
if (!candidates.length) {
|
|
123
|
-
throw new
|
|
125
|
+
throw new UpdateError('No endpoints configured', 'NO_ENDPOINTS');
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
const firstEndpoint = pickRandomEndpoint(candidates, random);
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, machine-readable error codes. Unlike messages (which are localized
|
|
3
|
+
* and vary across platforms), codes are safe to aggregate on in a logger.
|
|
4
|
+
*
|
|
5
|
+
* The native-originated codes (second group) are defined in
|
|
6
|
+
* cpp/patch_core/error_codes.h — the single source of truth shared by the
|
|
7
|
+
* Android/iOS/Harmony modules — and flow through promise rejections onto the
|
|
8
|
+
* `code` property, which toUpdateError() preserves.
|
|
9
|
+
*/
|
|
10
|
+
export type UpdateErrorCode =
|
|
11
|
+
// JS-layer codes
|
|
12
|
+
| 'MODULE_NOT_LOADED'
|
|
13
|
+
| 'APPKEY_REQUIRED'
|
|
14
|
+
| 'NO_ENDPOINTS'
|
|
15
|
+
| 'HTTP_STATUS'
|
|
16
|
+
| 'CHECK_FAILED'
|
|
17
|
+
| 'DOWNLOAD_FAILED'
|
|
18
|
+
| 'SWITCH_VERSION_FAILED'
|
|
19
|
+
| 'MARK_SUCCESS_FAILED'
|
|
20
|
+
| 'APK_INSTALL_PENDING'
|
|
21
|
+
| 'STORAGE_PERMISSION_REJECTED'
|
|
22
|
+
| 'STORAGE_PERMISSION_ERROR'
|
|
23
|
+
| 'APK_DOWNLOAD_FAILED'
|
|
24
|
+
// Native codes (see cpp/patch_core/error_codes.h)
|
|
25
|
+
| 'INVALID_OPTIONS'
|
|
26
|
+
| 'PATCH_FAILED'
|
|
27
|
+
| 'FILE_OPERATION_FAILED'
|
|
28
|
+
| 'RESTART_FAILED'
|
|
29
|
+
| 'INVALID_HASH_INFO'
|
|
30
|
+
| 'UNSUPPORTED_PLATFORM';
|
|
31
|
+
|
|
32
|
+
export class UpdateError extends Error {
|
|
33
|
+
code: UpdateErrorCode;
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
extra?: Record<string, string | number>;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
message: string,
|
|
39
|
+
code: UpdateErrorCode,
|
|
40
|
+
options?: { cause?: unknown; extra?: Record<string, string | number> },
|
|
41
|
+
) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'UpdateError';
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.cause = options?.cause;
|
|
46
|
+
this.extra = options?.extra;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attach a code to an unknown thrown value. An existing Error keeps its
|
|
52
|
+
* identity (message, stack, and any code already assigned upstream) so callers
|
|
53
|
+
* comparing the caught error to the original still match; non-Error values are
|
|
54
|
+
* wrapped.
|
|
55
|
+
*/
|
|
56
|
+
export const toUpdateError = (
|
|
57
|
+
e: unknown,
|
|
58
|
+
code: UpdateErrorCode,
|
|
59
|
+
): UpdateError => {
|
|
60
|
+
if (e instanceof Error) {
|
|
61
|
+
const err = e as UpdateError;
|
|
62
|
+
if (!err.code) {
|
|
63
|
+
err.code = code;
|
|
64
|
+
}
|
|
65
|
+
return err;
|
|
66
|
+
}
|
|
67
|
+
return new UpdateError(String(e ?? code), code);
|
|
68
|
+
};
|
package/src/index.ts
CHANGED
package/src/locales/en.ts
CHANGED
|
@@ -19,7 +19,6 @@ export default {
|
|
|
19
19
|
time_remaining: 'Time remaining: {{time}}',
|
|
20
20
|
|
|
21
21
|
// Error messages
|
|
22
|
-
error_code: 'Error code: {{code}}',
|
|
23
22
|
error_message: 'Error message: {{message}}',
|
|
24
23
|
retry_count: 'Retry attempt: {{count}}/{{max}}',
|
|
25
24
|
|
|
@@ -57,6 +56,11 @@ export default {
|
|
|
57
56
|
error_ping_failed: 'Ping failed',
|
|
58
57
|
error_ping_timeout: 'Ping timeout',
|
|
59
58
|
error_http_status: '{{status}} {{statusText}}',
|
|
59
|
+
error_apk_pending_install:
|
|
60
|
+
'The APK has been downloaded, please complete the installation in the system installer',
|
|
61
|
+
error_storage_permission_rejected:
|
|
62
|
+
'Storage permission denied, unable to download the APK',
|
|
63
|
+
error_apk_download_failed: 'Failed to download or install the APK',
|
|
60
64
|
|
|
61
65
|
// Development messages
|
|
62
66
|
dev_debug_disabled:
|
package/src/locales/zh.ts
CHANGED
|
@@ -19,7 +19,6 @@ export default {
|
|
|
19
19
|
time_remaining: '剩余时间: {{time}}',
|
|
20
20
|
|
|
21
21
|
// Error messages
|
|
22
|
-
error_code: '错误代码: {{code}}',
|
|
23
22
|
error_message: '错误信息: {{message}}',
|
|
24
23
|
retry_count: '重试次数: {{count}}/{{max}}',
|
|
25
24
|
|
|
@@ -54,6 +53,9 @@ export default {
|
|
|
54
53
|
error_ping_failed: 'Ping 失败',
|
|
55
54
|
error_ping_timeout: 'Ping 超时',
|
|
56
55
|
error_http_status: '{{status}} {{statusText}}',
|
|
56
|
+
error_apk_pending_install: '安装包已下载完成,请在系统安装界面完成安装',
|
|
57
|
+
error_storage_permission_rejected: '存储权限被拒绝,无法下载安装包',
|
|
58
|
+
error_apk_download_failed: '安装包下载或安装失败',
|
|
57
59
|
|
|
58
60
|
// Development messages
|
|
59
61
|
dev_debug_disabled:
|
package/src/provider.tsx
CHANGED
|
@@ -46,15 +46,6 @@ export const UpdateProvider = ({
|
|
|
46
46
|
const [progress, setProgress] = useState<ProgressData>();
|
|
47
47
|
const [lastError, setLastError] = useState<Error>();
|
|
48
48
|
|
|
49
|
-
const throwErrorIfEnabled = useCallback(
|
|
50
|
-
(e: Error) => {
|
|
51
|
-
if (options.throwError) {
|
|
52
|
-
throw e;
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
[options.throwError],
|
|
56
|
-
);
|
|
57
|
-
|
|
58
49
|
const dismissError = useCallback(() => {
|
|
59
50
|
setLastError(undefined);
|
|
60
51
|
}, []);
|
|
@@ -80,6 +71,25 @@ export const UpdateProvider = ({
|
|
|
80
71
|
[options.updateStrategy],
|
|
81
72
|
);
|
|
82
73
|
|
|
74
|
+
// All client errors flow through this single subscription (regardless of
|
|
75
|
+
// throwError), so the catches below only handle flow control and never
|
|
76
|
+
// duplicate the lastError/Alert surfacing.
|
|
77
|
+
useEffect(
|
|
78
|
+
() =>
|
|
79
|
+
client.onError((e, eventType) => {
|
|
80
|
+
setLastError(e);
|
|
81
|
+
alertError(
|
|
82
|
+
client.t(
|
|
83
|
+
eventType === 'errorChecking'
|
|
84
|
+
? 'error_update_check_failed'
|
|
85
|
+
: 'update_failed',
|
|
86
|
+
),
|
|
87
|
+
e.message,
|
|
88
|
+
);
|
|
89
|
+
}),
|
|
90
|
+
[client, alertError],
|
|
91
|
+
);
|
|
92
|
+
|
|
83
93
|
const switchVersion = useCallback(
|
|
84
94
|
async (info: CheckResult | undefined = updateInfoRef.current) => {
|
|
85
95
|
if (info && info.hash) {
|
|
@@ -117,10 +127,11 @@ export const UpdateProvider = ({
|
|
|
117
127
|
return false;
|
|
118
128
|
}
|
|
119
129
|
if (options.updateStrategy === 'silentAndNow') {
|
|
120
|
-
|
|
130
|
+
// Failures are surfaced via the onError subscription above.
|
|
131
|
+
client.switchVersion(hash).catch(noop);
|
|
121
132
|
return true;
|
|
122
133
|
} else if (options.updateStrategy === 'silentAndLater') {
|
|
123
|
-
client.switchVersionLater(hash);
|
|
134
|
+
client.switchVersionLater(hash).catch(noop);
|
|
124
135
|
return true;
|
|
125
136
|
}
|
|
126
137
|
alertUpdate(client.t('alert_title'), client.t('alert_update_ready'), [
|
|
@@ -128,26 +139,33 @@ export const UpdateProvider = ({
|
|
|
128
139
|
text: client.t('alert_next_time'),
|
|
129
140
|
style: 'cancel',
|
|
130
141
|
onPress: () => {
|
|
131
|
-
client.switchVersionLater(hash);
|
|
142
|
+
client.switchVersionLater(hash).catch(noop);
|
|
132
143
|
},
|
|
133
144
|
},
|
|
134
145
|
{
|
|
135
146
|
text: client.t('alert_update_now'),
|
|
136
147
|
style: 'default',
|
|
137
148
|
onPress: () => {
|
|
138
|
-
client.switchVersion(hash);
|
|
149
|
+
client.switchVersion(hash).catch(noop);
|
|
139
150
|
},
|
|
140
151
|
},
|
|
141
152
|
]);
|
|
142
153
|
return true;
|
|
143
154
|
} catch (e: any) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
// Client pipeline errors carry a code and were already surfaced via
|
|
156
|
+
// the onError subscription; errors thrown by user hooks
|
|
157
|
+
// (afterDownloadUpdate) bypass the pipeline and are surfaced here.
|
|
158
|
+
if (!e?.code) {
|
|
159
|
+
setLastError(e);
|
|
160
|
+
alertError(client.t('update_failed'), e.message);
|
|
161
|
+
}
|
|
162
|
+
if (options.throwError) {
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
147
165
|
return false;
|
|
148
166
|
}
|
|
149
167
|
},
|
|
150
|
-
[client, options, alertUpdate, alertError
|
|
168
|
+
[client, options, alertUpdate, alertError],
|
|
151
169
|
);
|
|
152
170
|
|
|
153
171
|
const downloadAndInstallApk = useCallback(
|
|
@@ -168,9 +186,16 @@ export const UpdateProvider = ({
|
|
|
168
186
|
try {
|
|
169
187
|
rootInfo = await client.checkUpdate(extra);
|
|
170
188
|
} catch (e: any) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
// Client pipeline errors carry a code and were already surfaced via
|
|
190
|
+
// the onError subscription; errors thrown by user hooks
|
|
191
|
+
// (beforeCheckUpdate) bypass the pipeline and are surfaced here.
|
|
192
|
+
if (!e?.code) {
|
|
193
|
+
setLastError(e);
|
|
194
|
+
alertError(client.t('error_update_check_failed'), e.message);
|
|
195
|
+
}
|
|
196
|
+
if (options.throwError) {
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
174
199
|
return;
|
|
175
200
|
}
|
|
176
201
|
if (!rootInfo) {
|
|
@@ -252,10 +277,9 @@ export const UpdateProvider = ({
|
|
|
252
277
|
},
|
|
253
278
|
[
|
|
254
279
|
client,
|
|
255
|
-
alertError,
|
|
256
|
-
throwErrorIfEnabled,
|
|
257
280
|
options,
|
|
258
281
|
alertUpdate,
|
|
282
|
+
alertError,
|
|
259
283
|
downloadAndInstallApk,
|
|
260
284
|
downloadUpdate,
|
|
261
285
|
],
|
|
@@ -274,7 +298,8 @@ export const UpdateProvider = ({
|
|
|
274
298
|
let markSuccessTimer: ReturnType<typeof setTimeout> | undefined;
|
|
275
299
|
if (autoMarkSuccess) {
|
|
276
300
|
markSuccessTimer = setTimeout(() => {
|
|
277
|
-
|
|
301
|
+
// Failures are reported and surfaced via the onError subscription.
|
|
302
|
+
Promise.resolve(markSuccess()).catch(noop);
|
|
278
303
|
}, 1000);
|
|
279
304
|
}
|
|
280
305
|
if (checkStrategy === 'both' || checkStrategy === 'onAppResume') {
|
|
@@ -351,7 +376,8 @@ export const UpdateProvider = ({
|
|
|
351
376
|
try {
|
|
352
377
|
const payload = typeof code === 'string' ? JSON.parse(code) : code;
|
|
353
378
|
return parseTestPayload(payload);
|
|
354
|
-
} catch {
|
|
379
|
+
} catch (e: any) {
|
|
380
|
+
log('parseTestQrCode: invalid payload', e?.message || e);
|
|
355
381
|
return false;
|
|
356
382
|
}
|
|
357
383
|
},
|
package/src/type.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { UpdateErrorCode } from './error';
|
|
2
|
+
|
|
1
3
|
export interface VersionInfo {
|
|
2
4
|
name: string;
|
|
3
5
|
hash: string;
|
|
@@ -52,6 +54,8 @@ export type EventType =
|
|
|
52
54
|
| 'downloadSuccess'
|
|
53
55
|
| 'errorUpdate'
|
|
54
56
|
| 'markSuccess'
|
|
57
|
+
| 'errorMarkSuccess'
|
|
58
|
+
| 'errorSwitchVersion'
|
|
55
59
|
| 'downloadingApk'
|
|
56
60
|
| 'rejectStoragePermission'
|
|
57
61
|
| 'errorStoragePermission'
|
|
@@ -59,6 +63,8 @@ export type EventType =
|
|
|
59
63
|
| 'errorInstallApk';
|
|
60
64
|
|
|
61
65
|
export interface EventData {
|
|
66
|
+
/** Stable machine-readable error code; present on error events */
|
|
67
|
+
code?: UpdateErrorCode;
|
|
62
68
|
currentVersion: string;
|
|
63
69
|
cInfo: {
|
|
64
70
|
rnu: string;
|