From 804485e431934da637c7d0c9074619f8c575322b Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 14 May 2026 16:17:37 +0800 Subject: [PATCH 1/3] fix(app-update): android downloadAPK Range/.partial resume with tri-state verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Android downloadAPK path wrote bytes straight to the final filePath and verified an existing APK by booleanly comparing it against a freshly-fetched SHA256SUMS.asc. When the network dropped mid-download every retry path landed on "ASC unreachable → existing APK invalid → delete" and restarted from byte zero, throwing away the previously-downloaded bytes on every reconnect. Mirror the proven react-native-bundle-update pattern: - In-flight bytes live in .partial; the final path only ever holds a fully transferred APK. Old half-baked files at the final path are promoted to .partial so existing installs benefit from resume on the next attempt. - Resume via `Range: bytes=-`; handle 206 (append), 200 (server ignored Range → restart), and 416 with Content-Range recovery for the crashed-just-before-rename case. - verifyExistingApk returns an ApkVerifyOutcome tri-state — Valid / HashMismatch / Indeterminate. Indeterminate (ASC fetch failure, ASC parse failure, etc.) preserves the partial and bubbles up a transient error so the JS retry layer can wait for network instead of wiping the bytes preemptively. --- .../ReactNativeAppUpdate.kt | 336 ++++++++++++++---- 1 file changed, 274 insertions(+), 62 deletions(-) diff --git a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt index ab129afa..f49c4d38 100644 --- a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt +++ b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt @@ -17,8 +17,6 @@ import com.margelo.nitro.nativelogger.OneKeyLog import com.tencent.mmkv.MMKV import okhttp3.OkHttpClient import okhttp3.Request -import okio.buffer -import okio.sink import java.io.BufferedInputStream import java.io.BufferedReader import java.io.File @@ -309,43 +307,71 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { } /** - * Check if an existing APK is valid by verifying GPG signature and SHA256 against the ASC file. - * Downloads ASC if not present. Returns true if APK is valid. + * Outcome of verifying an existing APK against its detached SHA256SUMS.asc. + * Three states matter: a clean pass, a real hash mismatch (file is stale and + * must be discarded), and "we can't tell" — typically because the ASC was + * unfetchable (no network) or the local ASC is unparseable. The previous + * Boolean collapsed Indeterminate into "invalid" and the caller deleted a + * perfectly-good partial download on every transient network blip. */ - private fun tryVerifyExistingApk(url: String, filePath: String, apkFile: File): Boolean { + private sealed class ApkVerifyOutcome { + object Valid : ApkVerifyOutcome() + object HashMismatch : ApkVerifyOutcome() + object Indeterminate : ApkVerifyOutcome() + } + + /** + * Check the apkFile against its detached SHA256SUMS.asc and return a + * tri-state outcome. Callers must NOT delete the APK on Indeterminate — + * the bytes on disk may still be the right ones; the next online retry can + * decide. Downloads ASC if not present. + */ + private fun verifyExistingApk(url: String, filePath: String, apkFile: File): ApkVerifyOutcome { return try { val ascFilePath = "$filePath.SHA256SUMS.asc" val ascFile = buildFile(ascFilePath) if (!ascFile.exists()) { val ascUrl = "$url.SHA256SUMS.asc" - OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC not found, downloading from $ascUrl") - if (downloadAscFile(ascUrl, ascFile) == null) { - OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: ASC download failed") - return false + OneKeyLog.info("AppUpdate", "verifyExistingApk: ASC not found, downloading from $ascUrl") + val downloaded = try { + downloadAscFile(ascUrl, ascFile) + } catch (e: Exception) { + // OkHttp throws IOException family on offline / DNS / connection-reset. + // Treat as Indeterminate so caller preserves the partial. + OneKeyLog.warn("AppUpdate", "verifyExistingApk: ASC download threw ${e.javaClass.simpleName}: ${e.message}") + null + } + if (downloaded == null) { + OneKeyLog.warn("AppUpdate", "verifyExistingApk: ASC unavailable, indeterminate") + return ApkVerifyOutcome.Indeterminate } - OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC downloaded to ${ascFile.absolutePath}") + OneKeyLog.info("AppUpdate", "verifyExistingApk: ASC downloaded to ${ascFile.absolutePath}") } val expectedSha256 = verifyAscAndExtractSha256(ascFile) if (expectedSha256 == null) { - OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: GPG verification or SHA256 extraction failed") - return false + // Local ASC failed parsing/GPG. Could be a corrupted cache from + // an earlier interrupted write — drop it so the next round + // re-downloads cleanly. Don't condemn the APK on this alone. + OneKeyLog.warn("AppUpdate", "verifyExistingApk: GPG verification or SHA256 extraction failed, discarding local ASC") + ascFile.delete() + return ApkVerifyOutcome.Indeterminate } - OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: computing SHA256 of existing APK (size=${apkFile.length()})...") + OneKeyLog.info("AppUpdate", "verifyExistingApk: computing SHA256 of existing APK (size=${apkFile.length()})...") val actualSha256 = computeSha256(apkFile) if (secureCompare(actualSha256, expectedSha256)) { - OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: SHA256 matches, APK is valid") - true + OneKeyLog.info("AppUpdate", "verifyExistingApk: SHA256 matches, APK is valid") + ApkVerifyOutcome.Valid } else { - OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: SHA256 mismatch, expected=${expectedSha256.take(16)}..., got=${actualSha256.take(16)}...") - false + OneKeyLog.warn("AppUpdate", "verifyExistingApk: SHA256 mismatch, expected=${expectedSha256.take(16)}..., got=${actualSha256.take(16)}...") + ApkVerifyOutcome.HashMismatch } } catch (e: Exception) { - OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: failed: ${e.javaClass.simpleName}: ${e.message}") - false + OneKeyLog.warn("AppUpdate", "verifyExistingApk: unexpected failure: ${e.javaClass.simpleName}: ${e.message}") + ApkVerifyOutcome.Indeterminate } } @@ -417,71 +443,257 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { throw Exception("Download URL must use HTTPS") } + // Resume model (mirrors react-native-bundle-update's downloadBundle): + // * Bytes in flight live in .partial. + // * The "final" filePath only ever holds a fully transferred APK + // (verified or about-to-be-verified). This keeps the + // "exists at filePath -> already valid" cache check below + // immune to the previous bug where a half-baked APK at the + // final path looked complete to clearCache / installAPK callers. + val partialFilePath = "$filePath.partial" val downloadedFile = buildFile(filePath) + val partialFile = buildFile(partialFilePath) + val expectedSize = if (fileSize > 0) fileSize else 0L + + // Phase 1 — adopt anything already at the final path. + // Old builds (pre-resume) wrote partial bytes here directly, so + // a small file at filePath is most likely a stalled download + // from an earlier app version: promote it to .partial so we can + // Range-resume instead of starting over. A correctly-sized file + // goes through the tri-state verifier and only gets deleted on + // a real hash mismatch — never on an unfetchable ASC. if (downloadedFile.exists()) { - OneKeyLog.info("AppUpdate", "downloadAPK: existing APK found (size=${downloadedFile.length()}), verifying...") - if (tryVerifyExistingApk(url, filePath, downloadedFile)) { - OneKeyLog.info("AppUpdate", "downloadAPK: existing APK is valid, skipping download") - sendEvent("update/downloaded") - return@async + val existingSize = downloadedFile.length() + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK at final path (size=$existingSize, expected=$expectedSize)") + when { + expectedSize > 0 && existingSize > expectedSize -> { + OneKeyLog.warn("AppUpdate", "downloadAPK: existing APK larger than expected, deleting") + downloadedFile.delete() + } + expectedSize > 0 && existingSize < expectedSize -> { + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK smaller than expected, promoting to .partial for resume") + if (partialFile.exists()) partialFile.delete() + if (!downloadedFile.renameTo(partialFile)) { + OneKeyLog.warn("AppUpdate", "downloadAPK: rename to .partial failed, deleting stale final") + downloadedFile.delete() + } + } + else -> { + when (verifyExistingApk(url, filePath, downloadedFile)) { + ApkVerifyOutcome.Valid -> { + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK is valid, skipping download") + sendEvent("update/downloaded") + return@async + } + ApkVerifyOutcome.HashMismatch -> { + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK hash mismatch, deleting and re-downloading") + downloadedFile.delete() + if (partialFile.exists()) partialFile.delete() + } + ApkVerifyOutcome.Indeterminate -> { + // ASC could not be fetched (offline) or could + // not be parsed. The on-disk APK might still + // be the right one — surface a transient + // error so the JS retry layer waits for + // network and re-runs verify, instead of + // wiping the bytes preemptively. + OneKeyLog.warn("AppUpdate", "downloadAPK: cannot verify existing APK (ASC unavailable); preserving file and aborting this attempt") + throw java.io.IOException("APK verification deferred: ASC unavailable") + } + } + } } - OneKeyLog.info("AppUpdate", "downloadAPK: existing APK invalid, deleting and re-downloading...") - downloadedFile.delete() } + // Phase 2 — pick up an in-flight partial. + var partialBytes = 0L + if (partialFile.exists()) { + val partialSize = partialFile.length() + when { + expectedSize > 0 && partialSize == expectedSize -> { + // Full body on disk but the previous run was killed + // before promotion. Try promote + verify before + // re-downloading. + OneKeyLog.info("AppUpdate", "downloadAPK: partial matches expected size ($partialSize), trying promote+verify") + if (downloadedFile.exists()) downloadedFile.delete() + if (partialFile.renameTo(downloadedFile)) { + when (verifyExistingApk(url, filePath, downloadedFile)) { + ApkVerifyOutcome.Valid -> { + OneKeyLog.info("AppUpdate", "downloadAPK: recovered crashed-before-rename APK, skipping download") + sendEvent("update/downloaded") + return@async + } + ApkVerifyOutcome.HashMismatch -> { + OneKeyLog.warn("AppUpdate", "downloadAPK: promoted partial failed hash check, discarding") + downloadedFile.delete() + } + ApkVerifyOutcome.Indeterminate -> { + // Roll back the rename so the bytes stay + // as .partial for the next attempt. + OneKeyLog.warn("AppUpdate", "downloadAPK: promoted partial verify indeterminate, rolling back to .partial") + if (!downloadedFile.renameTo(partialFile)) { + OneKeyLog.warn("AppUpdate", "downloadAPK: rollback rename failed, deleting final to preserve invariant") + downloadedFile.delete() + } + throw java.io.IOException("APK verification deferred: ASC unavailable") + } + } + } else { + OneKeyLog.warn("AppUpdate", "downloadAPK: rename .partial -> final failed, discarding both files") + if (downloadedFile.exists()) downloadedFile.delete() + } + } + expectedSize > 0 && partialSize > expectedSize -> { + OneKeyLog.warn("AppUpdate", "downloadAPK: stale partial (>expected $partialSize/$expectedSize), discarding") + partialFile.delete() + } + partialSize > 0 -> { + partialBytes = partialSize + OneKeyLog.info("AppUpdate", "downloadAPK: resuming from $partialBytes bytes (expected=$expectedSize)") + } + else -> partialFile.delete() + } + } + + // Phase 3 — fetch (with Range header iff resuming). val client = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .followRedirects(false) .followSslRedirects(false) .build() - val request = Request.Builder().url(url).build() - val response = client.newCall(request).execute() + val requestBuilder = Request.Builder().url(url) + if (partialBytes > 0) { + requestBuilder.addHeader("Range", "bytes=$partialBytes-") + } + sendEvent("update/start") + OneKeyLog.info("AppUpdate", "downloadAPK: starting download (resume=${partialBytes > 0})...") + + val response = client.newCall(requestBuilder.build()).execute() + + // 416 Range Not Satisfiable: server says our offset is past the + // file length. Two sub-cases distinguishable from + // `Content-Range: bytes */`: + // (a) total == partialBytes → file is exactly complete on + // server; our partial IS the whole APK and just needs + // SHA verify + rename. Recover instead of wipe. + // (b) anything else → partial is corrupt or build changed. + // Wipe and bubble up. + if (response.code == 416) { + val contentRange = response.header("Content-Range") + response.close() + val totalFromHeader = contentRange + ?.let { Regex("""bytes\s+\*\s*/\s*(\d+)""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() } + if (totalFromHeader != null && totalFromHeader == partialBytes && partialFile.exists()) { + OneKeyLog.info("AppUpdate", "downloadAPK: HTTP 416 with total=$totalFromHeader matches partial, attempting promote+verify") + if (downloadedFile.exists()) downloadedFile.delete() + if (partialFile.renameTo(downloadedFile)) { + when (verifyExistingApk(url, filePath, downloadedFile)) { + ApkVerifyOutcome.Valid -> { + OneKeyLog.info("AppUpdate", "downloadAPK: 416 recovery succeeded, skipping download") + sendEvent("update/downloaded") + return@async + } + ApkVerifyOutcome.HashMismatch -> { + OneKeyLog.warn("AppUpdate", "downloadAPK: 416 recovery hash mismatch, discarding") + downloadedFile.delete() + } + ApkVerifyOutcome.Indeterminate -> { + OneKeyLog.warn("AppUpdate", "downloadAPK: 416 recovery verify indeterminate, rolling back") + if (!downloadedFile.renameTo(partialFile)) { + downloadedFile.delete() + } + throw java.io.IOException("APK verification deferred: ASC unavailable") + } + } + } + } + OneKeyLog.warn("AppUpdate", "downloadAPK: HTTP 416 (range not satisfiable), discarding partial and failing attempt") + if (partialFile.exists()) partialFile.delete() + throw Exception("HTTP 416 (range not satisfiable)") + } - if (!response.isSuccessful) { + if (!response.isSuccessful || (response.code != 200 && response.code != 206)) { OneKeyLog.error("AppUpdate", "downloadAPK: HTTP error, statusCode=${response.code}") + response.close() sendEvent("update/error", message = response.code.toString()) throw Exception(response.code.toString()) } - val body = response.body ?: throw Exception("Empty response body") - val contentLength = if (fileSize > 0) fileSize else body.contentLength() - OneKeyLog.info("AppUpdate", "downloadAPK: HTTP 200, contentLength=$contentLength, starting download...") - val source = body.source() - val sink = downloadedFile.sink().buffer() - val sinkBuffer = sink.buffer + val expectsResume = partialBytes > 0 + val isPartialResponse = response.code == 206 - var totalBytesRead = 0L - val bufferSize = 8 * 1024L - sendEvent("update/start") - var prevProgress = 0 - - try { - while (true) { - val bytesRead = source.read(sinkBuffer, bufferSize) - if (bytesRead == -1L) break - sink.emit() - totalBytesRead += bytesRead - if (contentLength > 0) { - val progress = ((totalBytesRead * 100) / contentLength).toInt() - if (prevProgress != progress) { - sendEvent("update/downloading", progress = progress) - OneKeyLog.info("AppUpdate", "download progress: $progress%") - builder.setProgress(100, progress, false) - if (ActivityCompat.checkSelfPermission( - context, android.Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { - notifyManager.notify(NOTIFICATION_ID, builder.build()) + // Server may legally ignore Range and reply 200 with the full + // body. Drop the stale partial and restart from byte zero. + if (expectsResume && !isPartialResponse) { + OneKeyLog.warn("AppUpdate", "downloadAPK: requested Range but server returned 200, restarting from scratch") + if (partialFile.exists()) partialFile.delete() + partialBytes = 0L + } + + val body = response.body ?: run { + response.close() + throw Exception("Empty response body") + } + val contentLength = body.contentLength() + val totalSize: Long = if (isPartialResponse) { + val contentRange = response.header("Content-Range") + val parsedTotal = contentRange + ?.let { Regex("""bytes \d+-\d+/(\d+)""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() } + parsedTotal ?: (partialBytes + contentLength.coerceAtLeast(0L)) + } else { + if (contentLength > 0) contentLength else expectedSize + } + OneKeyLog.info("AppUpdate", "downloadAPK: HTTP ${response.code}, contentLength=$contentLength, totalSize=$totalSize, partialBytes=$partialBytes, downloading...") + + val parentDir = partialFile.parentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + OneKeyLog.info("AppUpdate", "downloadAPK: created parent directory: ${parentDir.absolutePath}") + } + + // Append iff server granted us a 206. On a 200 (full body + // restart) we truncate the partial file. + val appendMode = isPartialResponse + var totalBytesRead = if (isPartialResponse) partialBytes else 0L + var prevProgress = -1 + + body.byteStream().use { inputStream -> + FileOutputStream(partialFile.absolutePath, appendMode).use { outputStream -> + val buffer = ByteArray(8192) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + totalBytesRead += bytesRead + if (totalSize > 0) { + val progress = ((totalBytesRead * 100) / totalSize).toInt().coerceIn(0, 100) + if (progress != prevProgress) { + sendEvent("update/downloading", progress = progress) + OneKeyLog.info("AppUpdate", "download progress: $progress% ($totalBytesRead/$totalSize)") + builder.setProgress(100, progress, false) + if (ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notifyManager.notify(NOTIFICATION_ID, builder.build()) + } + prevProgress = progress } - prevProgress = progress } } } - } finally { - sink.flush() - sink.close() - source.close() + } + + OneKeyLog.info("AppUpdate", "downloadAPK: download finished, totalBytesRead=$totalBytesRead, finalizing...") + + // Promote .partial -> final ONLY after the full transfer. Doing + // it before would mean a SHA mismatch leaves a half-baked + // filePath that the next call would mistake for a cached good + // APK. + if (downloadedFile.exists()) downloadedFile.delete() + if (!partialFile.renameTo(downloadedFile)) { + OneKeyLog.error("AppUpdate", "downloadAPK: rename .partial -> final failed") + throw Exception("Failed to finalize download") } OneKeyLog.info("AppUpdate", "Download completed") From 8a391de55fa84df3132feaf130b9ba3623fe548c Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 14 May 2026 17:28:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(app-update):=20harden=20APK=20resume=20?= =?UTF-8?q?=E2=80=94=20promote=20helper,=20byte-copy=20rollback,=20206=20s?= =?UTF-8?q?tart=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up on the Range/.partial resume work: - Extract the three-times-duplicated promote+verify+dispatch into `tryPromoteAndVerify` so future tweaks to promotion semantics happen in one place. - `rollbackFinalToPartial` falls back to a stream copy when the same-fs rename fails, so a transient FS hiccup cannot wipe the only copy of an already-downloaded payload. - Validate `Content-Range: bytes start-end/total` on 206 responses — if start ≠ requested offset (CDN bug / proxy rewrite), demote to a full restart instead of appending mis-aligned bytes and only catching it at SHA verify. - 416 + hash mismatch now surfaces "server build changed mid-download" instead of the misleading raw "HTTP 416". - Type the deferred-verification error as `ApkVerificationDeferredException` (extends IOException) so JS retry layer can branch on class name instead of substring-matching the message. --- .../ReactNativeAppUpdate.kt | 240 ++++++++++++++---- 1 file changed, 189 insertions(+), 51 deletions(-) diff --git a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt index f49c4d38..55d363f1 100644 --- a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt +++ b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt @@ -375,6 +375,114 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { } } + /** + * Thrown when we cannot decide whether the bytes on disk are still + * the right APK (typically: ASC unreachable due to offline). Extends + * IOException so existing IOException-aware callers still match it, + * but a dedicated type lets JS branch on `IOException`/class name + * instead of substring-matching the message — which would silently + * rot the moment we tweak the wording. + */ + private class ApkVerificationDeferredException : + java.io.IOException(DEFERRED_VERIFICATION_MESSAGE) { + companion object { + const val DEFERRED_VERIFICATION_MESSAGE = "APK verification deferred: ASC unavailable" + } + } + + /** + * Move bytes from the final path back into the .partial slot. Tries + * an atomic rename first; on the rare same-fs rename failure, falls + * back to a stream copy so a transient FS hiccup cannot destroy the + * one and only copy of an already-downloaded payload (the exact + * regression this PR is trying to avoid). Returns true if the bytes + * are at partialFile by the time we return. + */ + private fun rollbackFinalToPartial(downloadedFile: File, partialFile: File): Boolean { + if (!downloadedFile.exists()) return false + if (partialFile.exists()) partialFile.delete() + if (downloadedFile.renameTo(partialFile)) return true + OneKeyLog.warn("AppUpdate", "rollbackFinalToPartial: rename failed, falling back to byte copy (size=${downloadedFile.length()})") + return try { + FileInputStream(downloadedFile).use { input -> + FileOutputStream(partialFile).use { output -> + val buffer = ByteArray(8192) + var n: Int + while (input.read(buffer).also { n = it } != -1) { + output.write(buffer, 0, n) + } + } + } + val ok = partialFile.length() == downloadedFile.length() + if (ok) { + downloadedFile.delete() + OneKeyLog.info("AppUpdate", "rollbackFinalToPartial: byte copy fallback succeeded (size=${partialFile.length()})") + true + } else { + OneKeyLog.error("AppUpdate", "rollbackFinalToPartial: byte copy size mismatch (partial=${partialFile.length()}, final=${downloadedFile.length()}), discarding copy") + partialFile.delete() + false + } + } catch (e: Exception) { + OneKeyLog.error("AppUpdate", "rollbackFinalToPartial: byte copy fallback failed: ${e.javaClass.simpleName}: ${e.message}") + if (partialFile.exists()) partialFile.delete() + false + } + } + + /** + * Outcome of promoting a fully-sized .partial to the final path and + * running the GPG/SHA verifier against it. + */ + private sealed class PromoteOutcome { + object Valid : PromoteOutcome() + object HashMismatch : PromoteOutcome() + object Deferred : PromoteOutcome() // verifier Indeterminate; bytes restored to .partial + object RenameFailed : PromoteOutcome() // promotion rename failed; bytes preserved at .partial + } + + /** + * Promote .partial -> final, run the verifier, and dispatch the + * tri-state outcome. On Indeterminate the bytes are rolled back into + * .partial (with copy fallback if rename fails), so callers can + * retry later. On promotion-rename failure the .partial is kept + * intact so Phase 3 can Range-resume on the next pass. + * + * This consolidates the previously three-times-duplicated + * promote+verify+dispatch logic in downloadAPK. Any future tweak to + * promotion semantics happens once here, not in three drift-prone + * sites. + */ + private fun tryPromoteAndVerify( + url: String, + filePath: String, + partialFile: File, + downloadedFile: File + ): PromoteOutcome { + if (downloadedFile.exists()) downloadedFile.delete() + if (!partialFile.renameTo(downloadedFile)) { + OneKeyLog.warn("AppUpdate", "tryPromoteAndVerify: rename .partial -> final failed, preserving .partial for Range resume") + // renameTo is atomic on same fs: either partial moved to + // final or it didn't. On failure the bytes are still at + // partialFile; leave them so Phase 3 can Range-resume. + if (downloadedFile.exists()) downloadedFile.delete() + return PromoteOutcome.RenameFailed + } + return when (verifyExistingApk(url, filePath, downloadedFile)) { + ApkVerifyOutcome.Valid -> PromoteOutcome.Valid + ApkVerifyOutcome.HashMismatch -> { + downloadedFile.delete() + PromoteOutcome.HashMismatch + } + ApkVerifyOutcome.Indeterminate -> { + if (!rollbackFinalToPartial(downloadedFile, partialFile)) { + OneKeyLog.error("AppUpdate", "tryPromoteAndVerify: rollback failed for both rename and byte copy; bytes lost") + } + PromoteOutcome.Deferred + } + } + } + private fun isDebuggable(): Boolean { val context = NitroModules.applicationContext ?: return false return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0 @@ -462,6 +570,14 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { // Range-resume instead of starting over. A correctly-sized file // goes through the tri-state verifier and only gets deleted on // a real hash mismatch — never on an unfetchable ASC. + // + // Caveat: when expectedSize == 0 (caller didn't pass fileSize) + // we cannot distinguish a complete cached APK from a pre-resume + // half-baked one — both flow into the verifier, and a stale + // half-baked file that happens to be online will hit + // HashMismatch and get deleted (correct, but loses the bytes). + // Always pass fileSize from JS to unlock the size-based + // pre-resume migration path. if (downloadedFile.exists()) { val existingSize = downloadedFile.length() OneKeyLog.info("AppUpdate", "downloadAPK: existing APK at final path (size=$existingSize, expected=$expectedSize)") @@ -498,7 +614,7 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { // network and re-runs verify, instead of // wiping the bytes preemptively. OneKeyLog.warn("AppUpdate", "downloadAPK: cannot verify existing APK (ASC unavailable); preserving file and aborting this attempt") - throw java.io.IOException("APK verification deferred: ASC unavailable") + throw ApkVerificationDeferredException() } } } @@ -515,32 +631,25 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { // before promotion. Try promote + verify before // re-downloading. OneKeyLog.info("AppUpdate", "downloadAPK: partial matches expected size ($partialSize), trying promote+verify") - if (downloadedFile.exists()) downloadedFile.delete() - if (partialFile.renameTo(downloadedFile)) { - when (verifyExistingApk(url, filePath, downloadedFile)) { - ApkVerifyOutcome.Valid -> { - OneKeyLog.info("AppUpdate", "downloadAPK: recovered crashed-before-rename APK, skipping download") - sendEvent("update/downloaded") - return@async - } - ApkVerifyOutcome.HashMismatch -> { - OneKeyLog.warn("AppUpdate", "downloadAPK: promoted partial failed hash check, discarding") - downloadedFile.delete() - } - ApkVerifyOutcome.Indeterminate -> { - // Roll back the rename so the bytes stay - // as .partial for the next attempt. - OneKeyLog.warn("AppUpdate", "downloadAPK: promoted partial verify indeterminate, rolling back to .partial") - if (!downloadedFile.renameTo(partialFile)) { - OneKeyLog.warn("AppUpdate", "downloadAPK: rollback rename failed, deleting final to preserve invariant") - downloadedFile.delete() - } - throw java.io.IOException("APK verification deferred: ASC unavailable") - } + when (tryPromoteAndVerify(url, filePath, partialFile, downloadedFile)) { + PromoteOutcome.Valid -> { + OneKeyLog.info("AppUpdate", "downloadAPK: recovered crashed-before-rename APK, skipping download") + sendEvent("update/downloaded") + return@async + } + PromoteOutcome.HashMismatch -> { + OneKeyLog.warn("AppUpdate", "downloadAPK: promoted partial failed hash check, discarding") + // Helper already deleted final; partial slot empty. + // Fall through: Phase 3 fetches from byte zero. + } + PromoteOutcome.Deferred -> { + throw ApkVerificationDeferredException() + } + PromoteOutcome.RenameFailed -> { + // Bytes preserved at .partial: Phase 3 will Range-resume. + OneKeyLog.warn("AppUpdate", "downloadAPK: promote rename failed, will Range-resume from .partial") + partialBytes = partialFile.length() } - } else { - OneKeyLog.warn("AppUpdate", "downloadAPK: rename .partial -> final failed, discarding both files") - if (downloadedFile.exists()) downloadedFile.delete() } } expectedSize > 0 && partialSize > expectedSize -> { @@ -586,25 +695,31 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { ?.let { Regex("""bytes\s+\*\s*/\s*(\d+)""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() } if (totalFromHeader != null && totalFromHeader == partialBytes && partialFile.exists()) { OneKeyLog.info("AppUpdate", "downloadAPK: HTTP 416 with total=$totalFromHeader matches partial, attempting promote+verify") - if (downloadedFile.exists()) downloadedFile.delete() - if (partialFile.renameTo(downloadedFile)) { - when (verifyExistingApk(url, filePath, downloadedFile)) { - ApkVerifyOutcome.Valid -> { - OneKeyLog.info("AppUpdate", "downloadAPK: 416 recovery succeeded, skipping download") - sendEvent("update/downloaded") - return@async - } - ApkVerifyOutcome.HashMismatch -> { - OneKeyLog.warn("AppUpdate", "downloadAPK: 416 recovery hash mismatch, discarding") - downloadedFile.delete() - } - ApkVerifyOutcome.Indeterminate -> { - OneKeyLog.warn("AppUpdate", "downloadAPK: 416 recovery verify indeterminate, rolling back") - if (!downloadedFile.renameTo(partialFile)) { - downloadedFile.delete() - } - throw java.io.IOException("APK verification deferred: ASC unavailable") - } + when (tryPromoteAndVerify(url, filePath, partialFile, downloadedFile)) { + PromoteOutcome.Valid -> { + OneKeyLog.info("AppUpdate", "downloadAPK: 416 recovery succeeded, skipping download") + sendEvent("update/downloaded") + return@async + } + PromoteOutcome.HashMismatch -> { + // Server says "you have it all" AND sizes match, + // but SHA doesn't: the upstream build was + // replaced after we started downloading. That's + // the actual failure — raw "HTTP 416" misleads + // anyone reading the error. + OneKeyLog.warn("AppUpdate", "downloadAPK: 416 recovery hash mismatch — server build changed mid-download") + if (partialFile.exists()) partialFile.delete() + throw java.io.IOException("Server build changed mid-download (size matches but hash differs)") + } + PromoteOutcome.Deferred -> { + throw ApkVerificationDeferredException() + } + PromoteOutcome.RenameFailed -> { + // Bytes still at .partial; surface a transient + // error so caller retries (and we'll try the + // promotion again next pass). + OneKeyLog.warn("AppUpdate", "downloadAPK: 416 recovery rename failed, retry later") + throw java.io.IOException("Failed to finalize 416 recovery (rename failed)") } } } @@ -621,29 +736,48 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { } val expectsResume = partialBytes > 0 - val isPartialResponse = response.code == 206 + var serverWillResume = response.code == 206 // Server may legally ignore Range and reply 200 with the full // body. Drop the stale partial and restart from byte zero. - if (expectsResume && !isPartialResponse) { + if (expectsResume && !serverWillResume) { OneKeyLog.warn("AppUpdate", "downloadAPK: requested Range but server returned 200, restarting from scratch") if (partialFile.exists()) partialFile.delete() partialBytes = 0L } + // 206 sanity check: the server MUST tell us where its body + // starts. If `Content-Range: bytes start-end/total` is missing + // or `start != partialBytes` (CDN bug / proxy rewrite), we'd + // be appending mis-aligned bytes and only catching it later + // at the SHA step — with the partial now corrupted. Demote to + // a full restart instead. + val rangeRegex = Regex("""bytes\s+(\d+)\s*-\s*(\d+)\s*/\s*(\d+|\*)""") + val rangeMatch = if (serverWillResume) { + response.header("Content-Range")?.let { rangeRegex.find(it) } + } else null + if (serverWillResume) { + val rangeStart = rangeMatch?.groupValues?.getOrNull(1)?.toLongOrNull() + if (rangeStart == null || rangeStart != partialBytes) { + OneKeyLog.warn("AppUpdate", "downloadAPK: 206 Content-Range start mismatch (header='${response.header("Content-Range")}', requested=$partialBytes); treating as full restart") + if (partialFile.exists()) partialFile.delete() + partialBytes = 0L + serverWillResume = false + } + } + val body = response.body ?: run { response.close() throw Exception("Empty response body") } val contentLength = body.contentLength() - val totalSize: Long = if (isPartialResponse) { - val contentRange = response.header("Content-Range") - val parsedTotal = contentRange - ?.let { Regex("""bytes \d+-\d+/(\d+)""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() } + val totalSize: Long = if (serverWillResume) { + val parsedTotal = rangeMatch?.groupValues?.getOrNull(3)?.toLongOrNull() parsedTotal ?: (partialBytes + contentLength.coerceAtLeast(0L)) } else { if (contentLength > 0) contentLength else expectedSize } + val isPartialResponse = serverWillResume OneKeyLog.info("AppUpdate", "downloadAPK: HTTP ${response.code}, contentLength=$contentLength, totalSize=$totalSize, partialBytes=$partialBytes, downloading...") val parentDir = partialFile.parentFile @@ -656,6 +790,10 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { // restart) we truncate the partial file. val appendMode = isPartialResponse var totalBytesRead = if (isPartialResponse) partialBytes else 0L + // -1 sentinel (vs the old 0) so 0% emits exactly once on a + // fresh start. Listeners just set state from event.progress, + // so this is a benign improvement (no double-fire on resume — + // a resumed download's first event is already >0%). var prevProgress = -1 body.byteStream().use { inputStream -> From b07a36eb72598decd25b9bcca819d14a1d98da2f Mon Sep 17 00:00:00 2001 From: huhuanming Date: Thu, 14 May 2026 17:28:56 +0800 Subject: [PATCH 3/3] chore: bump packages to 3.0.37 --- native-modules/native-logger/package.json | 2 +- native-modules/react-native-aes-crypto/package.json | 2 +- native-modules/react-native-app-update/package.json | 2 +- native-modules/react-native-async-storage/package.json | 2 +- native-modules/react-native-background-thread/package.json | 2 +- native-modules/react-native-bundle-update/package.json | 2 +- .../react-native-check-biometric-auth-changed/package.json | 2 +- native-modules/react-native-cloud-fs/package.json | 2 +- native-modules/react-native-cloud-kit-module/package.json | 2 +- native-modules/react-native-device-utils/package.json | 2 +- native-modules/react-native-dns-lookup/package.json | 2 +- native-modules/react-native-get-random-values/package.json | 2 +- native-modules/react-native-keychain-module/package.json | 2 +- native-modules/react-native-lite-card/package.json | 2 +- native-modules/react-native-network-info/package.json | 2 +- native-modules/react-native-pbkdf2/package.json | 2 +- native-modules/react-native-perf-memory/package.json | 2 +- native-modules/react-native-perf-stats/package.json | 2 +- native-modules/react-native-ping/package.json | 2 +- native-modules/react-native-splash-screen/package.json | 2 +- native-modules/react-native-split-bundle-loader/package.json | 4 ++-- native-modules/react-native-tcp-socket/package.json | 2 +- native-modules/react-native-zip-archive/package.json | 2 +- native-views/react-native-auto-size-input/package.json | 2 +- native-views/react-native-pager-view/package.json | 2 +- native-views/react-native-scroll-guard/package.json | 2 +- native-views/react-native-skeleton/package.json | 2 +- native-views/react-native-tab-view/package.json | 2 +- yarn.lock | 2 +- 29 files changed, 30 insertions(+), 30 deletions(-) diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index 5ca3b97b..e82f0e6a 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index cd24e5f8..832e7969 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index 325bd5ec..ec669578 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index f9c1dd45..d9f5ee41 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 7312d3ea..7d42bbe9 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index 384bd6a9..c120dda6 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 7b2182d2..c2f37c0c 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index f8f2014b..2fd4179c 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index 5abd5d5c..701bb816 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index db971439..51c9d2f8 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index 55e0cfcc..4101741f 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 03c353f4..4b01b5fd 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index ea70ed5b..27efe186 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index b4cac087..9f514626 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.36", + "version": "3.0.37", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index cf7ab87d..1c488df4 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 03139e50..fee8626a 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index 4529ac9a..090916e1 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 1eff058f..06fcdad2 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index 9f39b8e5..8df2b8c9 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 0897d56f..7ffe8e3a 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index b311ca70..12718f5b 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -82,7 +82,7 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@onekeyfe/react-native-bundle-update": ">=3.0.36", + "@onekeyfe/react-native-bundle-update": ">=3.0.37", "react": "*", "react-native": "*" }, diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index feaf3adc..c40b43bf 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index 6c3231dd..eddb6cb4 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-zip-archive TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index ff50cd36..9421f78d 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.36", + "version": "3.0.37", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index d15a203e..4127eaa6 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.36", + "version": "3.0.37", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index 4778a38b..c555ca48 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.36", + "version": "3.0.37", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index f3374042..c60ea7af 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.36", + "version": "3.0.37", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index de9fdeec..5ee14eed 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.36", + "version": "3.0.37", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/yarn.lock b/yarn.lock index 03a84a2b..eb12394f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3619,7 +3619,7 @@ __metadata: turbo: "npm:^2.5.6" typescript: "npm:^5.9.2" peerDependencies: - "@onekeyfe/react-native-bundle-update": ">=3.0.36" + "@onekeyfe/react-native-bundle-update": ">=3.0.37" react: "*" react-native: "*" languageName: unknown