@appsonair.ir/react-native 1.0.1 → 1.0.3

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/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { configure, getConfig } from './config';
2
2
  import { configureFromProject, getDeploymentKey, isOtaConfigured, resolveUpdaterConfig } from './projectConfig';
3
- import { sync, restartApp, installApk, resumePendingApkInstall, clearUpdate, getInstalledBundlePath, notifyAppReady, isSyncInProgress } from './sync';
3
+ import { sync, restartApp, installApk, resumePendingApkInstall, clearUpdate, getInstalledBundlePath, notifyAppReady } from './sync';
4
4
  import { UpdateCheckResult } from './types';
5
5
  export { configure, getConfig, getServerUrl } from './config';
6
6
  export { DEFAULT_SERVER_URL } from './constants';
@@ -8,7 +8,7 @@ export { configureFromProject, getDeploymentKey, isOtaConfigured, resolveUpdater
8
8
  export type { ProjectOtaConfig } from './types';
9
9
  export declare function getCurrentLabel(): Promise<string | null>;
10
10
  export declare function checkUpdate(): Promise<UpdateCheckResult>;
11
- export { sync, restartApp, installApk, resumePendingApkInstall, clearUpdate, getInstalledBundlePath, notifyAppReady, isSyncInProgress };
11
+ export { sync, restartApp, installApk, resumePendingApkInstall, clearUpdate, getInstalledBundlePath, notifyAppReady, isSyncInProgress, respondToUpdatePrompt, } from './sync';
12
12
  export { OTAUpdateModal } from './ui/OTAUpdateModal';
13
13
  export { OTAUpdateProvider } from './ui/OTAUpdateProvider';
14
14
  export declare const OTAUpdater: {
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.OTAUpdater = exports.OTAUpdateProvider = exports.OTAUpdateModal = exports.isSyncInProgress = exports.notifyAppReady = exports.getInstalledBundlePath = exports.clearUpdate = exports.resumePendingApkInstall = exports.installApk = exports.restartApp = exports.sync = exports.resolveUpdaterConfig = exports.isOtaConfigured = exports.getDeploymentKey = exports.configureFromProject = exports.DEFAULT_SERVER_URL = exports.getServerUrl = exports.getConfig = exports.configure = void 0;
20
+ exports.OTAUpdater = exports.OTAUpdateProvider = exports.OTAUpdateModal = exports.respondToUpdatePrompt = exports.isSyncInProgress = exports.notifyAppReady = exports.getInstalledBundlePath = exports.clearUpdate = exports.resumePendingApkInstall = exports.installApk = exports.restartApp = exports.sync = exports.resolveUpdaterConfig = exports.isOtaConfigured = exports.getDeploymentKey = exports.configureFromProject = exports.DEFAULT_SERVER_URL = exports.getServerUrl = exports.getConfig = exports.configure = void 0;
21
21
  exports.getCurrentLabel = getCurrentLabel;
22
22
  exports.checkUpdate = checkUpdate;
23
23
  const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
@@ -29,14 +29,6 @@ Object.defineProperty(exports, "getDeploymentKey", { enumerable: true, get: func
29
29
  Object.defineProperty(exports, "isOtaConfigured", { enumerable: true, get: function () { return projectConfig_1.isOtaConfigured; } });
30
30
  Object.defineProperty(exports, "resolveUpdaterConfig", { enumerable: true, get: function () { return projectConfig_1.resolveUpdaterConfig; } });
31
31
  const sync_1 = require("./sync");
32
- Object.defineProperty(exports, "sync", { enumerable: true, get: function () { return sync_1.sync; } });
33
- Object.defineProperty(exports, "restartApp", { enumerable: true, get: function () { return sync_1.restartApp; } });
34
- Object.defineProperty(exports, "installApk", { enumerable: true, get: function () { return sync_1.installApk; } });
35
- Object.defineProperty(exports, "resumePendingApkInstall", { enumerable: true, get: function () { return sync_1.resumePendingApkInstall; } });
36
- Object.defineProperty(exports, "clearUpdate", { enumerable: true, get: function () { return sync_1.clearUpdate; } });
37
- Object.defineProperty(exports, "getInstalledBundlePath", { enumerable: true, get: function () { return sync_1.getInstalledBundlePath; } });
38
- Object.defineProperty(exports, "notifyAppReady", { enumerable: true, get: function () { return sync_1.notifyAppReady; } });
39
- Object.defineProperty(exports, "isSyncInProgress", { enumerable: true, get: function () { return sync_1.isSyncInProgress; } });
40
32
  var config_2 = require("./config");
41
33
  Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return config_2.configure; } });
42
34
  Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_2.getConfig; } });
@@ -58,6 +50,16 @@ async function checkUpdate() {
58
50
  const deviceAbi = react_native_1.Platform.OS === 'android' ? await (0, sync_1.getDeviceAbi)() : undefined;
59
51
  return (0, sync_1.checkForUpdate)((0, config_1.getServerUrl)(config), config.deploymentKey, appVersion, meta?.label, meta?.packageHash, clientUniqueId, react_native_1.Platform.OS === 'android' ? react_native_device_info_1.default.getBuildNumber() : undefined, deviceAbi);
60
52
  }
53
+ var sync_2 = require("./sync");
54
+ Object.defineProperty(exports, "sync", { enumerable: true, get: function () { return sync_2.sync; } });
55
+ Object.defineProperty(exports, "restartApp", { enumerable: true, get: function () { return sync_2.restartApp; } });
56
+ Object.defineProperty(exports, "installApk", { enumerable: true, get: function () { return sync_2.installApk; } });
57
+ Object.defineProperty(exports, "resumePendingApkInstall", { enumerable: true, get: function () { return sync_2.resumePendingApkInstall; } });
58
+ Object.defineProperty(exports, "clearUpdate", { enumerable: true, get: function () { return sync_2.clearUpdate; } });
59
+ Object.defineProperty(exports, "getInstalledBundlePath", { enumerable: true, get: function () { return sync_2.getInstalledBundlePath; } });
60
+ Object.defineProperty(exports, "notifyAppReady", { enumerable: true, get: function () { return sync_2.notifyAppReady; } });
61
+ Object.defineProperty(exports, "isSyncInProgress", { enumerable: true, get: function () { return sync_2.isSyncInProgress; } });
62
+ Object.defineProperty(exports, "respondToUpdatePrompt", { enumerable: true, get: function () { return sync_2.respondToUpdatePrompt; } });
61
63
  var OTAUpdateModal_1 = require("./ui/OTAUpdateModal");
