@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 +2 -2
- package/lib/index.js +11 -9
- package/lib/sync.d.ts +17 -1
- package/lib/sync.js +156 -18
- package/lib/types.d.ts +25 -4
- package/lib/types.js +1 -0
- package/lib/ui/OTAUpdateModal.d.ts +3 -2
- package/lib/ui/OTAUpdateModal.js +118 -14
- package/lib/ui/OTAUpdateProvider.js +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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),
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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?:
|
|
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
|
-
/**
|
|
18
|
-
|
|
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?:
|
|
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 '../
|
|
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 {};
|
package/lib/ui/OTAUpdateModal.js
CHANGED
|
@@ -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
|
|
39
|
+
const sync_1 = require("../sync");
|
|
40
40
|
const types_1 = require("../types");
|
|
41
|
-
function OTAUpdateModal({ visible, status, progress, message, description, error, title =
|
|
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
|
-
|
|
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
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
{
|
|
70
|
-
{
|
|
71
|
-
|
|
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:
|
|
138
|
+
maxWidth: 340,
|
|
87
139
|
backgroundColor: '#fff',
|
|
88
140
|
borderRadius: 16,
|
|
89
141
|
padding: 24,
|
|
90
|
-
alignItems: '
|
|
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: '#
|
|
188
|
+
color: '#4b5563',
|
|
110
189
|
textAlign: 'center',
|
|
111
|
-
|
|
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
|
}
|