@appsonair.ir/react-native 1.0.6 → 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.
@@ -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.ZipInputStream
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
- ZipInputStream(FileInputStream(zipFile)).use { zipInput ->
23
- var entry = zipInput.nextEntry
24
- while (entry != null) {
25
- val outFile = safeOutputFile(destDir, entry.name)
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
- } else {
30
- outFile.parentFile?.mkdirs()
31
- FileOutputStream(outFile).use { output ->
32
- zipInput.copyTo(output)
33
- }
41
+ continue
34
42
  }
35
43
 
36
- zipInput.closeEntry()
37
- entry = zipInput.nextEntry
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 outFile = File(destDir, entryName)
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>): string;
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;
@@ -456,6 +489,117 @@ function getApkFilePath(label) {
456
489
  const dirs = ReactNativeBlobUtil.fs.dirs;
457
490
  return `${dirs.DocumentDir}/ota-updates/${label}.apk`;
458
491
  }
492
+ function getApkZipPath(label) {
493
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
494
+ const dirs = ReactNativeBlobUtil.fs.dirs;
495
+ return `${dirs.DocumentDir}/ota-updates/${label}-apk.zip`;
496
+ }
497
+ function getApkExtractDir(label) {
498
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
499
+ const dirs = ReactNativeBlobUtil.fs.dirs;
500
+ return `${dirs.DocumentDir}/ota-updates/${label}-apk`;
501
+ }
502
+ function apkAbiFromName(fileName) {
503
+ const lower = fileName.replace(/^.*[/\\]/, '').toLowerCase();
504
+ if (lower.includes('arm64-v8a') || lower.includes('arm64') || lower.includes('-v8a')) {
505
+ return 'arm64-v8a';
506
+ }
507
+ if (lower.includes('armeabi-v7a') ||
508
+ lower.includes('armeabi') ||
509
+ lower.includes('-v7a')) {
510
+ return 'armeabi-v7a';
511
+ }
512
+ if (lower.includes('x86_64')) {
513
+ return 'x86_64';
514
+ }
515
+ if (lower.includes('universal')) {
516
+ return 'universal';
517
+ }
518
+ return null;
519
+ }
520
+ async function listFilesRecursive(dir) {
521
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
522
+ const entries = await ReactNativeBlobUtil.fs.ls(dir);
523
+ const out = [];
524
+ for (const entry of entries) {
525
+ const path = `${dir}/${entry}`;
526
+ try {
527
+ const entryStat = await ReactNativeBlobUtil.fs.stat(path);
528
+ if (entryStat.type === 'directory') {
529
+ out.push(...(await listFilesRecursive(path)));
530
+ }
531
+ else {
532
+ out.push(path);
533
+ }
534
+ }
535
+ catch {
536
+ out.push(path);
537
+ }
538
+ }
539
+ return out;
540
+ }
541
+ async function pickExtractedApk(dir, deviceAbi) {
542
+ const files = await listFilesRecursive(dir);
543
+ const apks = files.filter((path) => path.toLowerCase().endsWith('.apk'));
544
+ if (apks.length === 0) {
545
+ throw new Error(`No APK found after unzipping ${dir}`);
546
+ }
547
+ if (apks.length === 1) {
548
+ return apks[0];
549
+ }
550
+ const wanted = (deviceAbi ?? (await getDeviceAbi()) ?? '').toLowerCase();
551
+ const scored = apks.map((path) => {
552
+ const abi = apkAbiFromName(path);
553
+ let score = 0;
554
+ if (abi && wanted && abi === wanted) {
555
+ score = 3;
556
+ }
557
+ else if (wanted.includes('arm64') && abi === 'arm64-v8a') {
558
+ score = 3;
559
+ }
560
+ else if ((wanted.includes('v7') || wanted.includes('armeabi')) &&
561
+ abi === 'armeabi-v7a') {
562
+ score = 2;
563
+ }
564
+ else if (abi === 'universal') {
565
+ score = 1;
566
+ }
567
+ return { path, score };
568
+ });
569
+ scored.sort((a, b) => b.score - a.score);
570
+ return scored[0].path;
571
+ }
572
+ async function extractApkUpdate(label, zipPath, deviceAbi) {
573
+ const ReactNativeBlobUtil = require('react-native-blob-util').default;
574
+ const extractDir = getApkExtractDir(label);
575
+ const finalApkPath = getApkFilePath(label);
576
+ await removePathRecursive(extractDir);
577
+ await ReactNativeBlobUtil.fs.mkdir(extractDir);
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
+ }
591
+ const zipExists = await ReactNativeBlobUtil.fs.exists(zipPath);
592
+ if (zipExists) {
593
+ await ReactNativeBlobUtil.fs.unlink(zipPath);
594
+ }
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;
602
+ }
459
603
  function getBundleDirectory() {
460
604
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
461
605
  return `${ReactNativeBlobUtil.fs.dirs.DocumentDir}/ota-updates`;
@@ -480,10 +624,13 @@ async function cleanupOldUpdates(keepLabel) {
480
624
  keepLabel,
481
625
  `${keepLabel}.zip`,
482
626
  `${keepLabel}.bundle`,
627
+ `${keepLabel}.apk`,
628
+ `${keepLabel}-apk`,
629
+ `${keepLabel}-apk.zip`,
483
630
  ]);