62
64
  Object.defineProperty(exports, "OTAUpdateModal", { enumerable: true, get: function () { return OTAUpdateModal_1.OTAUpdateModal; } });
63
65
  var OTAUpdateProvider_1 = require("./ui/OTAUpdateProvider");
package/lib/sync.d.ts CHANGED
@@ -1,11 +1,17 @@
1
- import { OTAProgressMessages, StoredBundleMeta, SyncOptions, SyncResult, SyncStatus, UpdateCheckResult } from './types';
1
+ import { InstallMode, OTAProgressMessages, StoredBundleMeta, SyncOptions, SyncResult, SyncStatus, UpdateCheckResult } from './types';
2
2
  export interface OTAProgressState {
3
3
  visible: boolean;
4
4
  status: SyncStatus;
5
5
  progress: number;
6
6
  message: string;
7
7
  label?: string;
8
+ /** Release changelog / notes */
8
9
  description?: string;
10
+ isMandatory?: boolean;
11
+ installMode?: InstallMode;
12
+ /** Show Update / Skip actions (UPDATE_AVAILABLE or deferred done / error dismiss) */
13
+ showActions?: boolean;
14
+ canSkip?: boolean;
9
15
  error?: string;
10
16
  }
11
17
  type ProgressListener = (state: OTAProgressState) => void;
@@ -19,11 +25,22 @@ export declare const defaultMessages: {
19
25
  installing: string;
20
26
  restarting: string;
21
27
  error: string;
28
+ downloadInterrupted: string;
22
29
  upToDate: string;
30
+ updateAvailable: string;
31
+ changelogTitle: string;
32
+ updateNow: string;
33
+ skip: string;
34
+ dismiss: string;
35
+ appliedNextLaunch: string;
36
+ mandatoryHint: string;
23
37
  };
24
38
  export declare function getMessageForStatus(status: SyncStatus, messages?: Partial<typeof defaultMessages>): string;
25
39
  export declare function getModalTitle(messages?: Partial<typeof defaultMessages>): string;
26
40
  export declare function delay(ms: number): Promise<void>;
41
+ type UpdatePromptAction = 'update' | 'skip' | 'dismiss';
42
+ /** Resolve the Update / Skip / Dismiss prompt shown by OTAUpdateModal. */
43
+ export declare function respondToUpdatePrompt(action: UpdatePromptAction): void;
27
44
  export declare function getStoredBundleMeta(): Promise<StoredBundleMeta | null>;
28
45
  export declare function setStoredBundleMeta(meta: StoredBundleMeta): Promise<void>;
29
46
  export declare function getClientUniqueId(): Promise<string>;
package/lib/sync.js CHANGED
@@ -10,6 +10,7 @@ exports.hideProgress = hideProgress;
10
10
  exports.getMessageForStatus = getMessageForStatus;
11
11
  exports.getModalTitle = getModalTitle;
12
12
  exports.delay = delay;
13
+ exports.respondToUpdatePrompt = respondToUpdatePrompt;
13
14
  exports.getStoredBundleMeta = getStoredBundleMeta;
14
15
  exports.setStoredBundleMeta = setStoredBundleMeta;
15
16
  exports.getClientUniqueId = getClientUniqueId;
@@ -61,8 +62,10 @@ function hideProgress() {
61
62
  progressListeners.forEach((listener) => listener(progressState));
62
63
  }
