@appsonair.ir/react-native 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -20,10 +26,20 @@ export declare const defaultMessages: {
20
26
  restarting: string;
21
27
  error: string;
22
28
  upToDate: string;
29
+ updateAvailable: string;
30
+ changelogTitle: string;
31
+ updateNow: string;
32
+ skip: string;
33
+ dismiss: string;
34
+ appliedNextLaunch: string;
35
+ mandatoryHint: string;
23
36
  };
24
37
  export declare function getMessageForStatus(status: SyncStatus, messages?: Partial<typeof defaultMessages>): string;
25
38
  export declare function getModalTitle(messages?: Partial<typeof defaultMessages>): string;
26
39
  export declare function delay(ms: number): Promise<void>;
40
+ type UpdatePromptAction = 'update' | 'skip' | 'dismiss';
41
+ /** Resolve the Update / Skip / Dismiss prompt shown by OTAUpdateModal. */
42
+ export declare function respondToUpdatePrompt(action: UpdatePromptAction): void;
27
43
  export declare function getStoredBundleMeta(): Promise<StoredBundleMeta | null>;
28
44
  export declare function setStoredBundleMeta(meta: StoredBundleMeta): Promise<void>;
29
45
  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 = {
@@ -73,18 +76,29 @@ exports.defaultMessages = {
73
76
  restarting: 'در حال راه\u200cاندازی مجدد...',
74
77
  error: 'خطا در به\u200cروزرسانی',
75
78
  upToDate: 'برنامه به\u200cروز است',
79
+ updateAvailable: 'نسخه جدیدی در دسترس است',
80
+ changelogTitle: 'تغییرات این نسخه',
81
+ updateNow: 'به\u200cروزرسانی',
82
+ skip: 'فعلاً نه',
83
+ dismiss: 'باشه',
84
+ appliedNextLaunch: 'به\u200cروزرسانی نصب شد و در اجرای بعدی برنامه اعمال می\u200cشود',
85
+ mandatoryHint: 'این به\u200cروزرسانی الزامی است',
76
86
  };
77
87
  function getMessageForStatus(status, messages = {}) {
78
88
  const m = { ...exports.defaultMessages, ...messages };
79
89
  switch (status) {
80
90
  case types_1.SyncStatus.CHECKING:
81
91
  return m.checking;
92
+ case types_1.SyncStatus.UPDATE_AVAILABLE:
93
+ return m.updateAvailable;
82
94
  case types_1.SyncStatus.DOWNLOADING:
83
95
  return m.downloading;
84
96
  case types_1.SyncStatus.INSTALLING:
85
97
  return m.installing;
86
98
  case types_1.SyncStatus.RESTARTING:
87
99
  return m.restarting;
100
+ case types_1.SyncStatus.UPDATE_INSTALLED:
101
+ return m.appliedNextLaunch;
88
102
  case types_1.SyncStatus.ERROR:
89
103
  return m.error;
90
104
  default:
@@ -124,7 +138,41 @@ const CLIENT_ID_KEY = '@appsonair.ir/client-id';
124
138
  const NATIVE_VERSION_KEY = '@appsonair.ir/native-version';
125
139
  const NATIVE_BUILD_KEY = '@appsonair.ir/native-build';
126
140
  const RESTART_COOLDOWN_KEY = '@appsonair.ir/last-restart';
141
+ const SKIPPED_UPDATE_KEY = '@appsonair.ir/skipped-update';
127
142
  const RESTART_COOLDOWN_MS = 15000;
143
+ let pendingPrompt = null;
144
+ /** Resolve the Update / Skip / Dismiss prompt shown by OTAUpdateModal. */
145
+ function respondToUpdatePrompt(action) {
146
+ if (!pendingPrompt)
147
+ return;
148
+ const { resolve } = pendingPrompt;
149
+ pendingPrompt = null;
150
+ resolve(action);
151
+ }
152
+ function waitForUpdatePrompt() {
153
+ return new Promise((resolve) => {
154
+ pendingPrompt = { resolve };
155
+ });
156
+ }
157
+ function resolveInstallMode(mode) {
158
+ if (mode === 'next-launch' || mode === 'on-next-restart' || mode === 'on-next-resume') {
159
+ return 'next-launch';
160
+ }
161
+ return 'immediate';
162
+ }
163
+ function getUpdateChangelog(update) {
164
+ const text = update.changelog ?? update.description;
165
+ return text?.trim() ? text.trim() : undefined;
166
+ }
167
+ async function getSkippedUpdateLabel() {
168
+ return async_storage_1.default.getItem(SKIPPED_UPDATE_KEY);
169
+ }
170
+ async function markUpdateSkipped(label) {
171
+ await async_storage_1.default.setItem(SKIPPED_UPDATE_KEY, label);
172
+ }
173
+ async function clearSkippedUpdate() {
174
+ await async_storage_1.default.removeItem(SKIPPED_UPDATE_KEY);
175
+ }
128
176
  async function getStoredBundleMeta() {
129
177
  const raw = await async_storage_1.default.getItem(STORAGE_KEY);
130
178
  if (!raw)
@@ -503,23 +551,50 @@ function reportProgress(showUI, messages, onProgress, status, progress = 0, extr
503
551
  onProgress?.(status, progress);
504
552
  if (!showUI)
505
553
  return;
506
- // Silent phases — never show modal for check / install / up-to-date.
554
+ // Silent phases — never show modal for check / install / ignored / up-to-date.
507
555
  if (status === types_1.SyncStatus.CHECKING ||
508
556
  status === types_1.SyncStatus.UP_TO_DATE ||
509
557
  status === types_1.SyncStatus.UPDATE_IGNORED ||
510
- status === types_1.SyncStatus.INSTALLING) {
558
+ status === types_1.SyncStatus.INSTALLING ||
559
+ status === types_1.SyncStatus.APK_INSTALL_PENDING) {
511
560
  return;
512
561
  }
513
562
  if (status === types_1.SyncStatus.ERROR) {
514
563
  showProgress(status, progress, extra?.error ?? getMessageForStatus(status, messages), {
515
564
  error: extra?.error,
565
+ showActions: true,
566
+ canSkip: false,
516
567
  ...extra,
517
568
  });
518
569
  return;
519
570
  }
520
571
  if (shouldShowModalForStatus(status)) {
521
- showProgress(status, progress, getMessageForStatus(status, messages), extra);
572
+ showProgress(status, progress, extra?.message ?? getMessageForStatus(status, messages), {
573
+ showActions: false,
574
+ canSkip: false,
575
+ ...extra,
576
+ });
577
+ }
578
+ }
579
+ async function promptForUpdate(showUI, messages, onProgress, update, installMode) {
580
+ const changelog = getUpdateChangelog(update);
581
+ const isMandatory = update.isMandatory === true;
582
+ if (!showUI) {
583
+ return 'update';
584
+ }
585
+ reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_AVAILABLE, 0, {
586
+ label: update.label,
587
+ description: changelog,
588
+ isMandatory,
589
+ installMode,
590
+ showActions: true,
591
+ canSkip: !isMandatory,
592
+ });
593
+ const action = await waitForUpdatePrompt();
594
+ if (action === 'skip' && !isMandatory) {
595
+ return 'skip';
522
596
  }
597
+ return 'update';
523
598
  }
524
599
  async function restartApp() {
525
600
  if (OTAUpdaterNative?.restartApp) {
@@ -590,16 +665,51 @@ async function performSync(options = {}, onProgress, messages) {
590
665
  onProgress?.(types_1.SyncStatus.UP_TO_DATE, 1);
591
666
  return { status: types_1.SyncStatus.UP_TO_DATE };
592
667
  }
668
+ const installMode = resolveInstallMode(options.installMode ?? config.installMode ?? 'immediate');
669
+ const changelog = getUpdateChangelog(update);
670
+ if (!update.isMandatory) {
671
+ const skipped = await getSkippedUpdateLabel();
672
+ if (skipped && skipped === update.label) {
673
+ if (showUI)
674
+ hideProgress();
675
+ onProgress?.(types_1.SyncStatus.UPDATE_IGNORED, 1);
676
+ return {
677
+ status: types_1.SyncStatus.UPDATE_IGNORED,
678
+ label: update.label,
679
+ description: changelog,
680
+ };
681
+ }
682
+ }
683
+ else {
684
+ await clearSkippedUpdate();
685
+ }
686
+ const decision = await promptForUpdate(showUI, messages, onProgress, update, installMode);
687
+ if (decision === 'skip') {
688
+ await markUpdateSkipped(update.label);
689
+ if (showUI)
690
+ hideProgress();
691
+ onProgress?.(types_1.SyncStatus.UPDATE_IGNORED, 1);
692
+ return {
693
+ status: types_1.SyncStatus.UPDATE_IGNORED,
694
+ label: update.label,
695
+ description: changelog,
696
+ };
697
+ }
698
+ await clearSkippedUpdate();
593
699
  if (update.updateType === 'apk') {
594
- return performApkSync(update, showUI, messages, onProgress);
700
+ return performApkSync(update, showUI, messages, onProgress, changelog);
595
701
  }
596
702
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, {
597
703
  label: update.label,
598
- description: update.description,
704
+ description: changelog,
705
+ isMandatory: update.isMandatory,
706
+ installMode,
599
707
  });
600
708
  const bundlePath = await downloadBundle(update.downloadUrl, config.deploymentKey, update.label, update.size, (p) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
601
709
  label: update.label,
602
- description: update.description,
710
+ description: changelog,
711
+ isMandatory: update.isMandatory,
712
+ installMode,
603
713
  }), 'bundle', undefined, update.packageFormat ?? 'bundle', update.packageHash);
604
714
  onProgress?.(types_1.SyncStatus.INSTALLING, 1);
605
715
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
@@ -615,12 +725,13 @@ async function performSync(options = {}, onProgress, messages) {
615
725
  packageHash: update.packageHash,
616
726
  installedAt: new Date().toISOString(),
617
727
  });
618
- const installMode = options.installMode ?? config.installMode ?? 'immediate';
619
728
  const shouldRestartNow = installMode === 'immediate' || update.isMandatory === true;
620
729
  if (shouldRestartNow) {
621
730
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.RESTARTING, 1, {
622
731
  label: update.label,
623
- description: update.description,
732
+ description: changelog,
733
+ isMandatory: update.isMandatory,
734
+ installMode,
624
735
  });
625
736
  await markRestartPending();
626
737
  await delay(300);
@@ -628,40 +739,60 @@ async function performSync(options = {}, onProgress, messages) {
628
739
  return {
629
740
  status: types_1.SyncStatus.UPDATE_INSTALLED,
630
741
  label: update.label,
631
- description: update.description,
742
+ description: changelog,
632
743
  };
633
744
  }
634
- if (showUI)
745
+ if (showUI) {
746
+ reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_INSTALLED, 1, {
747
+ label: update.label,
748
+ description: changelog,
749
+ isMandatory: update.isMandatory,
750
+ installMode,
751
+ showActions: true,
752
+ canSkip: false,
753
+ message: getMessageForStatus(types_1.SyncStatus.UPDATE_INSTALLED, messages),
754
+ });
755
+ await waitForUpdatePrompt();
635
756
  hideProgress();
757
+ }
636
758
  return {
637
759
  status: types_1.SyncStatus.UPDATE_INSTALLED,
638
760
  label: update.label,
639
- description: update.description,
761
+ description: changelog,
640
762
  };