484
631
  const entries = await ReactNativeBlobUtil.fs.ls(dir);
485
632
  await Promise.all(entries.map(async (entry) => {
486
- if (keep.has(entry) || entry.endsWith('.apk')) {
633
+ if (keep.has(entry)) {
487
634
  return;
488
635
  }
489
636
  await removePathRecursive(`${dir}/${entry}`);
@@ -575,7 +722,7 @@ async function cleanupPartialDownload(path) {
575
722
  * bytesWritten !== Content-Length — including when Content-Length is wrong
576
723
  * but the full file actually landed. Recover those cases via size/hash checks.
577
724
  */
578
- async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label) {
725
+ async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label, deviceAbi) {
579
726
  try {
580
727
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
581
728
  const exists = await ReactNativeBlobUtil.fs.exists(destPath);
@@ -594,6 +741,9 @@ async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expe
594
741
  else if (!sizeLooksComplete) {
595
742
  return null;
596
743
  }
744
+ if (extension === 'apk' && (await shouldExtractApkZip(destPath, packageFormat))) {
745
+ return extractApkUpdate(label, destPath, deviceAbi);
746
+ }
597
747
  if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
598
748
  return extractBundleUpdate(label, destPath);
599
749
  }
@@ -606,16 +756,22 @@ async function tryRecoverCompletedDownload(destPath, extension, totalBytes, expe
606
756
  async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, onProgress, extension = 'bundle', deviceAbi, packageFormat = 'bundle', expectedHash) {
607
757
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
608
758
  const isZipBundle = extension === 'bundle' && packageFormat === 'zip';
759
+ const isApkZip = extension === 'apk' && packageFormat === 'zip';
609
760
  const destPath = isZipBundle
610
761
  ? getBundleZipPath(label)
611
- : extension === 'apk'
612
- ? getApkFilePath(label)
613
- : getLegacyBundleFilePath(label);
762
+ : isApkZip
763
+ ? getApkZipPath(label)
764
+ : extension === 'apk'
765
+ ? getApkFilePath(label)
766
+ : getLegacyBundleFilePath(label);
614
767
  const totalBytes = await resolveDownloadSize(downloadUrl, deploymentKey, expectedSize, deviceAbi);
615
768
  await ensureBundleDirectory();
616
769
  if (isZipBundle) {
617
770
  await removePathRecursive(getBundleReleaseDir(label));
618
771
  }
772
+ if (isApkZip) {
773
+ await removePathRecursive(getApkExtractDir(label));
774
+ }
619
775
  let lastError;
620
776
  for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt++) {
621
777
  await cleanupPartialDownload(destPath);
@@ -632,9 +788,11 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
632
788
  if (!onProgress)
633
789
  return;
634
790
  const resolvedTotal = total > 0 ? total : totalBytes;
635
- if (resolvedTotal <= 0)
791
+ if (resolvedTotal <= 0) {
792
+ onProgress(0, Math.max(received, 0), 0);
636
793
  return;
637
- onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99));
794
+ }
795
+ onProgress(Math.min(Math.max(received / resolvedTotal, 0), 0.99), Math.max(received, 0), resolvedTotal);
638
796
  };
639
797
  let pollTimer = null;
640
798
  if (onProgress && totalBytes > 0) {
@@ -658,9 +816,6 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
658
816
  }
659
817
  try {
660
818
  const response = await task;
661
- if (onProgress) {
662
- onProgress(1);
663
- }
664
819
  const downloadedPath = String(response.path()).replace(/^file:\/\//, '');
665
820
  if (downloadedPath !== destPath) {
666
821
  await ReactNativeBlobUtil.fs.cp(downloadedPath, destPath);
@@ -670,12 +825,18 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
670
825
  if (!stat || stat.size < minSize) {
671
826
  throw new Error(`Downloaded file is too small (${stat?.size ?? 0} bytes)`);
672
827
  }
828
+ if (onProgress) {
829
+ onProgress(1, stat.size, totalBytes > 0 ? totalBytes : stat.size);
830
+ }
673
831
  if (expectedHash) {
674
832
  const valid = await verifyBundleHash(destPath, expectedHash);
675
833
  if (!valid) {
676
- throw new Error('Package hash verification failed');
834
+ throw new Error('Wrong package: hash verification failed');
677
835
  }
678
836
  }
837
+ if (extension === 'apk' && (await shouldExtractApkZip(destPath, packageFormat))) {
838
+ return extractApkUpdate(label, destPath, deviceAbi);
839
+ }
679
840
  if (extension === 'bundle' && (await shouldExtractZipPackage(destPath, packageFormat))) {
680
841
  return extractBundleUpdate(label, destPath);
681
842
  }
@@ -684,7 +845,7 @@ async function downloadBundle(downloadUrl, deploymentKey, label, expectedSize, o
684
845
  catch (error) {
685
846
  lastError = error;
686
847
  if (isRetryableDownloadError(error)) {
687
- const recovered = await tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label);
848
+ const recovered = await tryRecoverCompletedDownload(destPath, extension, totalBytes, expectedHash, packageFormat, label, deviceAbi);
688
849
  if (recovered) {
689
850
  if (onProgress)
690
851
  onProgress(1);
@@ -732,8 +893,17 @@ function reportProgress(showUI, messages, onProgress, status, progress = 0, extr
732
893
  status === types_1.SyncStatus.APK_INSTALL_PENDING) {
733
894
  return;
734
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
+ }
735
905
  if (status === types_1.SyncStatus.ERROR) {
736
- showProgress(status, progress, extra?.error ?? getMessageForStatus(status, messages), {
906
+ showProgress(status, progress, extra?.error ?? statusMessage, {
737
907
  error: extra?.error,
738
908
  showActions: true,
739
909
  canSkip: false,
@@ -742,24 +912,36 @@ function reportProgress(showUI, messages, onProgress, status, progress = 0, extr
742
912
  return;
743
913
  }
744
914
  if (shouldShowModalForStatus(status)) {
745
- showProgress(status, progress, extra?.message ?? getMessageForStatus(status, messages), {
915
+ showProgress(status, progress, statusMessage, {
746
916
  showActions: false,
747
917
  canSkip: false,
748
918
  ...extra,
749
919
  });
750
920
  }
751
921
  }
752
- async function promptForUpdate(showUI, messages, onProgress, update, installMode) {
753
- const changelog = getUpdateChangelog(update);
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) {
754
939
  const isMandatory = update.isMandatory === true;
755
940
  if (!showUI) {
756
941
  return 'update';
757
942
  }
758
943
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_AVAILABLE, 0, {
759
- label: update.label,
760
- description: changelog,
761
- isMandatory,
762
- installMode,
944
+ ...buildUpdateProgressExtra(update, installMode, currentLabel),
763
945
  showActions: true,
764
946
  canSkip: !isMandatory,
765
947
  });
@@ -856,7 +1038,7 @@ async function performSync(options = {}, onProgress, messages) {
856
1038
  else {
857
1039
  await clearSkippedUpdate();
858
1040
  }
859
- const decision = await promptForUpdate(showUI, messages, onProgress, update, installMode);
1041
+ const decision = await promptForUpdate(showUI, messages, onProgress, update, installMode, meta?.label);
860
1042
  if (decision === 'skip') {
861
1043
  await markUpdateSkipped(update.label);
862
1044
  if (showUI)
@@ -870,19 +1052,14 @@ async function performSync(options = {}, onProgress, messages) {
870
1052
  }
871
1053
  await clearSkippedUpdate();
872
1054
  if (update.updateType === 'apk') {
873
- return performApkSync(update, showUI, messages, onProgress, changelog);
1055
+ return performApkSync(update, showUI, messages, onProgress, changelog, meta?.label);
874
1056
  }
875
- reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, {
876
- label: update.label,
877
- description: changelog,
878
- isMandatory: update.isMandatory,
879
- installMode,
880
- });
881
- const bundlePath = await downloadBundle(update.downloadUrl, config.deploymentKey, update.label, update.size, (p) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
882
- label: update.label,
883
- description: changelog,
884
- isMandatory: update.isMandatory,
885
- 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,
886
1063
  }), 'bundle', undefined, update.packageFormat ?? 'bundle', update.packageHash);
887
1064
  onProgress?.(types_1.SyncStatus.INSTALLING, 1);
888
1065
  const ReactNativeBlobUtil = require('react-native-blob-util').default;
@@ -904,10 +1081,8 @@ async function performSync(options = {}, onProgress, messages) {
904
1081
  const shouldRestartNow = installMode === 'immediate' || update.isMandatory === true;
905
1082
  if (shouldRestartNow) {
906
1083
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.RESTARTING, 1, {
907
- label: update.label,
908
- description: changelog,
909
- isMandatory: update.isMandatory,
910
- installMode,
1084
+ ...progressExtra,
1085
+ downloadedBytes: progressExtra.totalBytes,
911
1086
  });
912
1087
  await markRestartPending();
913
1088
  await delay(300);
@@ -920,10 +1095,8 @@ async function performSync(options = {}, onProgress, messages) {
920
1095
  }
921
1096
  if (showUI) {
922
1097
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.UPDATE_INSTALLED, 1, {
923
- label: update.label,
924
- description: changelog,
925
- isMandatory: update.isMandatory,
926
- installMode,
1098
+ ...progressExtra,
1099
+ downloadedBytes: progressExtra.totalBytes,
927
1100
  showActions: true,
928
1101
  canSkip: false,
929
1102
  message: getMessageForStatus(types_1.SyncStatus.UPDATE_INSTALLED, messages),
@@ -954,22 +1127,23 @@ async function performSync(options = {}, onProgress, messages) {
954
1127
  };
955
1128
  }
956
1129
  }
957
- async function performApkSync(update, showUI, messages, onProgress, changelog) {
1130
+ async function performApkSync(update, showUI, messages, onProgress, changelog, currentLabel) {
958
1131
  if (!update.downloadUrl || !update.label || !update.packageHash) {
959
1132
  return { status: types_1.SyncStatus.UP_TO_DATE };
960
1133
  }
961
1134
  const notes = changelog ?? getUpdateChangelog(update);
1135
+ const progressExtra = buildUpdateProgressExtra(update, undefined, currentLabel);
1136
+ const packageFormat = update.packageFormat ?? 'zip';
962
1137
  try {
963
- reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, 0, {
964
- label: update.label,
965
- description: notes,
966
- isMandatory: update.isMandatory,
967
- });
968
- const apkPath = await downloadBundle(update.downloadUrl, (0, config_1.getConfig)().deploymentKey, update.label, update.size, (p) => reportProgress(showUI, messages, onProgress, types_1.SyncStatus.DOWNLOADING, p, {
969
- label: update.label,
970
- description: notes,
971
- isMandatory: update.isMandatory,
972
- }), 'apk', update.deviceAbi, '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
+ }
973
1147
  onProgress?.(types_1.SyncStatus.INSTALLING, 1);
974
1148
  // Drop the OTA JS path before opening the installer so the new APK does not
975
1149
  // keep loading the previous bundle on first launch after install.
@@ -977,9 +1151,8 @@ async function performApkSync(update, showUI, messages, onProgress, changelog) {
977
1151
  const installResult = await installApk(apkPath);
978
1152
  if (installResult === 'permission_required') {
979
1153
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.INSTALLING, 1, {
980
- label: update.label,
981
- description: notes,
982
- isMandatory: update.isMandatory,
1154
+ ...progressExtra,
1155
+ downloadedBytes: progressExtra.totalBytes,
983
1156
  });
984
1157
  return {
985
1158
  status: types_1.SyncStatus.APK_INSTALL_PENDING,
@@ -999,6 +1172,7 @@ async function performApkSync(update, showUI, messages, onProgress, changelog) {
999
1172
  catch (error) {
1000
1173
  const err = error instanceof Error ? error : new Error(String(error));
1001
1174
  reportProgress(showUI, messages, onProgress, types_1.SyncStatus.ERROR, 0, {
1175
+ ...progressExtra,
1002
1176
  error: err.message,
1003
1177
  showActions: true,
1004
1178
  canSkip: false,
@@ -1036,6 +1210,9 @@ async function clearUpdate() {
1036
1210
  await removePathRecursive(getBundleReleaseDir(meta.label));
1037
1211
  await cleanupPartialDownload(getBundleZipPath(meta.label));
1038
1212
  await cleanupPartialDownload(getLegacyBundleFilePath(meta.label));
1213
+ await cleanupPartialDownload(getApkFilePath(meta.label));
1214
+ await cleanupPartialDownload(getApkZipPath(meta.label));
1215
+ await removePathRecursive(getApkExtractDir(meta.label));
1039
1216
  }
1040
1217
  }
1041
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 { defaultMessages, OTAProgressState } from '../sync';
3
- interface OTAUpdateModalProps extends OTAProgressState {
4
- title?: string;
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;
@@ -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, title = sync_1.defaultMessages.title, fontFamily, messages, }) {
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
- (0, sync_1.respondToUpdatePrompt)('skip');
95
+ handleSkip();
66
96
  else if (showActions && (isDeferredDone || isError)) {
67
- (0, sync_1.respondToUpdatePrompt)('dismiss');
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
- {title}
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={() => (0, sync_1.respondToUpdatePrompt)('update')}>
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={() => (0, sync_1.respondToUpdatePrompt)('skip')}>
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={() => (0, sync_1.respondToUpdatePrompt)('dismiss')}>
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
- <OTAUpdateModal_1.OTAUpdateModal {...progress} title={modalTitle} fontFamily={fontFamily} messages={messages}/>
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsonair.ir/react-native",
3
- "version": "1.0.6",
3
+ "version": "1.0.10",
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",