63
64
  function shouldShowModalForStatus(status) {
64
- return (status === types_1.SyncStatus.DOWNLOADING ||
65
+ return (status === types_1.SyncStatus.UPDATE_AVAILABLE ||
66
+ status === types_1.SyncStatus.DOWNLOADING ||
65
67
  status === types_1.SyncStatus.RESTARTING ||
68
+ status === types_1.SyncStatus.UPDATE_INSTALLED ||
66
69
  status === types_1.SyncStatus.ERROR);
67
70
  }
68
71
  exports.defaultMessages = {
@@ -72,19 +75,31 @@ exports.defaultMessages = {
72
75
  installing: 'در حال نصب به\u200cروزرسانی...',
73
76
  restarting: 'در حال راه\u200cاندازی مجدد...',
74
77
  error: 'خطا در به\u200cروزرسانی',
78
+ downloadInterrupted: 'دانلود قطع شد. لطفاً دوباره تلاش کنید',
75
79
  upToDate: 'برنامه به\u200cروز است',
80
+ updateAvailable: 'نسخه جدیدی در دسترس است',
81
+ changelogTitle: 'تغییرات این نسخه',
82
+ updateNow: 'به\u200cروزرسانی',
83
+ skip: 'فعلاً نه',
84
+ dismiss: 'باشه',
85
+ appliedNextLaunch: 'به\u200cروزرسانی نصب شد و در اجرای بعدی برنامه اعمال می\u200cشود',
86
+ mandatoryHint: 'این به\u200cروزرسانی الزامی است',
76
87
  };
77
88
  function getMessageForStatus(status, messages = {}) {
78
89
  const m = { ...exports.defaultMessages, ...messages };
79
90
  switch (status) {
80
91
  case types_1.SyncStatus.CHECKING:
81
92
  return m.checking;
93
+ case types_1.SyncStatus.UPDATE_AVAILABLE:
94
+ return m.updateAvailable;
82
95
  case types_1.SyncStatus.DOWNLOADING:
83
96
  return m.downloading;
84
97
  case types_1.SyncStatus.INSTALLING:
85
98
  return m.installing;
86
99
  case types_1.SyncStatus.RESTARTING:
87
100
  return m.restarting;
101
+ case types_1.SyncStatus.UPDATE_INSTALLED:
102
+ return m.appliedNextLaunch;
88
103
  case types_1.SyncStatus.ERROR:
89
104
  return m.error;
90
105
  default:
@@ -124,7 +139,41 @@ const CLIENT_ID_KEY = '@appsonair.ir/client-id';
124
139
  const NATIVE_VERSION_KEY = '@appsonair.ir/native-version';
125
140
  const NATIVE_BUILD_KEY = '@appsonair.ir/native-build';
126
141
  const RESTART_COOLDOWN_KEY = '@appsonair.ir/last-restart';
142
+ const SKIPPED_UPDATE_KEY = '@appsonair.ir/skipped-update';
127
143
  const RESTART_COOLDOWN_MS = 15000;
144
+ let pendingPrompt = null;
145
+ /** Resolve the Update / Skip / Dismiss prompt shown by OTAUpdateModal. */
146
+ function respondToUpdatePrompt(action) {
147
+ if (!pendingPrompt)
148
+ return;
149
+ const { resolve } = pendingPrompt;
150
+ pendingPrompt = null;
151
+ resolve(action);
152
+ }
153
+ function waitForUpdatePrompt() {
154
+ return new Promise((resolve) => {
155
+ pendingPrompt = { resolve };
156
+ });
157
+ }
158
+ function resolveInstallMode(mode) {
159
+ if (mode === 'next-launch' || mode === 'on-next-restart' || mode === 'on-next-resume') {
160
+ return 'next-launch';
161
+ }
162
+ return 'immediate';
163
+ }
164
+ function getUpdateChangelog(update) {
165
+ const text = update.changelog ?? update.description;
166
+ return text?.trim() ? text.trim() : undefined;
167
+ }
168
+ async function getSkippedUpdateLabel() {
169
+ return async_storage_1.default.getItem(SKIPPED_UPDATE_KEY);
170
+ }
171
+ async function markUpdateSkipped(label) {
172
+ await async_storage_1.default.setItem(SKIPPED_UPDATE_KEY, label);
173
+ }
174
+ async function clearSkippedUpdate() {
175
+ await async_storage_1.default.removeItem(SKIPPED_UPDATE_KEY);
176
+ }
128
177
  async function getStoredBundleMeta() {
129
178
  const raw = await async_storage_1.default.getItem(STORAGE_KEY);
130
179
  if (!raw)
@@ -365,6 +414,31 @@ async function ensureBundleDirectory() {
365
414
  await ReactNativeBlobUtil.fs.mkdir(dir);
366
415
  }
367
416
  }
417
+ /** Keep only the active OTA package so leftover extracts don't fill disk / slow IO. */
418
+ async function cleanupOldUpdates(keepLabel) {
419
+ try {
420
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
421
+ const dir = getBundleDirectory();
422
+ const exists = await ReactNativeBlobUtil.fs.exists(dir);
423
+ if (!exists)
424
+ return;
425
+ const keep = new Set([
426
+ keepLabel,
427
+ `${keepLabel}.zip`,
428
+ `${keepLabel}.bundle`,
429
+ ]);
430
+ const entries = await ReactNativeBlobUtil.fs.ls(dir);
431
+ await Promise.all(entries.map(async (entry) => {
432
+ if (keep.has(entry) || entry.endsWith('.apk')) {
433
+ return;
434
+ }
435
+ await removePathRecursive(`${dir}/${entry}`);
436
+ }));
437
+ }
438
+ catch {
439
+ // Best-effort cleanup; a leftover folder must not fail install.
440
+ }
441
+ }
368
442
  async function resolveDownloadSize(downloadUrl, deploymentKey, expectedSize, deviceAbi) {
369
443
  if (expectedSize && expectedSize > 0) {
370
444
  return expectedSize;
@@ -401,6 +475,80 @@ async function resolveDownloadSize(downloadUrl, deploymentKey, expectedSize, dev
401
475
  }
402
476
  return expectedSize ?? 0;
403
477
  }
478
+ const DOWNLOAD_MAX_ATTEMPTS = 3;
479
+ function getErrorMessage(error) {
480
+ if (error instanceof Error)
481
+ return error.message;
482
+ if (typeof error === 'string')
483
+ return error;
484
+ if (error && typeof error === 'object' && 'message' in error) {
485
+ return String(error.message);
486
+ }
487
+ return String(error);
488
+ }
489
+ function isRetryableDownloadError(error) {
490
+ const message = getErrorMessage(error).toLowerCase();
491
+ return (message.includes('download interrupted') ||
492
+ message.includes('network') ||
493
+ message.includes('timeout') ||
494
+ message.includes('timed out') ||
495
+ message.includes('connection') ||
496
+ message.includes('socket') ||
497
+ message.includes('econnreset') ||
498
+ message.includes('failed to connect') ||
499
+ message.includes('software caused connection abort'));
500
+ }
501
+ function toUserFacingDownloadError(error) {
502
+ if (isRetryableDownloadError(error)) {
503
+ return new Error(exports.defaultMessages.downloadInterrupted);
504
+ }
505
+ return error instanceof Error ? error : new Error(getErrorMessage(error));
506
+ }
507
+ async function cleanupPartialDownload(path) {
508
+ try {
509
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
510
+ const exists = await ReactNativeBlobUtil.fs.exists(path);
511
+ if (exists) {
512
+ await ReactNativeBlobUtil.fs.unlink(path);
513
+ }
514
+ }
515
+ catch {
516
+ // Ignore cleanup errors.
517
+ }
518
+ }
519
+ /**
520
+ * react-native-blob-util rejects with "Download interrupted." whenever
521
+ * bytesWritten !== Content-Length — including when Content-Length is wrong
522
+ * but the full file actually landed. Recover those cases via size/hash checks.
523
+ */
524
+ async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label) {
525
+ try {
526
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
527
+ const exists = await ReactNativeBlobUtil.fs.exists(destPath);
528
+ if (!exists)
529
+ return null;
530
+ const stat = await ReactNativeBlobUtil.fs.stat(destPath);
531
+ const minSize = extension === 'apk' ? 1024 * 100 : 1024;
532
+ if (!stat || stat.size < minSize)
533
+ return null;
534
+ const sizeLooksComplete = totalBytes <= 0 || Math.abs(stat.size - totalBytes) <= 1;
535
+ if (expectedHash) {
536
+ const valid = await verifyBundleHash(destPath, expectedHash);
537
+ if (!valid)
538
+ return null;
539
+ }
540
+ else if (!sizeLooksComplete) {
541
+ return null;
542
+ }
543
+ if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
544
+ return extractBundleUpdate(label, destPath);
545
+ }
546
+ return destPath;
547
+ }
548
+ catch {
549
+ return null;
550
+ }
551
+ }
404
552
  async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, onProgress, extension = 'bundle', deviceAbi, packageFormat = 'bundle', expectedHash) {
405
553
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
406
554
  const isZipBundle = extension === 'bundle' && packageFormat === 'zip';
@@ -414,76 +562,95 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
414
562
  if (isZipBundle) {
415
563
  await removePathRecursive(getBundleReleaseDir(label));
416
564
  }
417
- const destExists = await ReactNativeBlobUtil.fs.exists(destPath);
418
- if (destExists) {
419
- await ReactNativeBlobUtil.fs.unlink(destPath);
420
- }
421
- const task = ReactNativeBlobUtil.config({
422
- path: destPath,
423
- fileCache: false,
424
- overwrite: true,
425
- }).fetch('GET', downloadUrl, {
426
- 'X-Deployment-Key': deploymentKey,
427
- ...(deviceAbi ? { 'X-Device-Abi': deviceAbi } : {}),
428
- });
429
- const reportDownloadProgress = (received, total) => {
430
- if (!onProgress)
431
- return;
432
- const resolvedTotal = total > 0 ? total : totalBytes;
433
- if (resolvedTotal <= 0)
434
- return;
435
- onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99));
436
- };
437
- let pollTimer = null;
438
- if (onProgress && totalBytes > 0) {
439
- pollTimer = setInterval(async () => {
440
- try {
441
- const exists = await ReactNativeBlobUtil.fs.exists(destPath);
442
- if (!exists)
443
- return;
444
- const stat = await ReactNativeBlobUtil.fs.stat(destPath);
445
- reportDownloadProgress(stat.size, totalBytes);
446
- }
447
- catch {
448
- // Ignore polling errors while the file is being written.
449
- }
450
- }, 150);
451
- }
452
- if (onProgress) {
453
- task.progress({ interval: 100, count: 10 }, (received, total) => {
454
- reportDownloadProgress(received, total);
565
+ let lastError;
566
+ for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt++) {
567
+ await cleanupPartialDownload(destPath);
568
+ const task = ReactNativeBlobUtil.config({
569
+ path: destPath,
570
+ fileCache: false,
571
+ overwrite: true,
572
+ timeout: 10 * 60 * 1000,
573
+ }).fetch('GET', downloadUrl, {
574
+ 'X-Deployment-Key': deploymentKey,
575
+ ...(deviceAbi ? { 'X-Device-Abi': deviceAbi } : {}),
455
576
  });
456
- }
457
- try {
458
- const response = await task;
459
- if (onProgress) {
460
- onProgress(1);
461
- }
462
- const downloadedPath = String(response.path()).replace(/^file:\/\//, '');
463
- if (downloadedPath !== destPath) {
464
- await ReactNativeBlobUtil.fs.cp(downloadedPath, destPath);
577
+ const reportDownloadProgress = (received, total) => {
578
+ if (!onProgress)
579
+ return;
580
+ const resolvedTotal = total > 0 ? total : totalBytes;
581
+ if (resolvedTotal <= 0)
582
+ return;
583
+ onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99));
584
+ };
585
+ let pollTimer = null;
586
+ if (onProgress && totalBytes > 0) {
587
+ pollTimer = setInterval(async () => {
588
+ try {
589
+ const exists = await ReactNativeBlobUtil.fs.exists(destPath);
590
+ if (!exists)
591
+ return;
592
+ const stat = await ReactNativeBlobUtil.fs.stat(destPath);
593
+ reportDownloadProgress(stat.size, totalBytes);
594
+ }
595
+ catch {
596
+ // Ignore polling errors while the file is being written.
597
+ }
598
+ }, 150);
465
599
  }
466
- const stat = await ReactNativeBlobUtil.fs.stat(destPath);
467
- const minSize = extension === 'apk' ? 1024 * 100 : 1024;
468
- if (!stat || stat.size < minSize) {
469
- throw new Error(`Downloaded file is too small (${stat?.size ?? 0} bytes)`);
600
+ if (onProgress) {
601
+ task.progress({ interval: 100, count: 10 }, (received, total) => {
602
+ reportDownloadProgress(received, total);
603
+ });
470
604
  }
471
- if (expectedHash) {
472
- const valid = await verifyBundleHash(destPath, expectedHash);
473
- if (!valid) {
474
- throw new Error('Package hash verification failed');
605
+ try {
606
+ const response = await task;
607
+ if (onProgress) {
608
+ onProgress(1);
609
+ }
610
+ const downloadedPath = String(response.path()).replace(/^file:\/\//, '');
611
+ if (downloadedPath !== destPath) {
612
+ await ReactNativeBlobUtil.fs.cp(downloadedPath, destPath);
475
613
  }
614
+ const stat = await ReactNativeBlobUtil.fs.stat(destPath);
615
+ const minSize = extension === 'apk' ? 1024 * 100 : 1024;
616
+ if (!stat || stat.size < minSize) {
617
+ throw new Error(`Downloaded file is too small (${stat?.size ?? 0} bytes)`);
618
+ }
619
+ if (expectedHash) {
620
+ const valid = await verifyBundleHash(destPath, expectedHash);
621
+ if (!valid) {
622
+ throw new Error('Package hash verification failed');
623
+ }
624
+ }
625
+ if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
626
+ return extractBundleUpdate(label, destPath);
627
+ }
628
+ return destPath;
476
629
  }
477
- if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
478
- return extractBundleUpdate(label, destPath);
630
+ catch (error) {
631
+ lastError = error;
632
+ if (isRetryableDownloadError(error)) {
633
+ const recovered = await tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label);
634
+ if (recovered) {
635
+ if (onProgress)
636
+ onProgress(1);
637
+ return recovered;
638
+ }
639
+ }
640
+ await cleanupPartialDownload(destPath);
641
+ if (attempt < DOWNLOAD_MAX_ATTEMPTS && isRetryableDownloadError(error)) {
642
+ await delay(attempt * 750);
643
+ continue;
644
+ }
645
+ throw toUserFacingDownloadError(error);
479
646
  }
480
- return destPath;
481
- }
482
- finally {
483
- if (pollTimer) {
484
- clearInterval(pollTimer);
647
+ finally {
648
+ if (pollTimer) {
649
+ clearInterval(pollTimer);
650
+ }
485
651
  }
486
652
  }
653
+ throw toUserFacingDownloadError(lastError);
487
654
  }
488
655
  async function verifyBundleHash(filePath, expectedHash) {
489
656
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
@@ -503,24 +670,51 @@ function reportProgress(showUI, messages, onProgress, status, progress = 0, extr
503
670
  onProgress?.(status, progress);
504
671
  if (!showUI)
505
672
  return;
506
- // Silent phases — never show modal for check / install / up-to-date.
673
+ // Silent phases — never show modal for check / install / ignored / up-to-date.
507
674
  if (status === types_1.SyncStatus.CHECKING ||
508
675
  status === types_1.SyncStatus.UP_TO_DATE ||
509
676
  status === types_1.SyncStatus.UPDATE_IGNORED ||
510
- status === types_1.SyncStatus.INSTALLING) {
677
+ status === types_1.SyncStatus.INSTALLING ||
678
+ status === types_1.SyncStatus.APK_INSTALL_PENDING) {
511
679
  return;
512
680
  }
513
681
  if (status === types_1.SyncStatus.ERROR) {
514
682
  showProgress(status, progress, extra?.error ?? getMessageForStatus(status, messages), {
515
683
  error: extra?.error,
684
+ showActions: true,
685
+ canSkip: false,
516
686
  ...extra,
517
687
  });
518
688
  return;
519
689
  }
520
690
  if (shouldShowModalForStatus(status)) {
521
- showProgress(status, progress, getMessageForStatus(status, messages), extra);
691
+ showProgress(status, progress, extra?.message ?? getMessageForStatus(status, messages), {
692
+ showActions: false,
693
+ canSkip: false,
694
+ ...extra,
695
+ });
522
696
  }
523
697
  }
698
+ async function promptForUpdate(showUI, messages, onProgress, update, installMode) {
699
+ const changelog = getUpdateChangelog(update);
700
+ const isMandatory = update.isMandatory === true;
701
+ if (!showUI) {
702
+ return 'update';
703
+ }
704
+ reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_AVAILABLE, 0, {
705
+ label: update.label,
706
+ description: changelog,
707
+ isMandatory,
708
+ installMode,
709
+ showActions: true,
710
+ canSkip: !isMandatory,
711
+ });
712
+ const action = await waitForUpdatePrompt();
713
+ if (action === 'skip' && !isMandatory) {
714
+ return 'skip';
715
+ }
716
+ return 'update';
717
+ }
524
718
  async function restartApp() {
525
719
  if (OTAUpdaterNative?.restartApp) {
526
720
  await OTAUpdaterNative.restartApp();
@@ -590,16 +784,51 @@ async function performSync(options = {}, onProgress, messages) {
590
784
  onProgress?.(types_1.SyncStatus.UP_TO_DATE, 1);
591
785
  return { status: types_1.SyncStatus.UP_TO_DATE };
592
786
  }
787
+ const installMode = resolveInstallMode(options.installMode ?? config.installMode ?? 'immediate');
788
+ const changelog = getUpdateChangelog(update);
789
+ if (!update.isMandatory) {
790
+ const skipped = await getSkippedUpdateLabel();
791
+ if (skipped && skipped === update.label) {
792
+ if (showUI)
793
+ hideProgress();
794
+ onProgress?.(types_1.SyncStatus.UPDATE_IGNORED, 1);
795
+ return {
796
+ status: types_1.SyncStatus.UPDATE_IGNORED,
797
+ label: update.label,
798
+ description: changelog,
799
+ };
800
+ }
801
+ }
802
+ else {
803
+ await clearSkippedUpdate();
804
+ }
805
+ const decision = await promptForUpdate(showUI, messages, onProgress, update, installMode);
806
+ if (decision === 'skip') {
807
+ await markUpdateSkipped(update.label);
808
+ if (showUI)
809
+ hideProgress();
810
+ onProgress?.(types_1.SyncStatus.UPDATE_IGNORED, 1);
811
+ return {
812
+ status: types_1.SyncStatus.UPDATE_IGNORED,
813
+ label: update.label,
814
+ description: changelog,
815
+ };
816
+ }
817
+ await clearSkippedUpdate();
593
818
  if (update.updateType === 'apk') {
594
- return performApkSync(update, showUI, messages, onProgress);
819
+ return performApkSync(update, showUI, messages, onProgress, changelog);
595
820
  }
596
821
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, {
597
822
  label: update.label,
598
- description: update.description,
823
+ description: changelog,
824
+ isMandatory: update.isMandatory,
825
+ installMode,
599
826
  });
600
827
  const bundlePath = await downloadBundle(update.downloadUrl, config.deploymentKey, update.label, update.size, (p) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
601
828
  label: update.label,
602
- description: update.description,
829
+ description: changelog,
830
+ isMandatory: update.isMandatory,
831
+ installMode,
603
832
  }), 'bundle', undefined, update.packageFormat ?? 'bundle', update.packageHash);
604
833
  onProgress?.(types_1.SyncStatus.INSTALLING, 1);
605
834
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
@@ -615,12 +844,14 @@ async function performSync(options = {}, onProgress, messages) {
615
844
  packageHash: update.packageHash,
616
845
  installedAt: new Date().toISOString(),
617
846
  });
618
- const installMode = options.installMode ?? config.installMode ?? 'immediate';
847
+ await cleanupOldUpdates(update.label);
619
848
  const shouldRestartNow = installMode === 'immediate' || update.isMandatory === true;
620
849
  if (shouldRestartNow) {
621
850
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.RESTARTING, 1, {
622
851
  label: update.label,
623
- description: update.description,
852
+ description: changelog,
853
+ isMandatory: update.isMandatory,
854
+ installMode,
624
855
  });
625
856
  await markRestartPending();
626
857
  await delay(300);
@@ -628,46 +859,62 @@ async function performSync(options = {}, onProgress, messages) {
628
859
  return {
629
860
  status: types_1.SyncStatus.UPDATE_INSTALLED,
630
861
  label: update.label,
631
- description: update.description,
862
+ description: changelog,
632
863
  };
633
864
  }
634
- if (showUI)
865
+ if (showUI) {
866
+ reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_INSTALLED, 1, {
867
+ label: update.label,
868
+ description: changelog,
869
+ isMandatory: update.isMandatory,
870
+ installMode,
871
+ showActions: true,
872
+ canSkip: false,
873
+ message: getMessageForStatus(types_1.SyncStatus.UPDATE_INSTALLED, messages),
874
+ });
875
+ await waitForUpdatePrompt();
635
876
  hideProgress();
877
+ }
636
878
  return {
637
879
  status: types_1.SyncStatus.UPDATE_INSTALLED,
638
880
  label: update.label,
639
- description: update.description,
881
+ description: changelog,
640
882
  };
641
883
  }
642
884
  catch (error) {
643
885
  const err = error instanceof Error ? error : new Error(String(error));
644
886
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.ERROR, 0, {
645
887
  error: err.message,
888
+ showActions: true,
889
+ canSkip: false,
646
890
  });
891
+ if (showUI) {
892
+ await waitForUpdatePrompt();
893
+ hideProgress();
894
+ }
647
895
  return {
648
896
  status: types_1.SyncStatus.ERROR,
649
897
  error: err,
650
898
  };
651
899
  }
