@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 file = File(normalizePath(path))
20
- if (!file.exists() || file.length() < MIN_APK_BYTES) {
21
- throw IllegalArgumentException("APK file is missing or invalid: ${file.absolutePath}")
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, file.absolutePath)
31
+ OTAUpdaterStorage.setPendingApkPath(reactContext, prepared.absolutePath)
44
32
  openUnknownSourcesSettings(reactContext)
45
33
  return "permission_required"
46
34
  }
47
35
 
48
- launchInstallIntent(reactContext, file)
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
- val file = File(path)
63
- if (!file.exists() || file.length() < MIN_APK_BYTES) {
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
- launchInstallIntent(reactContext, file)
69
- OTAUpdaterStorage.clearPendingApkPath(reactContext)
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
- UiThreadUtil.runOnUiThread {
111
+ Thread {
112
112
  try {
113
- val result = OTAApkInstallHelper.install(reactContext, path)
114
- promise.resolve(result)
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
- return downloadedPath.toLowerCase().endsWith('.zip');
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
- await ReactNativeBlobUtil.fs.cp(extractedApk, finalApkPath);
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 resolvedTotal = total > 0 ? total : totalBytes;
791
- if (resolvedTotal <= 0) {
792
- onProgress(0, Math.max(received, 0), 0);
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
- onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99), Math.max(received, 0), resolvedTotal);
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) {
@@ -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
- const progressPercent = Math.round(Math.min(Math.max(progress, 0), 1) * 100);
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 = status === types_1.SyncStatus.DOWNLOADING;
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
- ? [currentAppVersion, currentBuildNumber ? `(${currentBuildNumber})` : null]
79
- .filter(Boolean)
80
- .join(' ')
81
- : currentLabel || currentAppVersion || null;
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
- ? [appVersion, buildNumber != null ? `(${buildNumber})` : null]
84
- .filter(Boolean)
85
- .join(' ')
86
- : label || appVersion || null;
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 downloaded = downloadedBytes ?? 0;
91
- const total = totalBytes ?? 0;
92
- const showSizes = showProgressBar && (total > 0 || downloaded > 0);
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={[styles.progressFill, { width: `${progressPercent}%` }]}/>
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.Text style={[styles.percent, !fontFamily && styles.percentBold, textStyle]}>
142
- {progressPercent}٪
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}
147
- </>) : null}
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
- {!showProgressBar && total > 0 && (isPrompt || isError) ? (<react_native_1.Text style={[styles.sizeText, textStyle]}>
150
- حجم دانلود: {(0, sync_1.formatBytes)(total)}
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
- percent: {
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: 'center',
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsonair.ir/react-native",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "React Native client SDK for self-hosted OTA updates",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",