641
763
  }
642
764
  catch (error) {
643
765
  const err = error instanceof Error ? error : new Error(String(error));
644
766
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.ERROR, 0, {
645
767
  error: err.message,
768
+ showActions: true,
769
+ canSkip: false,
646
770
  });
771
+ if (showUI) {
772
+ await waitForUpdatePrompt();
773
+ hideProgress();
774
+ }
647
775
  return {
648
776
  status: types_1.SyncStatus.ERROR,
649
777
  error: err,
650
778
  };
651
779
  }
652
780
  }
653
- async function performApkSync(update, showUI, messages, onProgress) {
781
+ async function performApkSync(update, showUI, messages, onProgress, changelog) {
654
782
  if (!update.downloadUrl || !update.label || !update.packageHash) {
655
783
  return { status: types_1.SyncStatus.UP_TO_DATE };
656
784
  }
785
+ const notes = changelog ?? getUpdateChangelog(update);
657
786
  try {
658
787
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, {
659
788
  label: update.label,
660
- description: update.description,
789
+ description: notes,
790
+ isMandatory: update.isMandatory,
661
791
  });
662
792
  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
793
  label: update.label,
664
- description: update.description,
794
+ description: notes,
795
+ isMandatory: update.isMandatory,
665
796
  }), 'apk', update.deviceAbi);
