@appsonair.ir/react-native 1.0.10 → 1.0.12
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.
|
@@ -5,47 +5,35 @@ import android.content.Intent
|
|
|
5
5
|
import android.net.Uri
|
|
6
6
|
import android.os.Build
|
|
7
7
|
import android.provider.Settings
|
|
8
|
+
import android.util.Log
|
|
8
9
|
import androidx.core.content.FileProvider
|
|
9
10
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import java.io.BufferedInputStream
|
|
10
12
|
import java.io.File
|
|
13
|
+
import java.io.FileOutputStream
|
|
14
|
+
import java.util.zip.ZipFile
|
|
11
15
|
|
|
12
16
|
object OTAApkInstallHelper {
|
|
17
|
+
private const val TAG = "OTAApkInstall"
|
|
13
18
|
private const val MIN_APK_BYTES = 1024L * 100
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* @return "started" when the system install screen opens, "permission_required" when settings were opened
|
|
17
22
|
*/
|
|
18
23
|
fun install(reactContext: ReactApplicationContext, path: String): String {
|
|
19
|
-
val
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
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
|
-
}
|
|
24
|
+
val prepared = prepareInstallableApk(File(normalizePath(path)))
|
|
25
|
+
return installPrepared(reactContext, prepared)
|
|
26
|
+
}
|
|
40
27
|
|
|
28
|
+
fun installPrepared(reactContext: ReactApplicationContext, prepared: File): String {
|
|
41
29
|
if (needsInstallPermission(reactContext) &&
|
|
42
30
|
!reactContext.packageManager.canRequestPackageInstalls()) {
|
|
43
|
-
OTAUpdaterStorage.setPendingApkPath(reactContext,
|
|
31
|
+
OTAUpdaterStorage.setPendingApkPath(reactContext, prepared.absolutePath)
|
|
44
32
|
openUnknownSourcesSettings(reactContext)
|
|
45
33
|
return "permission_required"
|
|
46
34
|
}
|
|
47
35
|
|
|
48
|
-
launchInstallIntent(reactContext,
|
|
36
|
+
launchInstallIntent(reactContext, prepared)
|
|
49
37
|
OTAUpdaterStorage.clearPendingApkPath(reactContext)
|
|
50
38
|
return "started"
|
|
51
39
|
}
|
|
@@ -59,14 +47,129 @@ object OTAApkInstallHelper {
|
|
|
59
47
|
return
|
|
60
48
|
}
|
|
61
49
|
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
try {
|
|
51
|
+
val prepared = prepareInstallableApk(File(path))
|
|
52
|
+
launchInstallIntent(reactContext, prepared)
|
|
53
|
+
OTAUpdaterStorage.clearPendingApkPath(reactContext)
|
|
54
|
+
} catch (e: Exception) {
|
|
55
|
+
Log.e(TAG, "Failed to resume APK install", e)
|
|
64
56
|
OTAUpdaterStorage.clearPendingApkPath(reactContext)
|
|
65
|
-
return
|
|
66
57
|
}
|
|
58
|
+
}
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Ensures [file] is a real APK (has AndroidManifest.xml).
|
|
62
|
+
* If it is an outer zip that contains an .apk entry, extracts that APK first.
|
|
63
|
+
*/
|
|
64
|
+
fun prepareInstallableApk(file: File): File {
|
|
65
|
+
if (!file.exists() || file.length() < MIN_APK_BYTES) {
|
|
66
|
+
throw IllegalArgumentException("APK file is missing or invalid: ${file.absolutePath}")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isAndroidApk(file)) {
|
|
70
|
+
return file
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
val unwrapped = unwrapApkFromZip(file)
|
|
74
|
+
if (unwrapped != null && isAndroidApk(unwrapped)) {
|
|
75
|
+
Log.i(TAG, "Unwrapped nested APK from zip: ${unwrapped.absolutePath}")
|
|
76
|
+
return unwrapped
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw IllegalArgumentException(
|
|
80
|
+
"Wrong package: file is not a valid Android APK (${file.name}). " +
|
|
81
|
+
"Expected an APK or a zip that contains one.",
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun isAndroidApk(file: File): Boolean = hasAndroidManifest(file)
|
|
86
|
+
|
|
87
|
+
private fun hasAndroidManifest(file: File): Boolean {
|
|
88
|
+
return try {
|
|
89
|
+
ZipFile(file).use { zip ->
|
|
90
|
+
val entries = zip.entries()
|
|
91
|
+
while (entries.hasMoreElements()) {
|
|
92
|
+
val name = entries.nextElement().name.replace('\\', '/').lowercase()
|
|
93
|
+
if (name == "androidmanifest.xml" || name.endsWith("/androidmanifest.xml")) {
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
false
|
|
98
|
+
}
|
|
99
|
+
} catch (e: Exception) {
|
|
100
|
+
Log.w(TAG, "Not a readable zip/apk: ${file.absolutePath}", e)
|
|
101
|
+
false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun unwrapApkFromZip(zipFile: File): File? {
|
|
106
|
+
return try {
|
|
107
|
+
ZipFile(zipFile).use { zip ->
|
|
108
|
+
val apkEntries = mutableListOf<java.util.zip.ZipEntry>()
|
|
109
|
+
val entries = zip.entries()
|
|
110
|
+
while (entries.hasMoreElements()) {
|
|
111
|
+
val entry = entries.nextElement()
|
|
112
|
+
if (entry.isDirectory) continue
|
|
113
|
+
val name = entry.name.replace('\\', '/')
|
|
114
|
+
val base = name.substringAfterLast('/')
|
|
115
|
+
if (
|
|
116
|
+
name.split('/').contains("__MACOSX") ||
|
|
117
|
+
base.startsWith("._") ||
|
|
118
|
+
base == ".DS_Store"
|
|
119
|
+
) {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (base.endsWith(".apk", ignoreCase = true)) {
|
|
123
|
+
apkEntries.add(entry)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (apkEntries.isEmpty()) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
val entry =
|
|
132
|
+
apkEntries.maxByOrNull { scoreApkEntry(it.name) }
|
|
133
|
+
?: return null
|
|
134
|
+
|
|
135
|
+
val out =
|
|
136
|
+
File(
|
|
137
|
+
zipFile.parentFile,
|
|
138
|
+
"${zipFile.nameWithoutExtension}-unwrapped.apk",
|
|
139
|
+
)
|
|
140
|
+
if (out.exists()) {
|
|
141
|
+
out.delete()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
zip.getInputStream(entry).use { input ->
|
|
145
|
+
BufferedInputStream(input).use { buffered ->
|
|
146
|
+
FileOutputStream(out).use { output ->
|
|
147
|
+
buffered.copyTo(output)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (out.length() < MIN_APK_BYTES) {
|
|
153
|
+
out.delete()
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
out
|
|
158
|
+
}
|
|
159
|
+
} catch (e: Exception) {
|
|
160
|
+
Log.e(TAG, "Failed to unwrap APK from ${zipFile.absolutePath}", e)
|
|
161
|
+
null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private fun scoreApkEntry(name: String): Int {
|
|
166
|
+
val lower = name.lowercase()
|
|
167
|
+
return when {
|
|
168
|
+
lower.contains("arm64-v8a") || lower.contains("arm64") || lower.contains("-v8a") -> 3
|
|
169
|
+
lower.contains("armeabi-v7a") || lower.contains("armeabi") || lower.contains("-v7a") -> 2
|
|
170
|
+
lower.contains("universal") -> 1
|
|
171
|
+
else -> 0
|
|
172
|
+
}
|
|
70
173
|
}
|
|
71
174
|
|
|
72
175
|
private fun needsInstallPermission(context: Context): Boolean =
|
|
@@ -108,14 +108,33 @@ class OTAUpdaterModule(private val reactContext: ReactApplicationContext) :
|
|
|
108
108
|
|
|
109
109
|
@ReactMethod
|
|
110
110
|
fun installApk(path: String, promise: Promise) {
|
|
111
|
-
|
|
111
|
+
Thread {
|
|
112
112
|
try {
|
|
113
|
-
val
|
|
114
|
-
|
|
113
|
+
val prepared = OTAApkInstallHelper.prepareInstallableApk(File(path.removePrefix("file://")))
|
|
114
|
+
UiThreadUtil.runOnUiThread {
|
|
115
|
+
try {
|
|
116
|
+
val result = OTAApkInstallHelper.installPrepared(reactContext, prepared)
|
|
117
|
+
promise.resolve(result)
|
|
118
|
+
} catch (e: Exception) {
|
|
119
|
+
promise.reject("EINSTALL", e.message, e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
115
122
|
} catch (e: Exception) {
|
|
116
123
|
promise.reject("EINSTALL", e.message, e)
|
|
117
124
|
}
|
|
118
|
-
}
|
|
125
|
+
}.start()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@ReactMethod
|
|
129
|
+
fun isAndroidApkPackage(path: String, promise: Promise) {
|
|
130
|
+
Thread {
|
|
131
|
+
try {
|
|
132
|
+
val file = File(path.removePrefix("file://"))
|
|
133
|
+
promise.resolve(OTAApkInstallHelper.isAndroidApk(file))
|
|
134
|
+
} catch (e: Exception) {
|
|
135
|
+
promise.reject("EAPK", e.message, e)
|
|
136
|
+
}
|
|
137
|
+
}.start()
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
@ReactMethod
|
package/lib/sync.js
CHANGED
|
@@ -427,12 +427,27 @@ async function isZipArchive(filePath) {
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
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
430
|
if (packageFormat === 'zip') {
|
|
433
431
|
return true;
|
|
434
432
|
}
|
|
435
|
-
|
|
433
|
+
if (downloadedPath.toLowerCase().endsWith('.zip')) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
// Format mismatch: zip-of-apk was saved as `.apk`. Real APKs are also zip
|
|
437
|
+
// containers, so only extract when AndroidManifest.xml is missing.
|
|
438
|
+
if (!(await isZipArchive(downloadedPath))) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (OTAUpdaterNative?.isAndroidApkPackage) {
|
|
442
|
+
try {
|
|
443
|
+
const isApk = await OTAUpdaterNative.isAndroidApkPackage(downloadedPath);
|
|
444
|
+
return !isApk;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
436
451
|
}
|
|
437
452
|
async function shouldExtractZipPackage(downloadedPath, packageFormat) {
|
|
438
453
|
if (packageFormat === 'zip') {
|
|
@@ -580,16 +595,25 @@ async function extractApkUpdate(label, zipPath, deviceAbi) {
|
|
|
580
595
|
if (!extractedApk.toLowerCase().endsWith('.apk')) {
|
|
581
596
|
throw new Error('Zip did not contain a valid APK');
|
|
582
597
|
}
|
|
598
|
+
const extractedStat = await ReactNativeBlobUtil.fs.stat(extractedApk);
|
|
599
|
+
if (!extractedStat || extractedStat.size < 1024 * 100) {
|
|
600
|
+
throw new Error(`Extracted APK is too small (${extractedStat?.size ?? 0} bytes)`);
|
|
601
|
+
}
|
|
583
602
|
// Never hand the zip (or a nested extract path) to the installer.
|
|
584
603
|
if (extractedApk !== finalApkPath) {
|
|
585
604
|
const finalExists = await ReactNativeBlobUtil.fs.exists(finalApkPath);
|
|
586
605
|
if (finalExists) {
|
|
587
606
|
await ReactNativeBlobUtil.fs.unlink(finalApkPath);
|
|
588
607
|
}
|
|
589
|
-
|
|
608
|
+
try {
|
|
609
|
+
await ReactNativeBlobUtil.fs.mv(extractedApk, finalApkPath);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
await ReactNativeBlobUtil.fs.cp(extractedApk, finalApkPath);
|
|
613
|
+
}
|
|
590
614
|
}
|
|
591
615
|
const zipExists = await ReactNativeBlobUtil.fs.exists(zipPath);
|
|
592
|
-
if (zipExists) {
|
|
616
|
+
if (zipExists && zipPath !== finalApkPath) {
|
|
593
617
|
await ReactNativeBlobUtil.fs.unlink(zipPath);
|
|
594
618
|
}
|
|
595
619
|
// Drop extract clutter; keep only the installable APK.
|
|
@@ -598,6 +622,10 @@ async function extractApkUpdate(label, zipPath, deviceAbi) {
|
|
|
598
622
|
if (!apkExists) {
|
|
599
623
|
throw new Error('Failed to prepare APK for install');
|
|
600
624
|
}
|
|
625
|
+
const finalStat = await ReactNativeBlobUtil.fs.stat(finalApkPath);
|
|
626
|
+
if (!finalStat || finalStat.size < 1024 * 100) {
|
|
627
|
+
throw new Error(`Prepared APK is too small (${finalStat?.size ?? 0} bytes)`);
|
|
628
|
+
}
|
|
601
629
|
return finalApkPath;
|
|
602
630
|
}
|
|
603
631
|
function getBundleDirectory() {
|
|
@@ -784,15 +812,29 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
|
|
|
784
812
|
'X-Deployment-Key': deploymentKey,
|
|
785
813
|
...(deviceAbi ? { 'X-Device-Abi': deviceAbi } : {}),
|
|
786
814
|
});
|
|
815
|
+
let lastReportedProgress = 0;
|
|
816
|
+
let lastReportedReceived = 0;
|
|
817
|
+
let lastReportedTotal = totalBytes > 0 ? totalBytes : 0;
|
|
787
818
|
const reportDownloadProgress = (received, total) => {
|
|
788
819
|
if (!onProgress)
|
|
789
820
|
return;
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
821
|
+
const safeReceived = Math.max(Number(received) || 0, lastReportedReceived);
|
|
822
|
+
let resolvedTotal = total > 0 ? total : totalBytes;
|
|
823
|
+
if (!(resolvedTotal > 0)) {
|
|
824
|
+
resolvedTotal = lastReportedTotal;
|
|
825
|
+
}
|
|
826
|
+
if (resolvedTotal > lastReportedTotal) {
|
|
827
|
+
lastReportedTotal = resolvedTotal;
|
|
828
|
+
}
|
|
829
|
+
// Keep bytes monotonic even when total is still unknown.
|
|
830
|
+
lastReportedReceived = safeReceived;
|
|
831
|
+
if (!(resolvedTotal > 0)) {
|
|
832
|
+
onProgress(lastReportedProgress, safeReceived, 0);
|
|
793
833
|
return;
|
|
794
834
|
}
|
|
795
|
-
|
|
835
|
+
const nextProgress = Math.min(Math.max(safeReceived / resolvedTotal, lastReportedProgress), 0.99);
|
|
836
|
+
lastReportedProgress = nextProgress;
|
|
837
|
+
onProgress(nextProgress, safeReceived, resolvedTotal);
|
|
796
838
|
};
|
|
797
839
|
let pollTimer = null;
|
|
798
840
|
if (onProgress && totalBytes > 0) {
|
package/lib/ui/OTAUpdateModal.js
CHANGED
|
@@ -38,6 +38,25 @@ 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
|
+
/** Never returns "—" so download size labels don't flicker between ticks. */
|
|
42
|
+
function stableFormatBytes(bytes) {
|
|
43
|
+
if (bytes == null || !Number.isFinite(bytes) || bytes < 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (bytes < 1024) {
|
|
47
|
+
return `${Math.round(bytes)} B`;
|
|
48
|
+
}
|
|
49
|
+
if (bytes < 1024 * 1024) {
|
|
50
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
51
|
+
}
|
|
52
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
53
|
+
}
|
|
54
|
+
function joinVersionParts(parts) {
|
|
55
|
+
const filtered = parts
|
|
56
|
+
.map((part) => (part == null ? '' : String(part).trim()))
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
return filtered.length > 0 ? filtered.join(' ') : null;
|
|
59
|
+
}
|
|
41
60
|
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
61
|
const copy = (0, react_1.useMemo)(() => ({ ...sync_1.defaultMessages, ...messages }), [messages]);
|
|
43
62
|
const resolvedTitle = (0, react_1.useMemo)(() => {
|
|
@@ -59,13 +78,52 @@ function OTAUpdateModal({ visible, status, progress, message, description, isMan
|
|
|
59
78
|
...(customFontStyle ?? null),
|
|
60
79
|
writingDirection: 'rtl',
|
|
61
80
|
}), [customFontStyle]);
|
|
81
|
+
const isDownloading = status === types_1.SyncStatus.DOWNLOADING;
|
|
82
|
+
const stableProgressRef = (0, react_1.useRef)({ progress: 0, downloaded: 0, total: 0 });
|
|
83
|
+
(0, react_1.useEffect)(() => {
|
|
84
|
+
if (!isDownloading) {
|
|
85
|
+
stableProgressRef.current = { progress: 0, downloaded: 0, total: 0 };
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const nextProgress = Math.min(Math.max(Number(progress) || 0, 0), 1);
|
|
89
|
+
const nextDownloaded = Number(downloadedBytes);
|
|
90
|
+
const nextTotal = Number(totalBytes);
|
|
91
|
+
if (nextProgress >= stableProgressRef.current.progress) {
|
|
92
|
+
stableProgressRef.current.progress = nextProgress;
|
|
93
|
+
}
|
|
94
|
+
if (Number.isFinite(nextTotal) && nextTotal > stableProgressRef.current.total) {
|
|
95
|
+
stableProgressRef.current.total = nextTotal;
|
|
96
|
+
}
|
|
97
|
+
if (Number.isFinite(nextDownloaded) &&
|
|
98
|
+
nextDownloaded >= stableProgressRef.current.downloaded) {
|
|
99
|
+
stableProgressRef.current.downloaded = nextDownloaded;
|
|
100
|
+
}
|
|
101
|
+
}, [isDownloading, progress, downloadedBytes, totalBytes]);
|
|
62
102
|
if (!visible)
|
|
63
103
|
return null;
|
|
64
|
-
|
|
104
|
+
if (isDownloading) {
|
|
105
|
+
const nextProgress = Math.min(Math.max(Number(progress) || 0, 0), 1);
|
|
106
|
+
const nextDownloaded = Number(downloadedBytes);
|
|
107
|
+
const nextTotal = Number(totalBytes);
|
|
108
|
+
if (nextProgress >= stableProgressRef.current.progress) {
|
|
109
|
+
stableProgressRef.current.progress = nextProgress;
|
|
110
|
+
}
|
|
111
|
+
if (Number.isFinite(nextTotal) && nextTotal > stableProgressRef.current.total) {
|
|
112
|
+
stableProgressRef.current.total = nextTotal;
|
|
113
|
+
}
|
|
114
|
+
if (Number.isFinite(nextDownloaded) &&
|
|
115
|
+
nextDownloaded >= stableProgressRef.current.downloaded) {
|
|
116
|
+
stableProgressRef.current.downloaded = nextDownloaded;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const stableProgress = isDownloading
|
|
120
|
+
? stableProgressRef.current.progress
|
|
121
|
+
: Math.min(Math.max(Number(progress) || 0, 0), 1);
|
|
122
|
+
const progressPercent = Math.round(stableProgress * 100);
|
|
65
123
|
const isPrompt = status === types_1.SyncStatus.UPDATE_AVAILABLE;
|
|
66
124
|
const isDeferredDone = status === types_1.SyncStatus.UPDATE_INSTALLED;
|
|
67
125
|
const isError = status === types_1.SyncStatus.ERROR;
|
|
68
|
-
const showProgressBar =
|
|
126
|
+
const showProgressBar = isDownloading;
|
|
69
127
|
const showSpinner = status === types_1.SyncStatus.RESTARTING ||
|
|
70
128
|
status === types_1.SyncStatus.INSTALLING ||
|
|
71
129
|
(!showProgressBar && !isPrompt && !isDeferredDone && !isError);
|
|
@@ -74,22 +132,42 @@ function OTAUpdateModal({ visible, status, progress, message, description, isMan
|
|
|
74
132
|
: updateType === 'bundle'
|
|
75
133
|
? copy.versionBundle
|
|
76
134
|
: null;
|
|
135
|
+
// Show both version number and release label/name when available.
|
|
77
136
|
const currentVersionText = updateType === 'apk'
|
|
78
|
-
? [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
137
|
+
? joinVersionParts([
|
|
138
|
+
currentAppVersion,
|
|
139
|
+
currentBuildNumber ? `(${currentBuildNumber})` : null,
|
|
140
|
+
])
|
|
141
|
+
: joinVersionParts([
|
|
142
|
+
currentAppVersion,
|
|
143
|
+
currentLabel && currentLabel !== currentAppVersion ? currentLabel : null,
|
|
144
|
+
]);
|
|
82
145
|
const nextVersionText = updateType === 'apk'
|
|
83
|
-
? [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
146
|
+
? joinVersionParts([
|
|
147
|
+
appVersion,
|
|
148
|
+
buildNumber != null ? `(${buildNumber})` : null,
|
|
149
|
+
label && label !== appVersion ? label : null,
|
|
150
|
+
])
|
|
151
|
+
: joinVersionParts([
|
|
152
|
+
appVersion,
|
|
153
|
+
label && label !== appVersion ? label : null,
|
|
154
|
+
]);
|
|
87
155
|
const handleUpdate = () => (onUpdate ? onUpdate() : (0, sync_1.respondToUpdatePrompt)('update'));
|
|
88
156
|
const handleSkip = () => (onSkip ? onSkip() : (0, sync_1.respondToUpdatePrompt)('skip'));
|
|
89
157
|
const handleDismiss = () => onDismiss ? onDismiss() : (0, sync_1.respondToUpdatePrompt)('dismiss');
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
158
|
+
const stableDownloaded = showProgressBar
|
|
159
|
+
? stableProgressRef.current.downloaded
|
|
160
|
+
: downloadedBytes ?? 0;
|
|
161
|
+
const stableTotal = showProgressBar
|
|
162
|
+
? stableProgressRef.current.total
|
|
163
|
+
: totalBytes ?? 0;
|
|
164
|
+
const downloadedLabel = stableFormatBytes(stableDownloaded) || '0.0 MB';
|
|
165
|
+
const totalLabel = stableFormatBytes(stableTotal);
|
|
166
|
+
const sizeLabel = totalLabel
|
|
167
|
+
? `${downloadedLabel} / ${totalLabel}`
|
|
168
|
+
: downloadedLabel;
|
|
169
|
+
const showSizes = showProgressBar;
|
|
170
|
+
const showPromptSize = !showProgressBar && stableTotal > 0 && (isPrompt || isError);
|
|
93
171
|
return (<react_native_1.Modal visible transparent animationType="fade" statusBarTranslucent onRequestClose={() => {
|
|
94
172
|
if (canSkip)
|
|
95
173
|
handleSkip();
|
|
@@ -134,20 +212,25 @@ function OTAUpdateModal({ visible, status, progress, message, description, isMan
|
|
|
134
212
|
</react_native_1.ScrollView>
|
|
135
213
|
</react_native_1.View>) : null}
|
|
136
214
|
|
|
137
|
-
{showProgressBar ? (
|
|
215
|
+
{showProgressBar ? (<react_native_1.View style={styles.progressBlock}>
|
|
138
216
|
<react_native_1.View style={styles.progressTrack}>
|
|
139
|
-
<react_native_1.View style={[
|
|
217
|
+
<react_native_1.View style={[
|
|
218
|
+
styles.progressFill,
|
|
219
|
+
{ width: `${Math.max(progressPercent, 2)}%` },
|
|
220
|
+
]}/>
|
|
140
221
|
</react_native_1.View>
|
|
141
|
-
<react_native_1.
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
222
|
+
<react_native_1.View style={styles.progressMetaRow}>
|
|
223
|
+
<react_native_1.Text style={[styles.percent, !fontFamily && styles.percentBold]}>
|
|
224
|
+
{progressPercent}٪
|
|
225
|
+
</react_native_1.Text>
|
|
226
|
+
{showSizes ? (<react_native_1.Text style={styles.sizeText} numberOfLines={1}>
|
|
227
|
+
{sizeLabel}
|
|
228
|
+
</react_native_1.Text>) : null}
|
|
229
|
+
</react_native_1.View>
|
|
230
|
+
</react_native_1.View>) : null}
|
|
148
231
|
|
|
149
|
-
{
|
|
150
|
-
حجم دانلود: {(
|
|
232
|
+
{showPromptSize ? (<react_native_1.Text style={[styles.sizeTextCentered, textStyle]}>
|
|
233
|
+
حجم دانلود: {stableFormatBytes(stableTotal)}
|
|
151
234
|
</react_native_1.Text>) : null}
|
|
152
235
|
|
|
153
236
|
{showSpinner ? (<react_native_1.ActivityIndicator size="large" color="#2563eb" style={styles.spinner}/>) : null}
|
|
@@ -273,29 +356,48 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
273
356
|
textAlign: 'center',
|
|
274
357
|
lineHeight: 20,
|
|
275
358
|
},
|
|
359
|
+
progressBlock: {
|
|
360
|
+
width: '100%',
|
|
361
|
+
marginTop: 8,
|
|
362
|
+
},
|
|
276
363
|
progressTrack: {
|
|
277
364
|
width: '100%',
|
|
278
365
|
height: 8,
|
|
279
366
|
backgroundColor: '#e5e7eb',
|
|
280
367
|
borderRadius: 999,
|
|
281
368
|
overflow: 'hidden',
|
|
282
|
-
marginTop: 8,
|
|
283
369
|
},
|
|
284
370
|
progressFill: {
|
|
285
371
|
height: '100%',
|
|
286
372
|
backgroundColor: '#2563eb',
|
|
287
373
|
borderRadius: 999,
|
|
288
374
|
},
|
|
289
|
-
|
|
375
|
+
progressMetaRow: {
|
|
290
376
|
marginTop: 8,
|
|
377
|
+
width: '100%',
|
|
378
|
+
flexDirection: 'row',
|
|
379
|
+
alignItems: 'center',
|
|
380
|
+
justifyContent: 'space-between',
|
|
381
|
+
direction: 'ltr',
|
|
382
|
+
},
|
|
383
|
+
percent: {
|
|
291
384
|
fontSize: 13,
|
|
292
385
|
color: '#6b7280',
|
|
293
|
-
textAlign: '
|
|
386
|
+
textAlign: 'left',
|
|
387
|
+
writingDirection: 'ltr',
|
|
294
388
|
},
|
|
295
389
|
percentBold: {
|
|
296
390
|
fontWeight: '600',
|
|
297
391
|
},
|
|
298
392
|
sizeText: {
|
|
393
|
+
fontSize: 12,
|
|
394
|
+
color: '#9ca3af',
|
|
395
|
+
textAlign: 'right',
|
|
396
|
+
writingDirection: 'ltr',
|
|
397
|
+
flexShrink: 1,
|
|
398
|
+
marginLeft: 8,
|
|
399
|
+
},
|
|
400
|
+
sizeTextCentered: {
|
|
299
401
|
marginTop: 4,
|
|
300
402
|
fontSize: 12,
|
|
301
403
|
color: '#9ca3af',
|