652
900
  }
653
- async function performApkSync(update, showUI, messages, onProgress) {
901
+ async function performApkSync(update, showUI, messages, onProgress, changelog) {
654
902
  if (!update.downloadUrl || !update.label || !update.packageHash) {
655
903
  return { status: types_1.SyncStatus.UP_TO_DATE };
656
904
  }
905
+ const notes = changelog ?? getUpdateChangelog(update);
657
906
  try {
658
907
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, {
659
908
  label: update.label,
660
- description: update.description,
909
+ description: notes,
910
+ isMandatory: update.isMandatory,
661
911
  });
662
912
  const apkPath = await downloadBundle(update.downloadUrl, (0, config_1.getConfig)().deploymentKey, update.label, update.size, (p) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
663
913
  label: update.label,
664
- description: update.description,
665
- }), 'apk', update.deviceAbi);
914
+ description: notes,
915
+ isMandatory: update.isMandatory,
916
+ }), 'apk', update.deviceAbi, 'bundle', update.packageHash);
666
917
  onProgress?.(types_1.SyncStatus.INSTALLING, 1);
667
- const valid = await verifyBundleHash(apkPath, update.packageHash);
668
- if (!valid) {
669
- throw new Error('APK hash verification failed');
670
- }
671
918
  // Drop the OTA JS path before opening the installer so the new APK does not
