react-native-update 10.45.0 → 10.47.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 +197 -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/telemetry.ts +83 -0
- package/src/type.ts +12 -0
package/src/client.ts
CHANGED
|
@@ -39,7 +39,23 @@ 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';
|
|
44
|
+
import {
|
|
45
|
+
resolveServerEventHash,
|
|
46
|
+
resolveServerEventType,
|
|
47
|
+
truncateDetail,
|
|
48
|
+
} from './telemetry';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Receives every error the client reports, alongside the report event type.
|
|
52
|
+
* The UpdateProvider subscribes to surface errors as lastError/Alert; user
|
|
53
|
+
* code can subscribe too. Listeners run before any throwError rethrow.
|
|
54
|
+
*/
|
|
55
|
+
export type UpdateErrorListener = (
|
|
56
|
+
error: UpdateError,
|
|
57
|
+
eventType: EventType,
|
|
58
|
+
) => void;
|
|
43
59
|
|
|
44
60
|
const SERVER_PRESETS = {
|
|
45
61
|
// cn
|
|
@@ -119,6 +135,9 @@ export class Pushy {
|
|
|
119
135
|
clientType: 'Pushy' | 'Cresc' = 'Pushy';
|
|
120
136
|
lastChecking?: number;
|
|
121
137
|
lastRespJson?: Promise<CheckResult>;
|
|
138
|
+
// Endpoint that most recently served a successful checkUpdate; telemetry
|
|
139
|
+
// reuses it instead of re-running the fallback race.
|
|
140
|
+
private lastWorkingEndpoint?: string;
|
|
122
141
|
|
|
123
142
|
version = cInfo.rnu;
|
|
124
143
|
loggerPromise = (() => {
|
|
@@ -142,7 +161,10 @@ export class Pushy {
|
|
|
142
161
|
|
|
143
162
|
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
144
163
|
if (!options.appKey) {
|
|
145
|
-
throw
|
|
164
|
+
throw new UpdateError(
|
|
165
|
+
i18n.t('error_appkey_required'),
|
|
166
|
+
'APPKEY_REQUIRED',
|
|
167
|
+
);
|
|
146
168
|
}
|
|
147
169
|
}
|
|
148
170
|
|
|
@@ -184,13 +206,17 @@ export class Pushy {
|
|
|
184
206
|
report = async ({
|
|
185
207
|
type,
|
|
186
208
|
message = '',
|
|
209
|
+
code,
|
|
187
210
|
data = {},
|
|
188
211
|
}: {
|
|
189
212
|
type: EventType;
|
|
190
213
|
message?: string;
|
|
214
|
+
code?: UpdateErrorCode;
|
|
191
215
|
data?: Record<string, string | number>;
|
|
192
216
|
}) => {
|
|
193
|
-
log(`${type} ${message}`);
|
|
217
|
+
log(`${type} ${code ? `[${code}] ` : ''}${message}`);
|
|
218
|
+
// Fire-and-forget server telemetry; must not wait for the logger below.
|
|
219
|
+
this.reportToServer({ type, message, code, data });
|
|
194
220
|
if (this.options.logger === noop) {
|
|
195
221
|
// Wait briefly for a logger to arrive via setOptions (e.g. the rollback
|
|
196
222
|
// report fires in the constructor before the user configures one), but
|
|
@@ -214,6 +240,7 @@ export class Pushy {
|
|
|
214
240
|
overridePackageVersion,
|
|
215
241
|
buildTime,
|
|
216
242
|
message,
|
|
243
|
+
code,
|
|
217
244
|
...currentVersionInfo,
|
|
218
245
|
...data,
|
|
219
246
|
},
|
|
@@ -225,11 +252,110 @@ export class Pushy {
|
|
|
225
252
|
log('logger error:', e?.message || e);
|
|
226
253
|
}
|
|
227
254
|
};
|
|
255
|
+
/**
|
|
256
|
+
* Best-effort lifecycle event reporting to the update server (aggregate
|
|
257
|
+
* counts + sampled failure details power the version health view and the
|
|
258
|
+
* rollback safety net server-side). Single POST to the last known working
|
|
259
|
+
* endpoint, no retry, no fallback race; any failure is swallowed — telemetry
|
|
260
|
+
* must never affect the update flow. Opt out with disableTelemetry.
|
|
261
|
+
*/
|
|
262
|
+
private reportToServer = ({
|
|
263
|
+
type,
|
|
264
|
+
message = '',
|
|
265
|
+
code,
|
|
266
|
+
data = {},
|
|
267
|
+
}: {
|
|
268
|
+
type: EventType;
|
|
269
|
+
message?: string;
|
|
270
|
+
code?: UpdateErrorCode;
|
|
271
|
+
data?: Record<string, string | number>;
|
|
272
|
+
}) => {
|
|
273
|
+
try {
|
|
274
|
+
if (__DEV__ || this.options.disableTelemetry) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const serverType = resolveServerEventType(type, code);
|
|
278
|
+
if (!serverType) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const { appKey } = this.options;
|
|
282
|
+
const endpoint =
|
|
283
|
+
this.lastWorkingEndpoint || this.options.server?.main?.[0];
|
|
284
|
+
if (!appKey || !endpoint) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const hash = resolveServerEventHash({ serverType, data, currentVersion });
|
|
288
|
+
if (!hash) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const send = (payloadType: typeof serverType, detail?: string) =>
|
|
292
|
+
fetchWithTimeout(
|
|
293
|
+
`${endpoint}/report/${appKey}`,
|
|
294
|
+
{
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
type: payloadType,
|
|
299
|
+
hash,
|
|
300
|
+
packageVersion:
|
|
301
|
+
this.options.overridePackageVersion || packageVersion,
|
|
302
|
+
cInfo,
|
|
303
|
+
detail: truncateDetail(detail),
|
|
304
|
+
}),
|
|
305
|
+
},
|
|
306
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
307
|
+
).catch((e: any) => {
|
|
308
|
+
log('telemetry report failed:', e?.message || e);
|
|
309
|
+
});
|
|
310
|
+
send(serverType, message || undefined);
|
|
311
|
+
// A download that only succeeded after an incremental patch failed is
|
|
312
|
+
// still a patch_fail signal server-side (diff quality), carried in
|
|
313
|
+
// data.error alongside the downloadSuccess event.
|
|
314
|
+
if (serverType === 'download_success' && data.error) {
|
|
315
|
+
send('patch_fail', String(data.error));
|
|
316
|
+
}
|
|
317
|
+
} catch (e: any) {
|
|
318
|
+
log('telemetry error:', e?.message || e);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
228
321
|
throwIfEnabled = (e: Error) => {
|
|
229
322
|
if (this.options.throwError) {
|
|
230
323
|
throw e;
|
|
231
324
|
}
|
|
232
325
|
};
|
|
326
|
+
private errorListeners = new Set<UpdateErrorListener>();
|
|
327
|
+
/**
|
|
328
|
+
* Subscribe to every error the client reports (regardless of throwError).
|
|
329
|
+
* Returns an unsubscribe function.
|
|
330
|
+
*/
|
|
331
|
+
onError = (listener: UpdateErrorListener) => {
|
|
332
|
+
this.errorListeners.add(listener);
|
|
333
|
+
return () => {
|
|
334
|
+
this.errorListeners.delete(listener);
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Single exit point for errors: reports to the logger (with the stable
|
|
339
|
+
* code) and notifies onError listeners. Whether to also throw stays with
|
|
340
|
+
* the caller (throwIfEnabled or an unconditional rethrow).
|
|
341
|
+
*/
|
|
342
|
+
private emitError = (
|
|
343
|
+
error: UpdateError,
|
|
344
|
+
type: EventType,
|
|
345
|
+
{
|
|
346
|
+
message = error.message,
|
|
347
|
+
data,
|
|
348
|
+
}: { message?: string; data?: Record<string, string | number> } = {},
|
|
349
|
+
) => {
|
|
350
|
+
this.report({ type, message, code: error.code, data });
|
|
351
|
+
for (const listener of this.errorListeners) {
|
|
352
|
+
try {
|
|
353
|
+
listener(error, type);
|
|
354
|
+
} catch (e: any) {
|
|
355
|
+
log('onError listener error:', e?.message || e);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
233
359
|
notifyAfterCheckUpdate = (state: UpdateCheckState) => {
|
|
234
360
|
const { afterCheckUpdate } = this.options;
|
|
235
361
|
if (!afterCheckUpdate) {
|
|
@@ -302,11 +428,13 @@ export class Pushy {
|
|
|
302
428
|
|
|
303
429
|
if (!resp.ok) {
|
|
304
430
|
const respText = await resp.text();
|
|
305
|
-
throw
|
|
431
|
+
throw new UpdateError(
|
|
306
432
|
this.t('error_http_status', {
|
|
307
433
|
status: resp.status,
|
|
308
434
|
statusText: respText,
|
|
309
435
|
}),
|
|
436
|
+
'HTTP_STATUS',
|
|
437
|
+
{ extra: { status: resp.status } },
|
|
310
438
|
);
|
|
311
439
|
}
|
|
312
440
|
|
|
@@ -335,6 +463,7 @@ export class Pushy {
|
|
|
335
463
|
});
|
|
336
464
|
|
|
337
465
|
log('check endpoint success', endpoint);
|
|
466
|
+
this.lastWorkingEndpoint = endpoint;
|
|
338
467
|
return value;
|
|
339
468
|
};
|
|
340
469
|
assertDebug = (matter: string) => {
|
|
@@ -348,7 +477,13 @@ export class Pushy {
|
|
|
348
477
|
if (sharedState.marked || __DEV__ || !isFirstTime) {
|
|
349
478
|
return;
|
|
350
479
|
}
|
|
351
|
-
|
|
480
|
+
try {
|
|
481
|
+
await Promise.resolve(PushyModule.markSuccess());
|
|
482
|
+
} catch (e) {
|
|
483
|
+
const err = toUpdateError(e, 'MARK_SUCCESS_FAILED');
|
|
484
|
+
this.emitError(err, 'errorMarkSuccess');
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
352
487
|
sharedState.marked = true;
|
|
353
488
|
this.report({ type: 'markSuccess' });
|
|
354
489
|
};
|
|
@@ -366,7 +501,11 @@ export class Pushy {
|
|
|
366
501
|
}
|
|
367
502
|
} catch (e) {
|
|
368
503
|
sharedState.applyingUpdate = false;
|
|
369
|
-
|
|
504
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
505
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
506
|
+
data: { newVersion: hash },
|
|
507
|
+
});
|
|
508
|
+
throw err;
|
|
370
509
|
}
|
|
371
510
|
try {
|
|
372
511
|
return await PushyModule.reloadUpdate({ hash });
|
|
@@ -374,7 +513,11 @@ export class Pushy {
|
|
|
374
513
|
// reloadUpdate can reject (e.g. bundle missing); reset the flag so a
|
|
375
514
|
// later retry is not permanently blocked by a stuck applyingUpdate.
|
|
376
515
|
sharedState.applyingUpdate = false;
|
|
377
|
-
|
|
516
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
517
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
518
|
+
data: { newVersion: hash },
|
|
519
|
+
});
|
|
520
|
+
throw err;
|
|
378
521
|
}
|
|
379
522
|
}
|
|
380
523
|
};
|
|
@@ -385,7 +528,15 @@ export class Pushy {
|
|
|
385
528
|
}
|
|
386
529
|
if (assertHash(hash)) {
|
|
387
530
|
log(`switchVersionLater: ${hash}`);
|
|
388
|
-
|
|
531
|
+
try {
|
|
532
|
+
return await PushyModule.setNeedUpdate({ hash });
|
|
533
|
+
} catch (e) {
|
|
534
|
+
const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
|
|
535
|
+
this.emitError(err, 'errorSwitchVersion', {
|
|
536
|
+
data: { newVersion: hash },
|
|
537
|
+
});
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
389
540
|
}
|
|
390
541
|
};
|
|
391
542
|
checkUpdate = async (extra?: Record<string, any>) => {
|
|
@@ -456,14 +607,12 @@ export class Pushy {
|
|
|
456
607
|
return result;
|
|
457
608
|
} catch (e: any) {
|
|
458
609
|
this.lastRespJson = previousRespJson;
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
type: 'errorChecking',
|
|
463
|
-
message: errorMessage,
|
|
610
|
+
const err = toUpdateError(e, 'CHECK_FAILED');
|
|
611
|
+
this.emitError(err, 'errorChecking', {
|
|
612
|
+
message: err.message || this.t('error_cannot_connect_server'),
|
|
464
613
|
});
|
|
465
|
-
this.notifyAfterCheckUpdate({ status: 'error', error:
|
|
466
|
-
this.throwIfEnabled(
|
|
614
|
+
this.notifyAfterCheckUpdate({ status: 'error', error: err });
|
|
615
|
+
this.throwIfEnabled(err);
|
|
467
616
|
// Fall back to the previous successful response if we have one; otherwise
|
|
468
617
|
// return undefined so callers can distinguish "check failed" from a real
|
|
469
618
|
// empty result and avoid overwriting the last good updateInfo.
|
|
@@ -669,14 +818,22 @@ export class Pushy {
|
|
|
669
818
|
delete sharedState.progressHandlers[hash];
|
|
670
819
|
}
|
|
671
820
|
if (!succeeded) {
|
|
821
|
+
const message = errorMessages.join(';');
|
|
822
|
+
if (lastError) {
|
|
823
|
+
const err = toUpdateError(lastError, 'DOWNLOAD_FAILED');
|
|
824
|
+
this.emitError(err, 'errorUpdate', {
|
|
825
|
+
message,
|
|
826
|
+
data: { newVersion: hash },
|
|
827
|
+
});
|
|
828
|
+
throw err;
|
|
829
|
+
}
|
|
830
|
+
// No download URL was even attempted (e.g. dev without a full URL):
|
|
831
|
+
// report for diagnostics but there is no error object to surface.
|
|
672
832
|
this.report({
|
|
673
833
|
type: 'errorUpdate',
|
|
674
834
|
data: { newVersion: hash },
|
|
675
|
-
message
|
|
835
|
+
message,
|
|
676
836
|
});
|
|
677
|
-
if (lastError) {
|
|
678
|
-
throw lastError;
|
|
679
|
-
}
|
|
680
837
|
return;
|
|
681
838
|
} else {
|
|
682
839
|
const duration = Date.now() - patchStartTime;
|
|
@@ -717,8 +874,12 @@ export class Pushy {
|
|
|
717
874
|
return;
|
|
718
875
|
}
|
|
719
876
|
if (sharedState.apkStatus === 'downloaded') {
|
|
720
|
-
|
|
721
|
-
|
|
877
|
+
const err = new UpdateError(
|
|
878
|
+
this.t('error_apk_pending_install'),
|
|
879
|
+
'APK_INSTALL_PENDING',
|
|
880
|
+
);
|
|
881
|
+
this.emitError(err, 'errorInstallApk');
|
|
882
|
+
this.throwIfEnabled(err);
|
|
722
883
|
return;
|
|
723
884
|
}
|
|
724
885
|
if (Platform.Version <= 23) {
|
|
@@ -727,13 +888,18 @@ export class Pushy {
|
|
|
727
888
|
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
728
889
|
);
|
|
729
890
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
730
|
-
|
|
731
|
-
|
|
891
|
+
const err = new UpdateError(
|
|
892
|
+
this.t('error_storage_permission_rejected'),
|
|
893
|
+
'STORAGE_PERMISSION_REJECTED',
|
|
894
|
+
);
|
|
895
|
+
this.emitError(err, 'rejectStoragePermission');
|
|
896
|
+
this.throwIfEnabled(err);
|
|
732
897
|
return;
|
|
733
898
|
}
|
|
734
899
|
} catch (e: any) {
|
|
735
|
-
|
|
736
|
-
this.
|
|
900
|
+
const err = toUpdateError(e, 'STORAGE_PERMISSION_ERROR');
|
|
901
|
+
this.emitError(err, 'errorStoragePermission');
|
|
902
|
+
this.throwIfEnabled(err);
|
|
737
903
|
return;
|
|
738
904
|
}
|
|
739
905
|
}
|
|
@@ -761,10 +927,14 @@ export class Pushy {
|
|
|
761
927
|
hash: progressKey,
|
|
762
928
|
});
|
|
763
929
|
sharedState.apkStatus = 'downloaded';
|
|
764
|
-
} catch {
|
|
930
|
+
} catch (e) {
|
|
765
931
|
sharedState.apkStatus = null;
|
|
766
|
-
|
|
767
|
-
|
|
932
|
+
// Keep the native error (message/stack) instead of discarding it.
|
|
933
|
+
const err = toUpdateError(e, 'APK_DOWNLOAD_FAILED');
|
|
934
|
+
this.emitError(err, 'errorDownloadAndInstallApk', {
|
|
935
|
+
message: err.message || this.t('error_apk_download_failed'),
|
|
936
|
+
});
|
|
937
|
+
this.throwIfEnabled(err);
|
|
768
938
|
} finally {
|
|
769
939
|
if (sharedState.progressHandlers[progressKey]) {
|
|
770
940
|
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
|
},
|