@appsonair.ir/react-native 1.0.7 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/otaupdater/OTAApkInstallHelper.kt +17 -0
- package/android/src/main/java/com/otaupdater/OTAZipExtractHelper.kt +29 -14
- package/lib/index.d.ts +2 -0
- package/lib/index.js +5 -1
- package/lib/progress.d.ts +1 -1
- package/lib/progress.js +2 -1
- package/lib/sync.d.ts +25 -2
- package/lib/sync.js +123 -53
- package/lib/types.d.ts +41 -1
- package/lib/ui/OTAUpdateModal.d.ts +3 -8
- package/lib/ui/OTAUpdateModal.js +97 -7
- package/lib/ui/OTAUpdateProvider.d.ts +1 -1
- package/lib/ui/OTAUpdateProvider.js +22 -3
- package/lib/zipExtract.js +6 -0
- package/package.json +1 -1
|
@@ -21,6 +21,23 @@ object OTAApkInstallHelper {
|
|
|
21
21
|
throw IllegalArgumentException("APK file is missing or invalid: ${file.absolutePath}")
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
if (file.name.endsWith(".zip", ignoreCase = true)) {
|
|
25
|
+
throw IllegalArgumentException(
|
|
26
|
+
"Downloaded zip was not extracted to an APK before install: ${file.name}",
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
val archiveInfo =
|
|
31
|
+
reactContext.packageManager.getPackageArchiveInfo(
|
|
32
|
+
file.absolutePath,
|
|
33
|
+
0,
|
|
34
|
+
)
|
|
35
|
+
if (archiveInfo == null) {
|
|
36
|
+
throw IllegalArgumentException(
|
|
37
|
+
"Wrong package: file is not a valid Android APK (${file.name})",
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
if (needsInstallPermission(reactContext) &&
|
|
25
42
|
!reactContext.packageManager.canRequestPackageInstalls()) {
|
|
26
43
|
OTAUpdaterStorage.setPendingApkPath(reactContext, file.absolutePath)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
package com.otaupdater
|
|
2
2
|
|
|
3
|
+
import java.io.BufferedInputStream
|
|
3
4
|
import java.io.File
|
|
4
|
-
import java.io.FileInputStream
|
|
5
5
|
import java.io.FileOutputStream
|
|
6
|
-
import java.util.zip.
|
|
6
|
+
import java.util.zip.ZipFile
|
|
7
7
|
|
|
8
8
|
object OTAZipExtractHelper {
|
|
9
9
|
fun extract(zipPath: String, destDirPath: String) {
|
|
@@ -19,28 +19,43 @@ object OTAZipExtractHelper {
|
|
|
19
19
|
}
|
|
20
20
|
destDir.mkdirs()
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// ZipFile handles Info-ZIP / macOS ditto archives more reliably than ZipInputStream.
|
|
23
|
+
ZipFile(zipFile).use { zip ->
|
|
24
|
+
val entries = zip.entries()
|
|
25
|
+
while (entries.hasMoreElements()) {
|
|
26
|
+
val entry = entries.nextElement()
|
|
27
|
+
val normalized = entry.name.replace('\\', '/').trimStart('/')
|
|
28
|
+
val baseName = normalized.substringAfterLast('/')
|
|
29
|
+
if (
|
|
30
|
+
normalized.split('/').contains("__MACOSX") ||
|
|
31
|
+
baseName.startsWith("._") ||
|
|
32
|
+
baseName == ".DS_Store"
|
|
33
|
+
) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val outFile = safeOutputFile(destDir, normalized)
|
|
26
38
|
|
|
27
39
|
if (entry.isDirectory) {
|
|
28
40
|
outFile.mkdirs()
|
|
29
|
-
|
|
30
|
-
outFile.parentFile?.mkdirs()
|
|
31
|
-
FileOutputStream(outFile).use { output ->
|
|
32
|
-
zipInput.copyTo(output)
|
|
33
|
-
}
|
|
41
|
+
continue
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
entry
|
|
44
|
+
outFile.parentFile?.mkdirs()
|
|
45
|
+
zip.getInputStream(entry).use { input ->
|
|
46
|
+
BufferedInputStream(input).use { buffered ->
|
|
47
|
+
FileOutputStream(outFile).use { output ->
|
|
48
|
+
buffered.copyTo(output)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
private fun safeOutputFile(destDir: File, entryName: String): File {
|
|
43
|
-
val
|
|
57
|
+
val normalized = entryName.replace('\\', '/').trimStart('/')
|
|
58
|
+
val outFile = File(destDir, normalized)
|
|
44
59
|
val destPath = destDir.canonicalPath
|
|
45
60
|
val outPath = outFile.canonicalPath
|
|
46
61
|
if (outPath != destPath && !outPath.startsWith("$destPath${File.separator}")) {
|
package/lib/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { setUserInfo, clearUserInfo, getUserInfo } from './userInfo';
|
|
|
18
18
|
export type { OTAUserInfo } from './userInfo';
|
|
19
19
|
export { OTAUpdateModal } from './ui/OTAUpdateModal';
|
|
20
20
|
export { OTAUpdateProvider } from './ui/OTAUpdateProvider';
|
|
21
|
+
export { formatBytes, getProgressState, subscribeProgress } from './progress';
|
|
22
|
+
export type { OTAProgressState } from './sync';
|
|
21
23
|
export declare const OTAUpdater: {
|
|
22
24
|
configure: typeof configure;
|
|
23
25
|
getConfig: typeof getConfig;
|
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.getUserInfo = exports.clearUserInfo = exports.setUserInfo = exports.logEvent = exports.stopTelemetry = exports.startTelemetry = exports.ping = 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;
|
|
20
|
+
exports.OTAUpdater = exports.subscribeProgress = exports.getProgressState = exports.formatBytes = exports.OTAUpdateProvider = exports.OTAUpdateModal = exports.getUserInfo = exports.clearUserInfo = exports.setUserInfo = exports.logEvent = exports.stopTelemetry = exports.startTelemetry = exports.ping = 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"));
|
|
@@ -77,6 +77,10 @@ var OTAUpdateModal_1 = require("./ui/OTAUpdateModal");
|
|
|
77
77
|
Object.defineProperty(exports, "OTAUpdateModal", { enumerable: true, get: function () { return OTAUpdateModal_1.OTAUpdateModal; } });
|
|
78
78
|
var OTAUpdateProvider_1 = require("./ui/OTAUpdateProvider");
|
|
79
79
|
Object.defineProperty(exports, "OTAUpdateProvider", { enumerable: true, get: function () { return OTAUpdateProvider_1.OTAUpdateProvider; } });
|
|
80
|
+
var progress_1 = require("./progress");
|
|
81
|
+
Object.defineProperty(exports, "formatBytes", { enumerable: true, get: function () { return progress_1.formatBytes; } });
|
|
82
|
+
Object.defineProperty(exports, "getProgressState", { enumerable: true, get: function () { return progress_1.getProgressState; } });
|
|
83
|
+
Object.defineProperty(exports, "subscribeProgress", { enumerable: true, get: function () { return progress_1.subscribeProgress; } });
|
|
80
84
|
exports.OTAUpdater = {
|
|
81
85
|
configure: config_1.configure,
|
|
82
86
|
getConfig: config_1.getConfig,
|
package/lib/progress.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { OTAProgressState } from './sync';
|
|
2
|
-
export { defaultMessages, delay, getMessageForStatus, getModalTitle, getProgressState, hideProgress, subscribeProgress, } from './sync';
|
|
2
|
+
export { defaultMessages, delay, formatBytes, getMessageForStatus, getModalTitle, getProgressState, hideProgress, subscribeProgress, } from './sync';
|
package/lib/progress.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.subscribeProgress = exports.hideProgress = exports.getProgressState = exports.getModalTitle = exports.getMessageForStatus = exports.delay = exports.defaultMessages = void 0;
|
|
3
|
+
exports.subscribeProgress = exports.hideProgress = exports.getProgressState = exports.getModalTitle = exports.getMessageForStatus = exports.formatBytes = exports.delay = exports.defaultMessages = void 0;
|
|
4
4
|
var sync_1 = require("./sync");
|
|
5
5
|
Object.defineProperty(exports, "defaultMessages", { enumerable: true, get: function () { return sync_1.defaultMessages; } });
|
|
6
6
|
Object.defineProperty(exports, "delay", { enumerable: true, get: function () { return sync_1.delay; } });
|
|
7
|
+
Object.defineProperty(exports, "formatBytes", { enumerable: true, get: function () { return sync_1.formatBytes; } });
|
|
7
8
|
Object.defineProperty(exports, "getMessageForStatus", { enumerable: true, get: function () { return sync_1.getMessageForStatus; } });
|
|
8
9
|
Object.defineProperty(exports, "getModalTitle", { enumerable: true, get: function () { return sync_1.getModalTitle; } });
|
|
9
10
|
Object.defineProperty(exports, "getProgressState", { enumerable: true, get: function () { return sync_1.getProgressState; } });
|
package/lib/sync.d.ts
CHANGED
|
@@ -13,6 +13,22 @@ export interface OTAProgressState {
|
|
|
13
13
|
showActions?: boolean;
|
|
14
14
|
canSkip?: boolean;
|
|
15
15
|
error?: string;
|
|
16
|
+
/** Whether this update is a JS bundle or native APK */
|
|
17
|
+
updateType?: 'bundle' | 'apk';
|
|
18
|
+
/** Target release app version (from server) */
|
|
19
|
+
appVersion?: string;
|
|
20
|
+
/** Target APK versionCode (from server) */
|
|
21
|
+
buildNumber?: number;
|
|
22
|
+
/** Currently installed native app version */
|
|
23
|
+
currentAppVersion?: string;
|
|
24
|
+
/** Currently installed native build / versionCode */
|
|
25
|
+
currentBuildNumber?: string;
|
|
26
|
+
/** Currently installed JS bundle label, if any */
|
|
27
|
+
currentLabel?: string;
|
|
28
|
+
/** Total download size in bytes (0 if unknown) */
|
|
29
|
+
totalBytes?: number;
|
|
30
|
+
/** Bytes downloaded so far */
|
|
31
|
+
downloadedBytes?: number;
|
|
16
32
|
}
|
|
17
33
|
type ProgressListener = (state: OTAProgressState) => void;
|
|
18
34
|
export declare function getProgressState(): OTAProgressState;
|
|
@@ -20,6 +36,8 @@ export declare function subscribeProgress(listener: ProgressListener): () => voi
|
|
|
20
36
|
export declare function hideProgress(): void;
|
|
21
37
|
export declare const defaultMessages: {
|
|
22
38
|
title: string;
|
|
39
|
+
titleBundle: string;
|
|
40
|
+
titleApk: string;
|
|
23
41
|
checking: string;
|
|
24
42
|
downloading: string;
|
|
25
43
|
installing: string;
|
|
@@ -28,15 +46,20 @@ export declare const defaultMessages: {
|
|
|
28
46
|
downloadInterrupted: string;
|
|
29
47
|
upToDate: string;
|
|
30
48
|
updateAvailable: string;
|
|
49
|
+
updateAvailableBundle: string;
|
|
50
|
+
updateAvailableApk: string;
|
|
31
51
|
changelogTitle: string;
|
|
32
52
|
updateNow: string;
|
|
33
53
|
skip: string;
|
|
34
54
|
dismiss: string;
|
|
35
55
|
appliedNextLaunch: string;
|
|
36
56
|
mandatoryHint: string;
|
|
57
|
+
versionBundle: string;
|
|
58
|
+
versionApk: string;
|
|
37
59
|
};
|
|
38
60
|
export declare function getMessageForStatus(status: SyncStatus, messages?: Partial<typeof defaultMessages>): string;
|
|
39
|
-
export declare function getModalTitle(messages?: Partial<typeof defaultMessages
|
|
61
|
+
export declare function getModalTitle(messages?: Partial<typeof defaultMessages>, updateType?: 'bundle' | 'apk'): string;
|
|
62
|
+
export declare function formatBytes(bytes: number | undefined): string;
|
|
40
63
|
export declare function delay(ms: number): Promise<void>;
|
|
41
64
|
type UpdatePromptAction = 'update' | 'skip' | 'dismiss';
|
|
42
65
|
/** Resolve the Update / Skip / Dismiss prompt shown by OTAUpdateModal. */
|
|
@@ -54,7 +77,7 @@ export declare function checkForUpdate(serverUrl: string, deploymentKey: string,
|
|
|
54
77
|
export declare function getBundleReleaseDir(label: string): string;
|
|
55
78
|
export declare function getBundleFilePath(label: string): string;
|
|
56
79
|
export declare function getApkFilePath(label: string): string;
|
|
57
|
-
export declare function downloadBundle(downloadUrl: string, deploymentKey: string, label: string, expectedSize?: number, onProgress?: (progress: number) => void, extension?: 'bundle' | 'apk', deviceAbi?: string, packageFormat?: 'bundle' | 'zip', expectedHash?: string): Promise<string>;
|
|
80
|
+
export declare function downloadBundle(downloadUrl: string, deploymentKey: string, label: string, expectedSize?: number, onProgress?: (progress: number, received?: number, total?: number) => void, extension?: 'bundle' | 'apk', deviceAbi?: string, packageFormat?: 'bundle' | 'zip', expectedHash?: string): Promise<string>;
|
|
58
81
|
export declare function verifyBundleHash(filePath: string, expectedHash: string): Promise<boolean>;
|
|
59
82
|
type ProgressCallback = (status: SyncStatus, progress?: number) => void;
|
|
60
83
|
export declare function isSyncInProgress(): boolean;
|
package/lib/sync.js
CHANGED
|
@@ -42,6 +42,7 @@ exports.subscribeProgress = subscribeProgress;
|
|
|
42
42
|
exports.hideProgress = hideProgress;
|
|
43
43
|
exports.getMessageForStatus = getMessageForStatus;
|
|
44
44
|
exports.getModalTitle = getModalTitle;
|
|
45
|
+
exports.formatBytes = formatBytes;
|
|
45
46
|
exports.delay = delay;
|
|
46
47
|
exports.respondToUpdatePrompt = respondToUpdatePrompt;
|
|
47
48
|
exports.getStoredBundleMeta = getStoredBundleMeta;
|
|
@@ -105,6 +106,8 @@ function shouldShowModalForStatus(status) {
|
|
|
105
106
|
}
|
|
106
107
|
exports.defaultMessages = {
|
|
107
108
|
title: 'به\u200cروزرسانی برنامه',
|
|
109
|
+
titleBundle: 'به\u200cروزرسانی محتوا',
|
|
110
|
+
titleApk: 'به\u200cروزرسانی نسخه برنامه',
|
|
108
111
|
checking: 'در حال بررسی به\u200cروزرسانی...',
|
|
109
112
|
downloading: 'در حال دانلود به\u200cروزرسانی...',
|
|
110
113
|
installing: 'در حال نصب به\u200cروزرسانی...',
|
|
@@ -113,12 +116,16 @@ exports.defaultMessages = {
|
|
|
113
116
|
downloadInterrupted: 'دانلود قطع شد. لطفاً دوباره تلاش کنید',
|
|
114
117
|
upToDate: 'برنامه به\u200cروز است',
|
|
115
118
|
updateAvailable: 'نسخه جدیدی در دسترس است',
|
|
119
|
+
updateAvailableBundle: 'به\u200cروزرسانی محتوا آماده است',
|
|
120
|
+
updateAvailableApk: 'نسخه جدید برنامه آماده نصب است',
|
|
116
121
|
changelogTitle: 'تغییرات این نسخه',
|
|
117
122
|
updateNow: 'به\u200cروزرسانی',
|
|
118
123
|
skip: 'فعلاً نه',
|
|
119
124
|
dismiss: 'باشه',
|
|
120
125
|
appliedNextLaunch: 'به\u200cروزرسانی نصب شد و در اجرای بعدی برنامه اعمال می\u200cشود',
|
|
121
126
|
mandatoryHint: 'این به\u200cروزرسانی الزامی است',
|
|
127
|
+
versionBundle: 'باندل',
|
|
128
|
+
versionApk: 'نسخه برنامه',
|
|
122
129
|
};
|
|
123
130
|
function getMessageForStatus(status, messages = {}) {
|
|
124
131
|
const m = { ...exports.defaultMessages, ...messages };
|
|
@@ -141,10 +148,28 @@ function getMessageForStatus(status, messages = {}) {
|
|
|
141
148
|
return m.upToDate;
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
|
-
function getModalTitle(messages = {}) {
|
|
151
|
+
function getModalTitle(messages = {}, updateType) {
|
|
145
152
|
const m = { ...exports.defaultMessages, ...messages };
|
|
153
|
+
if (updateType === 'apk') {
|
|
154
|
+
return m.titleApk ?? m.title;
|
|
155
|
+
}
|
|
156
|
+
if (updateType === 'bundle') {
|
|
157
|
+
return m.titleBundle ?? m.title;
|
|
158
|
+
}
|
|
146
159
|
return m.title;
|
|
147
160
|
}
|
|
161
|
+
function formatBytes(bytes) {
|
|
162
|
+
if (bytes == null || !Number.isFinite(bytes) || bytes <= 0) {
|
|
163
|
+
return '—';
|
|
164
|
+
}
|
|
165
|
+
if (bytes < 1024) {
|
|
166
|
+
return `${Math.round(bytes)} B`;
|
|
167
|
+
}
|
|
168
|
+
if (bytes < 1024 * 1024) {
|
|
169
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
170
|
+
}
|
|
171
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
172
|
+
}
|
|
148
173
|
function delay(ms) {
|
|
149
174
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
175
|
}
|
|
@@ -401,6 +426,14 @@ async function isZipArchive(filePath) {
|
|
|
401
426
|
return false;
|
|
402
427
|
}
|
|
403
428
|
}
|
|
429
|
+
async function shouldExtractApkZip(downloadedPath, packageFormat) {
|
|
430
|
+
// APK files are themselves zip archives (PK magic). Only unzip when the
|
|
431
|
+
// server said this package is a zip *of* an APK, or the download is named .zip.
|
|
432
|
+
if (packageFormat === 'zip') {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
return downloadedPath.toLowerCase().endsWith('.zip');
|
|
436
|
+
}
|
|
404
437
|
async function shouldExtractZipPackage(downloadedPath, packageFormat) {
|
|
405
438
|
if (packageFormat === 'zip') {
|
|
406
439
|
return true;
|
|
@@ -539,14 +572,33 @@ async function pickExtractedApk(dir, deviceAbi) {
|
|
|
539
572
|
async function extractApkUpdate(label, zipPath, deviceAbi) {
|
|
540
573
|
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
541
574
|
const extractDir = getApkExtractDir(label);
|
|
575
|
+
const finalApkPath = getApkFilePath(label);
|
|
542
576
|
await removePathRecursive(extractDir);
|
|
543
577
|
await ReactNativeBlobUtil.fs.mkdir(extractDir);
|
|
544
578
|
await (0, zipExtract_1.extractZipToDirectory)(zipPath, extractDir);
|
|
579
|
+
const extractedApk = await pickExtractedApk(extractDir, deviceAbi);
|
|
580
|
+
if (!extractedApk.toLowerCase().endsWith('.apk')) {
|
|
581
|
+
throw new Error('Zip did not contain a valid APK');
|
|
582
|
+
}
|
|
583
|
+
// Never hand the zip (or a nested extract path) to the installer.
|
|
584
|
+
if (extractedApk !== finalApkPath) {
|
|
585
|
+
const finalExists = await ReactNativeBlobUtil.fs.exists(finalApkPath);
|
|
586
|
+
if (finalExists) {
|
|
587
|
+
await ReactNativeBlobUtil.fs.unlink(finalApkPath);
|
|
588
|
+
}
|
|
589
|
+
await ReactNativeBlobUtil.fs.cp(extractedApk, finalApkPath);
|
|
590
|
+
}
|
|
545
591
|
const zipExists = await ReactNativeBlobUtil.fs.exists(zipPath);
|
|
546
592
|
if (zipExists) {
|
|
547
593
|
await ReactNativeBlobUtil.fs.unlink(zipPath);
|
|
548
594
|
}
|
|
549
|
-
|
|
595
|
+
// Drop extract clutter; keep only the installable APK.
|
|
596
|
+
await removePathRecursive(extractDir);
|
|
597
|
+
const apkExists = await ReactNativeBlobUtil.fs.exists(finalApkPath);
|
|
598
|
+
if (!apkExists) {
|
|
599
|
+
throw new Error('Failed to prepare APK for install');
|
|
600
|
+
}
|
|
601
|
+
return finalApkPath;
|
|
550
602
|
}
|
|
551
603
|
function getBundleDirectory() {
|
|
552
604
|
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
@@ -689,7 +741,7 @@ async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expe
|
|
|
689
741
|
else if (!sizeLooksComplete) {
|
|
690
742
|
return null;
|
|
691
743
|
}
|
|
692
|
-
if (extension === 'apk' && (
|
|
744
|
+
if (extension === 'apk' && (await shouldExtractApkZip(destPath, packageFormat))) {
|
|
693
745
|
return extractApkUpdate(label, destPath, deviceAbi);
|
|
694
746
|
}
|
|
695
747
|
if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
|
|
@@ -736,9 +788,11 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
|
|
|
736
788
|
if (!onProgress)
|
|
737
789
|
return;
|
|
738
790
|
const resolvedTotal = total > 0 ? total : totalBytes;
|
|
739
|
-
if (resolvedTotal <= 0)
|
|
791
|
+
if (resolvedTotal <= 0) {
|
|
792
|
+
onProgress(0, Math.max(received, 0), 0);
|
|
740
793
|
return;
|
|
741
|
-
|
|
794
|
+
}
|
|
795
|
+
onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99), Math.max(received, 0), resolvedTotal);
|
|
742
796
|
};
|
|
743
797
|
let pollTimer = null;
|
|
744
798
|
if (onProgress && totalBytes > 0) {
|
|
@@ -762,9 +816,6 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
|
|
|
762
816
|
}
|
|
763
817
|
try {
|
|
764
818
|
const response = await task;
|
|
765
|
-
if (onProgress) {
|
|
766
|
-
onProgress(1);
|
|
767
|
-
}
|
|
768
819
|
const downloadedPath = String(response.path()).replace(/^file:\/\//, '');
|
|
769
820
|
if (downloadedPath !== destPath) {
|
|
770
821
|
await ReactNativeBlobUtil.fs.cp(downloadedPath, destPath);
|
|
@@ -774,13 +825,16 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
|
|
|
774
825
|
if (!stat || stat.size < minSize) {
|
|
775
826
|
throw new Error(`Downloaded file is too small (${stat?.size ?? 0} bytes)`);
|
|
776
827
|
}
|
|
828
|
+
if (onProgress) {
|
|
829
|
+
onProgress(1, stat.size, totalBytes > 0 ? totalBytes : stat.size);
|
|
830
|
+
}
|
|
777
831
|
if (expectedHash) {
|
|
778
832
|
const valid = await verifyBundleHash(destPath, expectedHash);
|
|
779
833
|
if (!valid) {
|
|
780
|
-
throw new Error('
|
|
834
|
+
throw new Error('Wrong package: hash verification failed');
|
|
781
835
|
}
|
|
782
836
|
}
|
|
783
|
-
if (extension === 'apk' && (
|
|
837
|
+
if (extension === 'apk' && (await shouldExtractApkZip(destPath, packageFormat))) {
|
|
784
838
|
return extractApkUpdate(label, destPath, deviceAbi);
|
|
785
839
|
}
|
|
786
840
|
if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
|
|
@@ -839,8 +893,17 @@ function reportProgress(showUI, messages, onProgress, status, progress = 0, extr
|
|
|
839
893
|
status === types_1.SyncStatus.APK_INSTALL_PENDING) {
|
|
840
894
|
return;
|
|
841
895
|
}
|
|
896
|
+
const updateType = extra?.updateType;
|
|
897
|
+
let statusMessage = extra?.message ?? getMessageForStatus(status, messages);
|
|
898
|
+
if (!extra?.message && status === types_1.SyncStatus.UPDATE_AVAILABLE && updateType) {
|
|
899
|
+
const m = { ...exports.defaultMessages, ...messages };
|
|
900
|
+
statusMessage =
|
|
901
|
+
updateType === 'apk'
|
|
902
|
+
? (m.updateAvailableApk ?? m.updateAvailable)
|
|
903
|
+
: (m.updateAvailableBundle ?? m.updateAvailable);
|
|
904
|
+
}
|
|
842
905
|
if (status === types_1.SyncStatus.ERROR) {
|
|
843
|
-
showProgress(status, progress, extra?.error ??
|
|
906
|
+
showProgress(status, progress, extra?.error ?? statusMessage, {
|
|
844
907
|
error: extra?.error,
|
|
845
908
|
showActions: true,
|
|
846
909
|
canSkip: false,
|
|
@@ -849,24 +912,36 @@ function reportProgress(showUI, messages, onProgress, status, progress = 0, extr
|
|
|
849
912
|
return;
|
|
850
913
|
}
|
|
851
914
|
if (shouldShowModalForStatus(status)) {
|
|
852
|
-
showProgress(status, progress,
|
|
915
|
+
showProgress(status, progress, statusMessage, {
|
|
853
916
|
showActions: false,
|
|
854
917
|
canSkip: false,
|
|
855
918
|
...extra,
|
|
856
919
|
});
|
|
857
920
|
}
|
|
858
921
|
}
|
|
859
|
-
|
|
860
|
-
|
|
922
|
+
function buildUpdateProgressExtra(update, installMode, currentLabel) {
|
|
923
|
+
return {
|
|
924
|
+
label: update.label,
|
|
925
|
+
description: getUpdateChangelog(update),
|
|
926
|
+
isMandatory: update.isMandatory,
|
|
927
|
+
installMode,
|
|
928
|
+
updateType: update.updateType ?? 'bundle',
|
|
929
|
+
appVersion: update.appVersion,
|
|
930
|
+
buildNumber: update.buildNumber,
|
|
931
|
+
currentAppVersion: react_native_device_info_1.default.getVersion(),
|
|
932
|
+
currentBuildNumber: react_native_1.Platform.OS === 'android' ? react_native_device_info_1.default.getBuildNumber() : undefined,
|
|
933
|
+
currentLabel: currentLabel ?? undefined,
|
|
934
|
+
totalBytes: update.size && update.size > 0 ? update.size : undefined,
|
|
935
|
+
downloadedBytes: 0,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
async function promptForUpdate(showUI, messages, onProgress, update, installMode, currentLabel) {
|
|
861
939
|
const isMandatory = update.isMandatory === true;
|
|
862
940
|
if (!showUI) {
|
|
863
941
|
return 'update';
|
|
864
942
|
}
|
|
865
943
|
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_AVAILABLE, 0, {
|
|
866
|
-
|
|
867
|
-
description: changelog,
|
|
868
|
-
isMandatory,
|
|
869
|
-
installMode,
|
|
944
|
+
...buildUpdateProgressExtra(update, installMode, currentLabel),
|
|
870
945
|
showActions: true,
|
|
871
946
|
canSkip: !isMandatory,
|
|
872
947
|
});
|
|
@@ -963,7 +1038,7 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
963
1038
|
else {
|
|
964
1039
|
await clearSkippedUpdate();
|
|
965
1040
|
}
|
|
966
|
-
const decision = await promptForUpdate(showUI, messages, onProgress, update, installMode);
|
|
1041
|
+
const decision = await promptForUpdate(showUI, messages, onProgress, update, installMode, meta?.label);
|
|
967
1042
|
if (decision === 'skip') {
|
|
968
1043
|
await markUpdateSkipped(update.label);
|
|
969
1044
|
if (showUI)
|
|
@@ -977,19 +1052,14 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
977
1052
|
}
|
|
978
1053
|
await clearSkippedUpdate();
|
|
979
1054
|
if (update.updateType === 'apk') {
|
|
980
|
-
return performApkSync(update, showUI, messages, onProgress, changelog);
|
|
1055
|
+
return performApkSync(update, showUI, messages, onProgress, changelog, meta?.label);
|
|
981
1056
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const bundlePath = await downloadBundle(update.downloadUrl, config.deploymentKey, update.label, update.size, (p) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
|
|
989
|
-
label: update.label,
|
|
990
|
-
description: changelog,
|
|
991
|
-
isMandatory: update.isMandatory,
|
|
992
|
-
installMode,
|
|
1057
|
+
const progressExtra = buildUpdateProgressExtra(update, installMode, meta?.label);
|
|
1058
|
+
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, progressExtra);
|
|
1059
|
+
const bundlePath = await downloadBundle(update.downloadUrl, config.deploymentKey, update.label, update.size, (p, received, total) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
|
|
1060
|
+
...progressExtra,
|
|
1061
|
+
downloadedBytes: received,
|
|
1062
|
+
totalBytes: total && total > 0 ? total : progressExtra.totalBytes,
|
|
993
1063
|
}), 'bundle', undefined, update.packageFormat ?? 'bundle', update.packageHash);
|
|
994
1064
|
onProgress?.(types_1.SyncStatus.INSTALLING, 1);
|
|
995
1065
|
const ReactNativeBlobUtil = require('react-native-blob-util').default;
|
|
@@ -1011,10 +1081,8 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
1011
1081
|
const shouldRestartNow = installMode === 'immediate' || update.isMandatory === true;
|
|
1012
1082
|
if (shouldRestartNow) {
|
|
1013
1083
|
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.RESTARTING, 1, {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
isMandatory: update.isMandatory,
|
|
1017
|
-
installMode,
|
|
1084
|
+
...progressExtra,
|
|
1085
|
+
downloadedBytes: progressExtra.totalBytes,
|
|
1018
1086
|
});
|
|
1019
1087
|
await markRestartPending();
|
|
1020
1088
|
await delay(300);
|
|
@@ -1027,10 +1095,8 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
1027
1095
|
}
|
|
1028
1096
|
if (showUI) {
|
|
1029
1097
|
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_INSTALLED, 1, {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
isMandatory: update.isMandatory,
|
|
1033
|
-
installMode,
|
|
1098
|
+
...progressExtra,
|
|
1099
|
+
downloadedBytes: progressExtra.totalBytes,
|
|
1034
1100
|
showActions: true,
|
|
1035
1101
|
canSkip: false,
|
|
1036
1102
|
message: getMessageForStatus(types_1.SyncStatus.UPDATE_INSTALLED, messages),
|
|
@@ -1061,22 +1127,23 @@ async function performSync(options = {}, onProgress, messages) {
|
|
|
1061
1127
|
};
|
|
1062
1128
|
}
|
|
1063
1129
|
}
|
|
1064
|
-
async function performApkSync(update, showUI, messages, onProgress, changelog) {
|
|
1130
|
+
async function performApkSync(update, showUI, messages, onProgress, changelog, currentLabel) {
|
|
1065
1131
|
if (!update.downloadUrl || !update.label || !update.packageHash) {
|
|
1066
1132
|
return { status: types_1.SyncStatus.UP_TO_DATE };
|
|
1067
1133
|
}
|
|
1068
1134
|
const notes = changelog ?? getUpdateChangelog(update);
|
|
1135
|
+
const progressExtra = buildUpdateProgressExtra(update, undefined, currentLabel);
|
|
1136
|
+
const packageFormat = update.packageFormat ?? 'zip';
|
|
1069
1137
|
try {
|
|
1070
|
-
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0,
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}), 'apk', update.deviceAbi, update.packageFormat ?? 'bundle', update.packageHash);
|
|
1138
|
+
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, progressExtra);
|
|
1139
|
+
const apkPath = await downloadBundle(update.downloadUrl, (0, config_1.getConfig)().deploymentKey, update.label, update.size, (p, received, total) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
|
|
1140
|
+
...progressExtra,
|
|
1141
|
+
downloadedBytes: received,
|
|
1142
|
+
totalBytes: total && total > 0 ? total : progressExtra.totalBytes,
|
|
1143
|
+
}), 'apk', update.deviceAbi, packageFormat, update.packageHash);
|
|
1144
|
+
if (!apkPath.toLowerCase().endsWith('.apk')) {
|
|
1145
|
+
throw new Error('Wrong package: expected an APK after zip extract');
|
|
1146
|
+
}
|
|
1080
1147
|
onProgress?.(types_1.SyncStatus.INSTALLING, 1);
|
|
1081
1148
|
// Drop the OTA JS path before opening the installer so the new APK does not
|
|
1082
1149
|
// keep loading the previous bundle on first launch after install.
|
|
@@ -1084,9 +1151,8 @@ async function performApkSync(update, showUI, messages, onProgress, changelog) {
|
|
|
1084
1151
|
const installResult = await installApk(apkPath);
|
|
1085
1152
|
if (installResult === 'permission_required') {
|
|
1086
1153
|
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.INSTALLING, 1, {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
isMandatory: update.isMandatory,
|
|
1154
|
+
...progressExtra,
|
|
1155
|
+
downloadedBytes: progressExtra.totalBytes,
|
|
1090
1156
|
});
|
|
1091
1157
|
return {
|
|
1092
1158
|
status: types_1.SyncStatus.APK_INSTALL_PENDING,
|
|
@@ -1106,6 +1172,7 @@ async function performApkSync(update, showUI, messages, onProgress, changelog) {
|
|
|
1106
1172
|
catch (error) {
|
|
1107
1173
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1108
1174
|
reportProgress(showUI, messages, onProgress, types_1.SyncStatus.ERROR, 0, {
|
|
1175
|
+
...progressExtra,
|
|
1109
1176
|
error: err.message,
|
|
1110
1177
|
showActions: true,
|
|
1111
1178
|
canSkip: false,
|
|
@@ -1143,6 +1210,9 @@ async function clearUpdate() {
|
|
|
1143
1210
|
await removePathRecursive(getBundleReleaseDir(meta.label));
|
|
1144
1211
|
await cleanupPartialDownload(getBundleZipPath(meta.label));
|
|
1145
1212
|
await cleanupPartialDownload(getLegacyBundleFilePath(meta.label));
|
|
1213
|
+
await cleanupPartialDownload(getApkFilePath(meta.label));
|
|
1214
|
+
await cleanupPartialDownload(getApkZipPath(meta.label));
|
|
1215
|
+
await removePathRecursive(getApkExtractDir(meta.label));
|
|
1146
1216
|
}
|
|
1147
1217
|
}
|
|
1148
1218
|
async function notifyAppReady() {
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
2
|
/** When to apply a downloaded JS bundle update. */
|
|
3
3
|
export type InstallMode = 'immediate' | 'next-launch'
|
|
4
4
|
/** @deprecated use `next-launch` */
|
|
@@ -35,6 +35,8 @@ export interface OTAUpdaterConfig {
|
|
|
35
35
|
}
|
|
36
36
|
export interface OTAProgressMessages {
|
|
37
37
|
title?: string;
|
|
38
|
+
titleBundle?: string;
|
|
39
|
+
titleApk?: string;
|
|
38
40
|
checking?: string;
|
|
39
41
|
downloading?: string;
|
|
40
42
|
installing?: string;
|
|
@@ -43,12 +45,43 @@ export interface OTAProgressMessages {
|
|
|
43
45
|
upToDate?: string;
|
|
44
46
|
/** Prompt when an update is available */
|
|
45
47
|
updateAvailable?: string;
|
|
48
|
+
updateAvailableBundle?: string;
|
|
49
|
+
updateAvailableApk?: string;
|
|
46
50
|
changelogTitle?: string;
|
|
47
51
|
updateNow?: string;
|
|
48
52
|
skip?: string;
|
|
49
53
|
dismiss?: string;
|
|
50
54
|
appliedNextLaunch?: string;
|
|
51
55
|
mandatoryHint?: string;
|
|
56
|
+
versionBundle?: string;
|
|
57
|
+
versionApk?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface OTAUpdateUIProps {
|
|
60
|
+
visible: boolean;
|
|
61
|
+
status: SyncStatus;
|
|
62
|
+
progress: number;
|
|
63
|
+
message: string;
|
|
64
|
+
label?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
isMandatory?: boolean;
|
|
67
|
+
installMode?: InstallMode;
|
|
68
|
+
showActions?: boolean;
|
|
69
|
+
canSkip?: boolean;
|
|
70
|
+
error?: string;
|
|
71
|
+
updateType?: 'bundle' | 'apk';
|
|
72
|
+
appVersion?: string;
|
|
73
|
+
buildNumber?: number;
|
|
74
|
+
currentAppVersion?: string;
|
|
75
|
+
currentBuildNumber?: string;
|
|
76
|
+
currentLabel?: string;
|
|
77
|
+
totalBytes?: number;
|
|
78
|
+
downloadedBytes?: number;
|
|
79
|
+
title?: string;
|
|
80
|
+
fontFamily?: string;
|
|
81
|
+
messages?: OTAProgressMessages;
|
|
82
|
+
onUpdate?: () => void;
|
|
83
|
+
onSkip?: () => void;
|
|
84
|
+
onDismiss?: () => void;
|
|
52
85
|
}
|
|
53
86
|
export interface OTAUpdateProviderProps {
|
|
54
87
|
children: ReactNode;
|
|
@@ -64,6 +97,13 @@ export interface OTAUpdateProviderProps {
|
|
|
64
97
|
fontFamily?: string;
|
|
65
98
|
/** Passed to sync() */
|
|
66
99
|
syncOptions?: SyncOptions;
|
|
100
|
+
/**
|
|
101
|
+
* Replace the built-in update modal.
|
|
102
|
+
* Receive progress props + action handlers; return null to hide.
|
|
103
|
+
*/
|
|
104
|
+
renderUpdateUI?: (props: OTAUpdateUIProps) => ReactNode;
|
|
105
|
+
/** Component alternative to renderUpdateUI */
|
|
106
|
+
UpdateComponent?: ComponentType<OTAUpdateUIProps>;
|
|
67
107
|
}
|
|
68
108
|
export interface UpdateCheckResult {
|
|
69
109
|
updateAvailable: boolean;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
fontFamily?: string;
|
|
6
|
-
messages?: Partial<typeof defaultMessages>;
|
|
7
|
-
}
|
|
8
|
-
export declare function OTAUpdateModal({ visible, status, progress, message, description, isMandatory, error, showActions, canSkip, title, fontFamily, messages, }: OTAUpdateModalProps): React.JSX.Element;
|
|
9
|
-
export {};
|
|
2
|
+
import { OTAUpdateUIProps } from '../types';
|
|
3
|
+
export type OTAUpdateModalProps = OTAUpdateUIProps;
|
|
4
|
+
export declare function OTAUpdateModal({ visible, status, progress, message, description, isMandatory, error, showActions, canSkip, updateType, appVersion, buildNumber, currentAppVersion, currentBuildNumber, currentLabel, label, totalBytes, downloadedBytes, title, fontFamily, messages, onUpdate, onSkip, onDismiss, }: OTAUpdateModalProps): React.JSX.Element;
|
package/lib/ui/OTAUpdateModal.js
CHANGED
|
@@ -38,8 +38,17 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
39
|
const sync_1 = require("../sync");
|
|
40
40
|
const types_1 = require("../types");
|
|
41
|
-
function OTAUpdateModal({ visible, status, progress, message, description, isMandatory, error, showActions, canSkip,
|
|
41
|
+
function OTAUpdateModal({ visible, status, progress, message, description, isMandatory, error, showActions, canSkip, updateType, appVersion, buildNumber, currentAppVersion, currentBuildNumber, currentLabel, label, totalBytes, downloadedBytes, title, fontFamily, messages, onUpdate, onSkip, onDismiss, }) {
|
|
42
42
|
const copy = (0, react_1.useMemo)(() => ({ ...sync_1.defaultMessages, ...messages }), [messages]);
|
|
43
|
+
const resolvedTitle = (0, react_1.useMemo)(() => {
|
|
44
|
+
if (title)
|
|
45
|
+
return title;
|
|
46
|
+
if (updateType === 'apk')
|
|
47
|
+
return copy.titleApk ?? copy.title;
|
|
48
|
+
if (updateType === 'bundle')
|
|
49
|
+
return copy.titleBundle ?? copy.title;
|
|
50
|
+
return copy.title;
|
|
51
|
+
}, [title, updateType, copy]);
|
|
43
52
|
const customFontStyle = (0, react_1.useMemo)(() => fontFamily
|
|
44
53
|
? {
|
|
45
54
|
fontFamily,
|
|
@@ -60,19 +69,58 @@ function OTAUpdateModal({ visible, status, progress, message, description, isMan
|
|
|
60
69
|
const showSpinner = status === types_1.SyncStatus.RESTARTING ||
|
|
61
70
|
status === types_1.SyncStatus.INSTALLING ||
|
|
62
71
|
(!showProgressBar && !isPrompt && !isDeferredDone && !isError);
|
|
72
|
+
const typeLabel = updateType === 'apk'
|
|
73
|
+
? copy.versionApk
|
|
74
|
+
: updateType === 'bundle'
|
|
75
|
+
? copy.versionBundle
|
|
76
|
+
: null;
|
|
77
|
+
const currentVersionText = updateType === 'apk'
|
|
78
|
+
? [currentAppVersion, currentBuildNumber ? `(${currentBuildNumber})` : null]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(' ')
|
|
81
|
+
: currentLabel || currentAppVersion || null;
|
|
82
|
+
const nextVersionText = updateType === 'apk'
|
|
83
|
+
? [appVersion, buildNumber != null ? `(${buildNumber})` : null]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join(' ')
|
|
86
|
+
: label || appVersion || null;
|
|
87
|
+
const handleUpdate = () => (onUpdate ? onUpdate() : (0, sync_1.respondToUpdatePrompt)('update'));
|
|
88
|
+
const handleSkip = () => (onSkip ? onSkip() : (0, sync_1.respondToUpdatePrompt)('skip'));
|
|
89
|
+
const handleDismiss = () => onDismiss ? onDismiss() : (0, sync_1.respondToUpdatePrompt)('dismiss');
|
|
90
|
+
const downloaded = downloadedBytes ?? 0;
|
|
91
|
+
const total = totalBytes ?? 0;
|
|
92
|
+
const showSizes = showProgressBar && (total > 0 || downloaded > 0);
|
|
63
93
|
return (<react_native_1.Modal visible transparent animationType="fade" statusBarTranslucent onRequestClose={() => {
|
|
64
94
|
if (canSkip)
|
|
65
|
-
(
|
|
95
|
+
handleSkip();
|
|
66
96
|
else if (showActions && (isDeferredDone || isError)) {
|
|
67
|
-
(
|
|
97
|
+
handleDismiss();
|
|
68
98
|
}
|
|
69
99
|
}}>
|
|
70
100
|
<react_native_1.View style={styles.overlay}>
|
|
71
101
|
<react_native_1.View style={styles.card}>
|
|
72
102
|
<react_native_1.Text style={[styles.title, !fontFamily && styles.titleBold, textStyle]}>
|
|
73
|
-
{
|
|
103
|
+
{resolvedTitle}
|
|
74
104
|
</react_native_1.Text>
|
|
75
105
|
|
|
106
|
+
{typeLabel ? (<react_native_1.View style={styles.badge}>
|
|
107
|
+
<react_native_1.Text style={[styles.badgeText, textStyle]}>{typeLabel}</react_native_1.Text>
|
|
108
|
+
</react_native_1.View>) : null}
|
|
109
|
+
|
|
110
|
+
{(currentVersionText || nextVersionText) && !error ? (<react_native_1.View style={styles.versionBox}>
|
|
111
|
+
{currentVersionText ? (<react_native_1.Text style={[styles.versionLine, textStyle]}>
|
|
112
|
+
فعلی: {currentVersionText}
|
|
113
|
+
</react_native_1.Text>) : null}
|
|
114
|
+
{nextVersionText ? (<react_native_1.Text style={[
|
|
115
|
+
styles.versionLine,
|
|
116
|
+
styles.versionNext,
|
|
117
|
+
!fontFamily && styles.titleBold,
|
|
118
|
+
textStyle,
|
|
119
|
+
]}>
|
|
120
|
+
جدید: {nextVersionText}
|
|
121
|
+
</react_native_1.Text>) : null}
|
|
122
|
+
</react_native_1.View>) : null}
|
|
123
|
+
|
|
76
124
|
<react_native_1.Text style={[styles.message, textStyle]}>{error ?? message}</react_native_1.Text>
|
|
77
125
|
|
|
78
126
|
{isMandatory && isPrompt ? (<react_native_1.Text style={[styles.mandatory, textStyle]}>{copy.mandatoryHint}</react_native_1.Text>) : null}
|
|
@@ -93,13 +141,20 @@ function OTAUpdateModal({ visible, status, progress, message, description, isMan
|
|
|
93
141
|
<react_native_1.Text style={[styles.percent, !fontFamily && styles.percentBold, textStyle]}>
|
|
94
142
|
{progressPercent}٪
|
|
95
143
|
</react_native_1.Text>
|
|
144
|
+
{showSizes ? (<react_native_1.Text style={[styles.sizeText, textStyle]}>
|
|
145
|
+
{(0, sync_1.formatBytes)(downloaded)} / {(0, sync_1.formatBytes)(total > 0 ? total : undefined)}
|
|
146
|
+
</react_native_1.Text>) : null}
|
|
96
147
|
</>) : null}
|
|
97
148
|
|
|
149
|
+
{!showProgressBar && total > 0 && (isPrompt || isError) ? (<react_native_1.Text style={[styles.sizeText, textStyle]}>
|
|
150
|
+
حجم دانلود: {(0, sync_1.formatBytes)(total)}
|
|
151
|
+
</react_native_1.Text>) : null}
|
|
152
|
+
|
|
98
153
|
{showSpinner ? (<react_native_1.ActivityIndicator size="large" color="#2563eb" style={styles.spinner}/>) : null}
|
|
99
154
|
|
|
100
155
|
{showActions ? (<react_native_1.View style={styles.actions}>
|
|
101
156
|
{isPrompt ? (<>
|
|
102
|
-
<react_native_1.Pressable style={[styles.button, styles.buttonPrimary]} onPress={
|
|
157
|
+
<react_native_1.Pressable style={[styles.button, styles.buttonPrimary]} onPress={handleUpdate}>
|
|
103
158
|
<react_native_1.Text style={[
|
|
104
159
|
styles.buttonPrimaryText,
|
|
105
160
|
!fontFamily && styles.titleBold,
|
|
@@ -108,10 +163,10 @@ function OTAUpdateModal({ visible, status, progress, message, description, isMan
|
|
|
108
163
|
{copy.updateNow}
|
|
109
164
|
</react_native_1.Text>
|
|
110
165
|
</react_native_1.Pressable>
|
|
111
|
-
{canSkip ? (<react_native_1.Pressable style={[styles.button, styles.buttonGhost]} onPress={
|
|
166
|
+
{canSkip ? (<react_native_1.Pressable style={[styles.button, styles.buttonGhost]} onPress={handleSkip}>
|
|
112
167
|
<react_native_1.Text style={[styles.buttonGhostText, textStyle]}>{copy.skip}</react_native_1.Text>
|
|
113
168
|
</react_native_1.Pressable>) : null}
|
|
114
|
-
</>) : (<react_native_1.Pressable style={[styles.button, styles.buttonPrimary]} onPress={
|
|
169
|
+
</>) : (<react_native_1.Pressable style={[styles.button, styles.buttonPrimary]} onPress={handleDismiss}>
|
|
115
170
|
<react_native_1.Text style={[
|
|
116
171
|
styles.buttonPrimaryText,
|
|
117
172
|
!fontFamily && styles.titleBold,
|
|
@@ -150,6 +205,35 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
150
205
|
titleBold: {
|
|
151
206
|
fontWeight: '700',
|
|
152
207
|
},
|
|
208
|
+
badge: {
|
|
209
|
+
alignSelf: 'center',
|
|
210
|
+
backgroundColor: '#eff6ff',
|
|
211
|
+
borderRadius: 999,
|
|
212
|
+
paddingHorizontal: 10,
|
|
213
|
+
paddingVertical: 4,
|
|
214
|
+
marginBottom: 8,
|
|
215
|
+
},
|
|
216
|
+
badgeText: {
|
|
217
|
+
fontSize: 12,
|
|
218
|
+
color: '#1d4ed8',
|
|
219
|
+
textAlign: 'center',
|
|
220
|
+
},
|
|
221
|
+
versionBox: {
|
|
222
|
+
backgroundColor: '#f9fafb',
|
|
223
|
+
borderRadius: 10,
|
|
224
|
+
paddingVertical: 8,
|
|
225
|
+
paddingHorizontal: 12,
|
|
226
|
+
marginBottom: 8,
|
|
227
|
+
gap: 4,
|
|
228
|
+
},
|
|
229
|
+
versionLine: {
|
|
230
|
+
fontSize: 13,
|
|
231
|
+
color: '#6b7280',
|
|
232
|
+
textAlign: 'center',
|
|
233
|
+
},
|
|
234
|
+
versionNext: {
|
|
235
|
+
color: '#111827',
|
|
236
|
+
},
|
|
153
237
|
message: {
|
|
154
238
|
fontSize: 15,
|
|
155
239
|
color: '#374151',
|
|
@@ -211,6 +295,12 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
211
295
|
percentBold: {
|
|
212
296
|
fontWeight: '600',
|
|
213
297
|
},
|
|
298
|
+
sizeText: {
|
|
299
|
+
marginTop: 4,
|
|
300
|
+
fontSize: 12,
|
|
301
|
+
color: '#9ca3af',
|
|
302
|
+
textAlign: 'center',
|
|
303
|
+
},
|
|
214
304
|
spinner: {
|
|
215
305
|
marginTop: 12,
|
|
216
306
|
alignSelf: 'center',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { OTAUpdateProviderProps } from '../types';
|
|
3
|
-
export declare function OTAUpdateProvider({ children, projectConfig, autoSync, skipInDev, messages, fontFamily, syncOptions, }: OTAUpdateProviderProps): React.JSX.Element;
|
|
3
|
+
export declare function OTAUpdateProvider({ children, projectConfig, autoSync, skipInDev, messages, fontFamily, syncOptions, renderUpdateUI, UpdateComponent, }: OTAUpdateProviderProps): React.JSX.Element;
|
|
@@ -43,12 +43,12 @@ const telemetry_1 = require("../telemetry");
|
|
|
43
43
|
const OTAUpdateModal_1 = require("./OTAUpdateModal");
|
|
44
44
|
const SYNC_DELAY_MS = 2500;
|
|
45
45
|
const FOREGROUND_SYNC_DEBOUNCE_MS = 30000;
|
|
46
|
-
function OTAUpdateProvider({ children, projectConfig, autoSync = true, skipInDev = true, messages, fontFamily, syncOptions, }) {
|
|
46
|
+
function OTAUpdateProvider({ children, projectConfig, autoSync = true, skipInDev = true, messages, fontFamily, syncOptions, renderUpdateUI, UpdateComponent, }) {
|
|
47
47
|
const [progress, setProgress] = (0, react_1.useState)((0, progress_1.getProgressState)());
|
|
48
48
|
const lastSyncAtRef = (0, react_1.useRef)(0);
|
|
49
49
|
const foregroundTimerRef = (0, react_1.useRef)(null);
|
|
50
50
|
const hasMountedSyncRef = (0, react_1.useRef)(false);
|
|
51
|
-
const modalTitle = (0, progress_1.getModalTitle)(messages);
|
|
51
|
+
const modalTitle = (0, progress_1.getModalTitle)(messages, progress.updateType);
|
|
52
52
|
(0, react_1.useEffect)(() => {
|
|
53
53
|
if (projectConfig) {
|
|
54
54
|
(0, projectConfig_1.configureFromProject)(projectConfig);
|
|
@@ -101,8 +101,27 @@ function OTAUpdateProvider({ children, projectConfig, autoSync = true, skipInDev
|
|
|
101
101
|
};
|
|
102
102
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
103
|
}, []);
|
|
104
|
+
const uiProps = (0, react_1.useMemo)(() => ({
|
|
105
|
+
...progress,
|
|
106
|
+
title: modalTitle,
|
|
107
|
+
fontFamily,
|
|
108
|
+
messages,
|
|
109
|
+
onUpdate: () => (0, sync_1.respondToUpdatePrompt)('update'),
|
|
110
|
+
onSkip: () => (0, sync_1.respondToUpdatePrompt)('skip'),
|
|
111
|
+
onDismiss: () => (0, sync_1.respondToUpdatePrompt)('dismiss'),
|
|
112
|
+
}), [progress, modalTitle, fontFamily, messages]);
|
|
113
|
+
let updateUI = null;
|
|
114
|
+
if (renderUpdateUI) {
|
|
115
|
+
updateUI = renderUpdateUI(uiProps);
|
|
116
|
+
}
|
|
117
|
+
else if (UpdateComponent) {
|
|
118
|
+
updateUI = <UpdateComponent {...uiProps}/>;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
updateUI = <OTAUpdateModal_1.OTAUpdateModal {...uiProps}/>;
|
|
122
|
+
}
|
|
104
123
|
return (<>
|
|
105
124
|
{children}
|
|
106
|
-
|
|
125
|
+
{updateUI}
|
|
107
126
|
</>);
|
|
108
127
|
}
|
package/lib/zipExtract.js
CHANGED
|
@@ -88,6 +88,12 @@ async function extractZipToDirectoryJs(zipPath, destDir) {
|
|
|
88
88
|
await mkdirRecursive(`${destDir}/${normalized.slice(0, -1)}`);
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
|
+
const base = normalized.split('/').pop() ?? normalized;
|
|
92
|
+
if (normalized.split('/').includes('__MACOSX') ||
|
|
93
|
+
base.startsWith('._') ||
|
|
94
|
+
base === '.DS_Store') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
91
97
|
const content = rawContent instanceof Uint8Array
|
|
92
98
|
? rawContent
|
|
93
99
|
: new Uint8Array(rawContent);
|