react-native-update 10.44.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.
Files changed (37) hide show
  1. package/android/jni/Android.mk +0 -1
  2. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  3. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  4. package/android/lib/x86/librnupdate.so +0 -0
  5. package/android/lib/x86_64/librnupdate.so +0 -0
  6. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +10 -1
  7. package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +24 -0
  8. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +2 -1
  9. package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +2 -1
  10. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +19 -4
  11. package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
  12. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +15 -15
  13. package/cpp/patch_core/error_codes.h +43 -0
  14. package/cpp/patch_core/patch_core.cpp +16 -1
  15. package/cpp/patch_core/patch_core.h +6 -0
  16. package/cpp/patch_core/tests/patch_core_test.cpp +77 -0
  17. package/harmony/pushy/src/main/cpp/pushy.cpp +1 -93
  18. package/harmony/pushy/src/main/ets/DownloadTask.ts +13 -10
  19. package/harmony/pushy/src/main/ets/NativePatchCore.ts +0 -4
  20. package/harmony/pushy/src/main/ets/UpdateContext.ts +20 -2
  21. package/harmony/pushy.har +0 -0
  22. package/ios/RCTPushy/RCTPushy.mm +223 -132
  23. package/ios/RCTPushy/RCTPushyDownloader.mm +22 -1
  24. package/package.json +7 -3
  25. package/src/client.ts +151 -53
  26. package/src/context.ts +21 -7
  27. package/src/core.ts +8 -2
  28. package/src/endpoint.ts +4 -2
  29. package/src/error.ts +68 -0
  30. package/src/index.ts +10 -1
  31. package/src/locales/en.ts +5 -1
  32. package/src/locales/zh.ts +3 -1
  33. package/src/provider.tsx +101 -54
  34. package/src/type.ts +6 -0
  35. package/src/utils.ts +56 -16
  36. package/android/jni/DownloadTask.c +0 -56
  37. package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
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 Error(i18n.t('error_appkey_required'));
156
+ throw new UpdateError(
157
+ i18n.t('error_appkey_required'),
158
+ 'APPKEY_REQUIRED',
159
+ );
146
160
  }
147
161
  }
148
162
 