666
797
  onProgress?.(types_1.SyncStatus.INSTALLING, 1);
667
798
  const valid = await verifyBundleHash(apkPath, update.packageHash);
@@ -675,12 +806,13 @@ async function performApkSync(update, showUI, messages, onProgress) {
675
806
  if (installResult === 'permission_required') {
676
807
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.INSTALLING, 1, {
677
808
  label: update.label,
678
- description: update.description,
809
+ description: notes,
810
+ isMandatory: update.isMandatory,
679
811
  });
680
812
  return {
681
813
  status: types_1.SyncStatus.APK_INSTALL_PENDING,
682
814
  label: update.label,
683
- description: update.description,
815
+ description: notes,
684
816
  };
685
817
  }
686
818
  if (showUI)
@@ -689,14 +821,20 @@ async function performApkSync(update, showUI, messages, onProgress) {
689
821
  return {
690
822
  status: types_1.SyncStatus.APK_INSTALL_PENDING,
691
823
  label: update.label,
692
- description: update.description,
824
+ description: notes,
693
825
  };
694
826
  }
695
827
  catch (error) {
696
828
  const err = error instanceof Error ? error : new Error(String(error));
697
829
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.ERROR, 0, {
698
830
  error: err.message,
831
+ showActions: true,
832
+ canSkip: false,
699
833
  });
834
+ if (showUI) {
835
+ await waitForUpdatePrompt();
836
+ hideProgress();
837
+ }
700
838
  return {
701
839
  status: types_1.SyncStatus.ERROR,
702
840
  error: err,
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.2",
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",