672
919
  // keep loading the previous bundle on first launch after install.
673
920
  await clearUpdate();
@@ -675,12 +922,13 @@ async function performApkSync(update, showUI, messages, onProgress) {
675
922
  if (installResult === 'permission_required') {
676
923
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.INSTALLING, 1, {
677
924
  label: update.label,
678
- description: update.description,
925
+ description: notes,
926
+ isMandatory: update.isMandatory,
679
927
  });
680
928
  return {
681
929
  status: types_1.SyncStatus.APK_INSTALL_PENDING,
682
930
  label: update.label,
683
- description: update.description,
931
+ description: notes,
684
932
  };
685
933
  }
686
934
  if (showUI)
@@ -689,14 +937,20 @@ async function performApkSync(update, showUI, messages, onProgress) {
689
937
  return {
690
938
  status: types_1.SyncStatus.APK_INSTALL_PENDING,
691
939
  label: update.label,
692
- description: update.description,
940
+ description: notes,
693
941
  };
694
942
  }
695
943
  catch (error) {
696
944
  const err = error instanceof Error ? error : new Error(String(error));
697
945
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.ERROR, 0, {
698
946
  error: err.message,
947
+ showActions: true,
948
+ canSkip: false,
699
949
  });
950
+ if (showUI) {
951
+ await waitForUpdatePrompt();
952
+ hideProgress();
953
+ }
700
954
  return {
701
955
  status: types_1.SyncStatus.ERROR,
702
956
  error: err,
@@ -717,10 +971,16 @@ async function getInstalledBundlePath() {
717
971
  return null;
718
972
  }
719
973
  async function clearUpdate() {
974
+ const meta = await getStoredBundleMeta();
720
975
  await clearStoredBundleMeta();
721
976
  if (OTAUpdaterNative?.clearBundlePath) {
722
977
  await OTAUpdaterNative.clearBundlePath();
723
978
  }
979
+ if (meta?.label) {
980
+ await removePathRecursive(getBundleReleaseDir(meta.label));
981
+ await cleanupPartialDownload(getBundleZipPath(meta.label));
982
+ await cleanupPartialDownload(getLegacyBundleFilePath(meta.label));
983
+ }
724
984
  }
725
985
  async function notifyAppReady() {
726
986
  const meta = await getStoredBundleMeta();
package/lib/types.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import type { ReactNode } from 'react';
2
+ /** When to apply a downloaded JS bundle update. */
3
+ export type InstallMode = 'immediate' | 'next-launch'
4
+ /** @deprecated use `next-launch` */
5
+ | 'on-next-restart'
6
+ /** @deprecated use `next-launch` */
7
+ | 'on-next-resume';
2
8
  export interface ProjectOtaConfig {
3
9
  /** Optional — defaults to https://appsonair.ir */
4
10
  serverUrl?: string;
5
11
  entryFile?: string;
6
- installMode?: OTAUpdaterConfig['installMode'];
12
+ installMode?: InstallMode;
7
13
  deployments: {
8
14
  staging: string;
9
15
  production: string;
@@ -14,8 +20,12 @@ export interface OTAUpdaterConfig {
14
20
  serverUrl?: string;
15
21
  /** Deployment key from the OTA server (Staging or Production) */
16
22
  deploymentKey: string;
17
- /** When to apply downloaded updates (default: immediate) */
18
- installMode?: 'immediate' | 'on-next-restart' | 'on-next-resume';
23
+ /**
24
+ * When to apply downloaded JS updates (default: immediate).
25
+ * - immediate: restart right after install
26
+ * - next-launch: install now, take effect on next app start
27
+ */
28
+ installMode?: InstallMode;
19
29
  /** Check interval in milliseconds when using auto-check (default: disabled) */
20
30
  checkFrequency?: number;
21
31
  /** Show built-in progress popup during sync (default: false, enabled by OTAUpdateProvider) */
@@ -31,6 +41,14 @@ export interface OTAProgressMessages {
31
41
  restarting?: string;
32
42
  error?: string;
33
43
  upToDate?: string;
44
+ /** Prompt when an update is available */
45
+ updateAvailable?: string;
46
+ changelogTitle?: string;
47
+ updateNow?: string;
48
+ skip?: string;
49
+ dismiss?: string;
50
+ appliedNextLaunch?: string;
51
+ mandatoryHint?: string;
34
52
  }
35
53
  export interface OTAUpdateProviderProps {
36
54
  children: ReactNode;
@@ -55,6 +73,8 @@ export interface UpdateCheckResult {
55
73
  targetAppVersion?: string;
56
74
  downloadUrl?: string;
57
75
  label?: string;
76
+ /** Release notes / changelog (alias of description) */
77
+ changelog?: string;
58
78
  description?: string;
59
79
  isMandatory?: boolean;
60
80
  appVersion?: string;
@@ -66,7 +86,7 @@ export interface UpdateCheckResult {
66
86
  }
67
87
  export interface SyncOptions {
68
88
  /** When to apply the update (default: immediate, or value from configure()) */
69
- installMode?: 'immediate' | 'on-next-restart' | 'on-next-resume';
89
+ installMode?: InstallMode;
70
90
  /** Minimum background duration before checking on resume (ms) */
71
91
  minimumBackgroundDuration?: number;
72
92
  /** Show built-in progress popup (default: from configure() or false) */
@@ -76,6 +96,7 @@ export interface SyncOptions {
76
96
  }
77
97
  export declare enum SyncStatus {
78
98
  UP_TO_DATE = "UP_TO_DATE",
99
+ UPDATE_AVAILABLE = "UPDATE_AVAILABLE",
79
100
  UPDATE_INSTALLED = "UPDATE_INSTALLED",
80
101
  UPDATE_IGNORED = "UPDATE_IGNORED",
81
102
  APK_INSTALL_PENDING = "APK_INSTALL_PENDING",
package/lib/types.js CHANGED
@@ -4,6 +4,7 @@ exports.SyncStatus = void 0;
4
4
  var SyncStatus;
5
5
  (function (SyncStatus) {
6
6
  SyncStatus["UP_TO_DATE"] = "UP_TO_DATE";
7
+ SyncStatus["UPDATE_AVAILABLE"] = "UPDATE_AVAILABLE";
7
8
  SyncStatus["UPDATE_INSTALLED"] = "UPDATE_INSTALLED";
8
9
  SyncStatus["UPDATE_IGNORED"] = "UPDATE_IGNORED";
9
10
  SyncStatus["APK_INSTALL_PENDING"] = "APK_INSTALL_PENDING";
@@ -1,8 +1,9 @@
1
1
  import React from 'react';
2
- import { OTAProgressState } from '../progress';
2
+ import { defaultMessages, OTAProgressState } from '../sync';
3
3
  interface OTAUpdateModalProps extends OTAProgressState {
4
4
  title?: string;
5
5
  fontFamily?: string;
6
+ messages?: Partial<typeof defaultMessages>;
6
7
  }
7
- export declare function OTAUpdateModal({ visible, status, progress, message, description, error, title, fontFamily, }: OTAUpdateModalProps): React.JSX.Element;
8
+ export declare function OTAUpdateModal({ visible, status, progress, message, description, isMandatory, error, showActions, canSkip, title, fontFamily, messages, }: OTAUpdateModalProps): React.JSX.Element;
8
9
  export {};
@@ -36,9 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.OTAUpdateModal = OTAUpdateModal;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
- const progress_1 = require("../progress");
39
+ const sync_1 = require("../sync");
40
40
  const types_1 = require("../types");
41
- function OTAUpdateModal({ visible, status, progress, message, description, error, title = progress_1.defaultMessages.title, fontFamily, }) {
41
+ function OTAUpdateModal({ visible, status, progress, message, description, isMandatory, error, showActions, canSkip, title = sync_1.defaultMessages.title, fontFamily, messages, }) {
42
+ const copy = (0, react_1.useMemo)(() => ({ ...sync_1.defaultMessages, ...messages }), [messages]);
42
43
  const customFontStyle = (0, react_1.useMemo)(() => fontFamily
43
44
  ? {
44
45
  fontFamily,
@@ -52,23 +53,74 @@ function OTAUpdateModal({ visible, status, progress, message, description, error
52
53
  if (!visible)
53
54
  return null;
54
55
  const progressPercent = Math.round(Math.min(Math.max(progress, 0), 1) * 100);
56
+ const isPrompt = status === types_1.SyncStatus.UPDATE_AVAILABLE;
57
+ const isDeferredDone = status === types_1.SyncStatus.UPDATE_INSTALLED;
58
+ const isError = status === types_1.SyncStatus.ERROR;
55
59
  const showProgressBar = status === types_1.SyncStatus.DOWNLOADING;
56
- return (<react_native_1.Modal visible transparent animationType="fade" statusBarTranslucent>
60
+ const showSpinner = status === types_1.SyncStatus.RESTARTING ||
61
+ status === types_1.SyncStatus.INSTALLING ||
62
+ (!showProgressBar && !isPrompt && !isDeferredDone && !isError);
63
+ return (<react_native_1.Modal visible transparent animationType="fade" statusBarTranslucent onRequestClose={() => {
64
+ if (canSkip)
65
+ (0, sync_1.respondToUpdatePrompt)('skip');
66
+ else if (showActions && (isDeferredDone || isError)) {
67
+ (0, sync_1.respondToUpdatePrompt)('dismiss');
68
+ }
69
+ }}>
57
70
  <react_native_1.View style={styles.overlay}>
58
71
  <react_native_1.View style={styles.card}>
59
72
  <react_native_1.Text style={[styles.title, !fontFamily && styles.titleBold, textStyle]}>
60
73
  {title}
61
74
  </react_native_1.Text>
75
+
62
76
  <react_native_1.Text style={[styles.message, textStyle]}>{error ?? message}</react_native_1.Text>
63
- {description && !error ? (<react_native_1.Text style={[styles.description, textStyle]}>{description}</react_native_1.Text>) : null}
64
77
 
65
- {showProgressBar ? (<react_native_1.View style={styles.progressTrack}>
66
- <react_native_1.View style={[styles.progressFill, { width: `${progressPercent}%` }]}/>
67
- </react_native_1.View>) : (<react_native_1.ActivityIndicator size="large" color="#2563eb" style={styles.spinner}/>)}
78
+ {isMandatory && isPrompt ? (<react_native_1.Text style={[styles.mandatory, textStyle]}>{copy.mandatoryHint}</react_native_1.Text>) : null}
79
+
80
+ {description && !error ? (<react_native_1.View style={styles.changelogBox}>
81
+ <react_native_1.Text style={[styles.changelogTitle, !fontFamily && styles.titleBold, textStyle]}>
82
+ {copy.changelogTitle}
83
+ </react_native_1.Text>
84
+ <react_native_1.ScrollView style={styles.changelogScroll} contentContainerStyle={styles.changelogContent} nestedScrollEnabled>
85
+ <react_native_1.Text style={[styles.description, textStyle]}>{description}</react_native_1.Text>
86
+ </react_native_1.ScrollView>
87
+ </react_native_1.View>) : null}
88
+
89
+ {showProgressBar ? (<>
90
+ <react_native_1.View style={styles.progressTrack}>
91
+ <react_native_1.View style={[styles.progressFill, { width: `${progressPercent}%` }]}/>
92
+ </react_native_1.View>
93
+ <react_native_1.Text style={[styles.percent, !fontFamily && styles.percentBold, textStyle]}>
94
+ {progressPercent}٪
95
+ </react_native_1.Text>
96
+ </>) : null}
97
+
98
+ {showSpinner ? (<react_native_1.ActivityIndicator size="large" color="#2563eb" style={styles.spinner}/>) : null}
68
99
 
69
- {showProgressBar ? (<react_native_1.Text style={[styles.percent, !fontFamily && styles.percentBold, textStyle]}>
70
- {progressPercent}٪
71
- </react_native_1.Text>) : null}
100
+ {showActions ? (<react_native_1.View style={styles.actions}>
101
+ {isPrompt ? (<>
102
+ <react_native_1.Pressable style={[styles.button, styles.buttonPrimary]} onPress={() => (0, sync_1.respondToUpdatePrompt)('update')}>
103
+ <react_native_1.Text style={[
104
+ styles.buttonPrimaryText,
105
+ !fontFamily && styles.titleBold,
106
+ textStyle,
107
+ ]}>
108
+ {copy.updateNow}
109
+ </react_native_1.Text>
110
+ </react_native_1.Pressable>
111
+ {canSkip ? (<react_native_1.Pressable style={[styles.button, styles.buttonGhost]} onPress={() => (0, sync_1.respondToUpdatePrompt)('skip')}>
112
+ <react_native_1.Text style={[styles.buttonGhostText, textStyle]}>{copy.skip}</react_native_1.Text>
113
+ </react_native_1.Pressable>) : null}
114
+ </>) : (<react_native_1.Pressable style={[styles.button, styles.buttonPrimary]} onPress={() => (0, sync_1.respondToUpdatePrompt)('dismiss')}>
115
+ <react_native_1.Text style={[
116
+ styles.buttonPrimaryText,
117
+ !fontFamily && styles.titleBold,
118
+ textStyle,
119
+ ]}>
120
+ {copy.dismiss}
121
+ </react_native_1.Text>
122
+ </react_native_1.Pressable>)}
123
+ </react_native_1.View>) : null}
72
124
  </react_native_1.View>
73
125
  </react_native_1.View>
74
126
  </react_native_1.Modal>);
@@ -83,11 +135,11 @@ const styles = react_native_1.StyleSheet.create({
83
135
  },
84
136
  card: {
85
137
  width: '100%',
86
- maxWidth: 320,
138
+ maxWidth: 340,
87
139
  backgroundColor: '#fff',
88
140
  borderRadius: 16,
89
141
  padding: 24,
90
- alignItems: 'center',
142
+ alignItems: 'stretch',
91
143
  },
92
144
  title: {
93
145
  fontSize: 18,
@@ -104,11 +156,38 @@ const styles = react_native_1.StyleSheet.create({
104
156
  textAlign: 'center',
105
157
  marginBottom: 8,
106
158
  },
159
+ mandatory: {
160
+ fontSize: 13,
161
+ color: '#b45309',
162
+ textAlign: 'center',
163
+ marginBottom: 8,
164
+ },
165
+ changelogBox: {
166
+ width: '100%',
167
+ marginTop: 4,
168
+ marginBottom: 12,
169
+ backgroundColor: '#f9fafb',
170
+ borderRadius: 12,
171
+ padding: 12,
172
+ maxHeight: 180,
173
+ },
174
+ changelogTitle: {
175
+ fontSize: 13,
176
+ color: '#111827',
177
+ marginBottom: 6,
178
+ textAlign: 'center',
179
+ },
180
+ changelogScroll: {
181
+ maxHeight: 140,
182
+ },
183
+ changelogContent: {
184
+ paddingBottom: 4,
185
+ },
107
186
  description: {
108
187
  fontSize: 13,
109
- color: '#6b7280',
188
+ color: '#4b5563',
110
189
  textAlign: 'center',
111
- marginBottom: 16,
190
+ lineHeight: 20,
112
191
  },
113
192
  progressTrack: {
114
193
  width: '100%',
@@ -134,5 +213,30 @@ const styles = react_native_1.StyleSheet.create({
134
213
  },
135
214
  spinner: {
136
215
  marginTop: 12,
216
+ alignSelf: 'center',
217
+ },
218
+ actions: {
219
+ marginTop: 16,
220
+ gap: 8,
221
+ },
222
+ button: {
223
+ borderRadius: 10,
224
+ paddingVertical: 12,
225
+ paddingHorizontal: 16,
226
+ alignItems: 'center',
227
+ },
228
+ buttonPrimary: {
229
+ backgroundColor: '#2563eb',
230
+ },
231
+ buttonPrimaryText: {
232
+ color: '#fff',
233
+ fontSize: 15,
234
+ },
235
+ buttonGhost: {
236
+ backgroundColor: 'transparent',
237
+ },
238
+ buttonGhostText: {
239
+ color: '#6b7280',
240
+ fontSize: 14,
137
241
  },
138
242
  });
@@ -97,6 +97,6 @@ function OTAUpdateProvider({ children, projectConfig, autoSync = true, skipInDev
97
97
  }, []);
98
98
  return (<>
99
99
  {children}
100
- <OTAUpdateModal_1.OTAUpdateModal {...progress} title={modalTitle} fontFamily={fontFamily}/>
100
+ <OTAUpdateModal_1.OTAUpdateModal {...progress} title={modalTitle} fontFamily={fontFamily} messages={messages}/>
101
101
  </>);
102
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsonair.ir/react-native",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "React Native client SDK for self-hosted OTA updates",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",