@@ -184,36 +198,88 @@ 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}`);
194
- await this.loggerPromise.promise;
209
+ log(`${type} ${code ? `[${code}] ` : ''}${message}`);
210
+ if (this.options.logger === noop) {
211
+ // Wait briefly for a logger to arrive via setOptions (e.g. the rollback
212
+ // report fires in the constructor before the user configures one), but
213
+ // give up after a bound instead of retaining the closure forever when
214
+ // no logger is ever provided.
215
+ await Promise.race([
216
+ this.loggerPromise.promise,
217
+ new Promise(resolve => setTimeout(resolve, 10 * 1000)),
218
+ ]);
219
+ }
195
220
  const { logger = noop, appKey } = this.options;
196
221
  const overridePackageVersion = this.options.overridePackageVersion;
197
- logger({
198
- type,
199
- data: {
200
- appKey,
201
- currentVersion,
202
- cInfo,
203
- packageVersion,
204
- overridePackageVersion,
205
- buildTime,
206
- message,
207
- ...currentVersionInfo,
208
- ...data,
209
- },
210
- });
222
+ try {
223
+ logger({
224
+ type,
225
+ data: {
226
+ appKey,
227
+ currentVersion,
228
+ cInfo,
229
+ packageVersion,
230
+ overridePackageVersion,
231
+ buildTime,
232
+ message,
233
+ code,
234
+ ...currentVersionInfo,
235
+ ...data,
236
+ },
237
+ });
238
+ } catch (e: any) {
239
+ // A user-provided logger must never break the update flow, and report()
240
+ // calls are fire-and-forget so a throw here would be an unhandled
241
+ // rejection.
242
+ log('logger error:', e?.message || e);
243
+ }
211
244
  };
212
245
  throwIfEnabled = (e: Error) => {
213
246
  if (this.options.throwError) {
214
247
  throw e;
215
248
  }
216
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
+ };
217
283
  notifyAfterCheckUpdate = (state: UpdateCheckState) => {
218
284
  const { afterCheckUpdate } = this.options;
219
285
  if (!afterCheckUpdate) {
@@ -286,11 +352,13 @@ export class Pushy {
286
352
 
287
353
  if (!resp.ok) {
288
354
  const respText = await resp.text();
289
- throw Error(
355
+ throw new UpdateError(
290
356
  this.t('error_http_status', {
291
357
  status: resp.status,
292
358
  statusText: respText,
293
359
  }),
360
+ 'HTTP_STATUS',
361
+ { extra: { status: resp.status } },
294
362
  );
295
363
  }
296
364
 
@@ -332,7 +400,13 @@ export class Pushy {
332
400
  if (sharedState.marked || __DEV__ || !isFirstTime) {
333
401
  return;
334
402
  }
335
- await Promise.resolve(PushyModule.markSuccess());
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
+ }
336
410
  sharedState.marked = true;
337
411
  this.report({ type: 'markSuccess' });
338
412
  };
@@ -350,7 +424,11 @@ export class Pushy {
350
424
  }
351
425
  } catch (e) {
352
426
  sharedState.applyingUpdate = false;
353
- throw e;
427
+ const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
428
+ this.emitError(err, 'errorSwitchVersion', {
429
+ data: { newVersion: hash },
430
+ });
431
+ throw err;
354
432
  }
355
433
  try {
356
434
  return await PushyModule.reloadUpdate({ hash });
@@ -358,7 +436,11 @@ export class Pushy {
358
436
  // reloadUpdate can reject (e.g. bundle missing); reset the flag so a
359
437
  // later retry is not permanently blocked by a stuck applyingUpdate.
360
438
  sharedState.applyingUpdate = false;
361
- throw e;
439
+ const err = toUpdateError(e, 'SWITCH_VERSION_FAILED');
440
+ this.emitError(err, 'errorSwitchVersion', {
441
+ data: { newVersion: hash },
442
+ });
443
+ throw err;
362
444
  }
363
445
  }
364
446
  };
@@ -369,7 +451,15 @@ export class Pushy {
369
451
  }
370
452
  if (assertHash(hash)) {
371
453
  log(`switchVersionLater: ${hash}`);
372
- return PushyModule.setNeedUpdate({ hash });
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
+ }
373
463
  }
374
464
  };
375
465
  checkUpdate = async (extra?: Record<string, any>) => {
@@ -440,31 +530,18 @@ export class Pushy {
440
530
  return result;
441
531
  } catch (e: any) {
442
532
  this.lastRespJson = previousRespJson;
443
- const errorMessage =
444
- e?.message || this.t('error_cannot_connect_server');
445
- this.report({
446
- type: 'errorChecking',
447
- message: errorMessage,
533
+ const err = toUpdateError(e, 'CHECK_FAILED');
534
+ this.emitError(err, 'errorChecking', {
535
+ message: err.message || this.t('error_cannot_connect_server'),
448
536
  });
449
- this.notifyAfterCheckUpdate({ status: 'error', error: e });
450
- this.throwIfEnabled(e);
537
+ this.notifyAfterCheckUpdate({ status: 'error', error: err });
538
+ this.throwIfEnabled(err);
451
539
  // Fall back to the previous successful response if we have one; otherwise
452
540
  // return undefined so callers can distinguish "check failed" from a real
453
541
  // empty result and avoid overwriting the last good updateInfo.
454
542
  return previousRespJson ? await previousRespJson : undefined;
455
543
  }
456
544
  };
457
- getBackupEndpoints = async () => {
458
- const { server } = this.options;
459
- if (!server) {
460
- return [];
461
- }
462
- const remoteEndpoints = await this.getRemoteEndpoints();
463
- return excludeConfiguredEndpoints(
464
- dedupeEndpoints(remoteEndpoints),
465
- this.getConfiguredCheckEndpoints(),
466
- );
467
- };
468
545
  downloadUpdate = async (
469
546
  updateInfo: CheckResult,
470
547
  onDownloadProgress?: (data: ProgressData) => void,
@@ -664,14 +741,22 @@ export class Pushy {
664
741
  delete sharedState.progressHandlers[hash];
665
742
  }
666
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.
667
755
  this.report({
668
756
  type: 'errorUpdate',
669
757
  data: { newVersion: hash },
670
- message: errorMessages.join(';'),
758
+ message,
671
759
  });
672
- if (lastError) {
673
- throw lastError;
674
- }
675
760
  return;
676
761
  } else {
677
762
  const duration = Date.now() - patchStartTime;
@@ -712,8 +797,12 @@ export class Pushy {
712
797
  return;
713
798
  }
714
799
  if (sharedState.apkStatus === 'downloaded') {
715
- this.report({ type: 'errorInstallApk' });
716
- this.throwIfEnabled(Error('errorInstallApk'));
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);
717
806
  return;
718
807
  }
719
808
  if (Platform.Version <= 23) {
@@ -722,13 +811,18 @@ export class Pushy {
722
811
  PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
723
812
  );
724
813
  if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
725
- this.report({ type: 'rejectStoragePermission' });
726
- this.throwIfEnabled(Error('rejectStoragePermission'));
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);
727
820
  return;
728
821
  }
729
822
  } catch (e: any) {
730
- this.report({ type: 'errorStoragePermission' });
731
- this.throwIfEnabled(e);
823
+ const err = toUpdateError(e, 'STORAGE_PERMISSION_ERROR');
824
+ this.emitError(err, 'errorStoragePermission');
825
+ this.throwIfEnabled(err);
732
826
  return;
733
827
  }
734
828
  }
@@ -756,10 +850,14 @@ export class Pushy {
756
850
  hash: progressKey,
757
851
  });
758
852
  sharedState.apkStatus = 'downloaded';
759
- } catch {
853
+ } catch (e) {
760
854
  sharedState.apkStatus = null;
761
- this.report({ type: 'errorDownloadAndInstallApk' });
762
- this.throwIfEnabled(Error('errorDownloadAndInstallApk'));
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);
763
861
  } finally {
764
862
  if (sharedState.progressHandlers[progressKey]) {
765
863
  sharedState.progressHandlers[progressKey].remove();
package/src/context.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext } from 'react';
1
+ import { createContext, useContext, useMemo } from 'react';
2
2
  import { CheckResult, ProgressData } from './type';
3
3
  import { Pushy, Cresc } from './client';
4
4
  import i18n from './i18n';
@@ -46,21 +46,35 @@ export const UpdateContext = createContext<{
46
46
  currentHash: string;
47
47
  packageVersion: string;
48
48
  client?: Pushy | Cresc;
49
- progress?: ProgressData;
50
49
  updateInfo?: CheckResult;
51
50
  lastError?: Error;
52
51
  }>(defaultContext);
53
52
 
54
- export const useUpdate = __DEV__ ? () => {
53
+ // Download progress ticks at high frequency, so it lives in its own context;
54
+ // otherwise every tick would re-render all useUpdate() consumers even when
55
+ // they never read progress.
56
+ export const ProgressContext = createContext<ProgressData | undefined>(
57
+ undefined,
58
+ );
59
+
60
+ /**
61
+ * Subscribe to download progress only. Components that render a progress bar
62
+ * should prefer this over useUpdate() so the rest of the tree is not
63
+ * re-rendered on every progress event.
64
+ */
65
+ export const useUpdateProgress = () => useContext(ProgressContext);
66
+
67
+ export const useUpdate = () => {
55
68
  const context = useContext(UpdateContext);
69
+ const progress = useContext(ProgressContext);
56
70
 
57
- // 检查是否在 UpdateProvider 内部使用
58
- if (!context.client) {
71
+ if (__DEV__ && !context.client) {
72
+ // 检查是否在 UpdateProvider 内部使用
59
73
  throw new Error(i18n.t('error_use_update_outside_provider'));
60
74
  }
61
75
 
62
- return context;
63
- } : () => useContext(UpdateContext);
76
+ return useMemo(() => ({ ...context, progress }), [context, progress]);
77
+ };
64
78
 
65
79
  /** @deprecated Please use `useUpdate` instead */
66
80
  export const usePushy = useUpdate;
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 Error(
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
 
@@ -82,7 +84,11 @@ export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule);
82
84
 
83
85
  if (!uuid) {
84
86
  uuid = require('nanoid/non-secure').nanoid();
85
- PushyModule.setUuid(uuid);
87
+ // If persisting fails the uuid drifts on every launch, which skews gray
88
+ // release bucketing and inflates stats — log it instead of failing silently.
89
+ Promise.resolve(PushyModule.setUuid(uuid)).catch((e: any) => {
90
+ log('setUuid error:', e?.message || e);
91
+ });
86
92
  }
87
93
 
88
94
  export const cInfo = {
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 Error('No endpoints configured');
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 Error('No endpoints configured');
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
@@ -1,4 +1,13 @@
1
1
  export { Pushy, Cresc } from './client';
2
- export { UpdateContext, usePushy, useUpdate } from './context';
2
+ export type { UpdateErrorListener } from './client';
3
+ export { UpdateError } from './error';
4
+ export type { UpdateErrorCode } from './error';
5
+ export {
6
+ ProgressContext,
7
+ UpdateContext,
8
+ usePushy,
9
+ useUpdate,
10
+ useUpdateProgress,
11
+ } from './context';
3
12
  export { PushyProvider, UpdateProvider } from './provider';
4
13
  export { PushyModule, UpdateModule } from './core';
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: