diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index 243fef07..12b2b881 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.67", + "version": "3.0.68", "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 d8eed6c8..ca714a54 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.67", + "version": "3.0.68", "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/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 0b492be9..b2db2c24 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 @@ -1143,7 +1143,18 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { OneKeyLog.info("AppUpdate", "verifyAPK: package name matches installed app") } - // Verify APK signing certificate matches the installed app + // Verify APK signing certificate matches the installed app. + // + // This cross-check is defense-in-depth ONLY: by the time verifyAPK + // runs, verifyASC has already proven the bytes on disk are the + // authentic OneKey APK via GPG + SHA-256, and Android's own + // PackageInstaller re-verifies the signing certificate at install + // time. So a genuine MISMATCH is still hard-failed, but when the + // platform can't read the archive's signers (null) we just log it + // and silently skip — on some OEM ROMs (Huawei/EMUI Android 9–10) + // and for large APKs getPackageArchiveInfo returns null signers + // even for a perfectly valid APK, and blocking on that bricks the + // update for those users. OneKeyLog.info("AppUpdate", "verifyAPK: verifying APK signing certificate (API level=${Build.VERSION.SDK_INT})...") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, PackageManager.GET_SIGNING_CERTIFICATES) @@ -1151,9 +1162,7 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { val apkSigners = apkInfo?.signingInfo?.apkContentsSigners val installedSigners = installedInfo?.signingInfo?.apkContentsSigners if (apkSigners == null || installedSigners == null) { - OneKeyLog.error("AppUpdate", "verifyAPK: signing info unavailable (apkSigners=${apkSigners != null}, installedSigners=${installedSigners != null})") - if (!debugBuild) throw Exception("SIGNATURE_UNAVAILABLE") - OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring unavailable signatures") + OneKeyLog.info("AppUpdate", "verifyAPK: signing info unavailable (apkSigners=${apkSigners != null}, installedSigners=${installedSigners != null}), skipping signing certificate check") } else { OneKeyLog.info("AppUpdate", "verifyAPK: APK signers count=${apkSigners.size}, installed signers count=${installedSigners.size}") if (apkSigners.toSet() != installedSigners.toSet()) { @@ -1172,9 +1181,7 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { val apkSignatures = apkInfo?.signatures val installedSignatures = installedInfo?.signatures if (apkSignatures == null || installedSignatures == null) { - OneKeyLog.error("AppUpdate", "verifyAPK: legacy signatures unavailable") - if (!debugBuild) throw Exception("SIGNATURE_UNAVAILABLE") - OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring unavailable signatures") + OneKeyLog.info("AppUpdate", "verifyAPK: legacy signatures unavailable (apkSignatures=${apkSignatures != null}, installedSignatures=${installedSignatures != null}), skipping signing certificate check") } else { OneKeyLog.info("AppUpdate", "verifyAPK: APK signatures count=${apkSignatures.size}, installed signatures count=${installedSignatures.size}") if (apkSignatures.toSet() != installedSignatures.toSet()) { diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index b14c6880..b107d006 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.67", + "version": "3.0.68", "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 c38d7ebe..466c4216 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.67", + "version": "3.0.68", "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 830e6e59..88489075 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.67", + "version": "3.0.68", "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-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index 4f1307c2..07ec40b3 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.67", + "version": "3.0.68", "description": "react-native-bundle-crypto", "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 ac10c0c6..220d96fa 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.67", + "version": "3.0.68", "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 7845deaa..10c7c89f 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.67", + "version": "3.0.68", "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 5fcf56b1..6557c48a 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.67", + "version": "3.0.68", "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 cc281584..9d8d6a3b 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.67", + "version": "3.0.68", "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 c6a2edf4..a17ec499 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.67", + "version": "3.0.68", "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 24af9a37..1f0e86ba 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.67", + "version": "3.0.68", "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 d9db4103..a012203a 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.67", + "version": "3.0.68", "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 400ff14a..9642cf85 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.67", + "version": "3.0.68", "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 cabe0a35..e70d6ef5 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.67", + "version": "3.0.68", "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 6cc4891a..ab7ed85f 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.67", + "version": "3.0.68", "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 e47db444..0910ade1 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.67", + "version": "3.0.68", "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 b3f6e194..8a4ec9e4 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.67", + "version": "3.0.68", "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 c3d53dab..2c8868e7 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.67", + "version": "3.0.68", "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 8f14bb29..1b2c4577 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.67", + "version": "3.0.68", "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-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index 8c1399a2..9dbc5e42 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.67", + "version": "3.0.68", "description": "react-native-range-downloader", "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 78728eeb..140665e5 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.67", + "version": "3.0.68", "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 a181f5b9..4a5ca3aa 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.67", + "version": "3.0.68", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index 4bbcf4ee..c7df46b8 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.67", + "version": "3.0.68", "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 48b6dd3a..ee33b7f9 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.67", + "version": "3.0.68", "description": "react-native-zip-archive Nitro HybridObject 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 72f13c69..84616f60 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.67", + "version": "3.0.68", "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-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index 161f8bbd..a958bec5 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.67", + "version": "3.0.68", "description": "react-native-chart-webview", "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 5faa05d9..6fbc3b71 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.67", + "version": "3.0.68", "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-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index 99a6ae37..8d947b7b 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.67", + "version": "3.0.68", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index ac8376f0..82400910 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.67", + "version": "3.0.68", "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-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index 62f3d2a4..321974e1 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.67", + "version": "3.0.68", "description": "react-native-segment-slider", "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 640a3d47..5f6073ad 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.67", + "version": "3.0.68", "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/android/src/main/java/com/rcttabview/RCTTabViewManager.kt b/native-views/react-native-tab-view/android/src/main/java/com/rcttabview/RCTTabViewManager.kt index f83f5050..79bfa4b2 100644 --- a/native-views/react-native-tab-view/android/src/main/java/com/rcttabview/RCTTabViewManager.kt +++ b/native-views/react-native-tab-view/android/src/main/java/com/rcttabview/RCTTabViewManager.kt @@ -193,6 +193,9 @@ class RCTTabViewManager(context: ReactApplicationContext) : } // iOS-only props (no-ops on Android) + // selectedIcons feeds UITabBarItem.selectedImage on iOS; Android keeps + // swapping the icon in JS via the focusedKey-driven `icons` array. + override fun setSelectedIcons(view: ReactBottomNavigationView?, value: ReadableArray?) {} override fun setTranslucent(view: ReactBottomNavigationView?, value: Boolean) {} override fun setSidebarAdaptable(view: ReactBottomNavigationView?, value: Boolean) {} override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) {} diff --git a/native-views/react-native-tab-view/ios/RCTTabViewComponentView.mm b/native-views/react-native-tab-view/ios/RCTTabViewComponentView.mm index b3cc9753..6ccad7e0 100644 --- a/native-views/react-native-tab-view/ios/RCTTabViewComponentView.mm +++ b/native-views/react-native-tab-view/ios/RCTTabViewComponentView.mm @@ -239,6 +239,19 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & [_containerView setValue:iconsArray forKey:@"icons"]; } + if (oldViewProps.selectedIcons != newViewProps.selectedIcons) { + auto selectedIconsArray = [[NSMutableArray alloc] init]; + for (auto &source: newViewProps.selectedIcons) { + NSMutableDictionary *iconDict = [NSMutableDictionary dictionary]; + iconDict[@"uri"] = [NSString stringWithUTF8String:source.uri.c_str()]; + iconDict[@"width"] = @(source.size.width); + iconDict[@"height"] = @(source.size.height); + iconDict[@"scale"] = @(source.scale); + [selectedIconsArray addObject:iconDict]; + } + [_containerView setValue:selectedIconsArray forKey:@"selectedIcons"]; + } + if (oldViewProps.sidebarAdaptable != newViewProps.sidebarAdaptable) { [_containerView setValue:@(newViewProps.sidebarAdaptable) forKey:@"sidebarAdaptable"]; } diff --git a/native-views/react-native-tab-view/ios/RCTTabViewContainerView.swift b/native-views/react-native-tab-view/ios/RCTTabViewContainerView.swift index 119453f7..45058791 100644 --- a/native-views/react-native-tab-view/ios/RCTTabViewContainerView.swift +++ b/native-views/react-native-tab-view/ios/RCTTabViewContainerView.swift @@ -56,6 +56,11 @@ class RCTTabViewContainerView: UIView { private var childViews: [UIView] = [] private var bottomAccessoryView: UIView? private var loadedIcons: [Int: UIImage] = [:] + // Solid/focused icons shown for the selected tab via UITabBarItem.selectedImage. + // Loaded once and swapped natively by UIKit on selection, so the focused + // artwork rides the system selection animation instead of waiting for a JS + // round-trip + async reload. + private var loadedSelectedIcons: [Int: UIImage] = [:] private var imageLoader: RCTImageLoaderProtocol? private let iconSize = CGSize(width: 27, height: 27) private var longPressHandler: LongPressGestureHandler? @@ -76,6 +81,10 @@ class RCTTabViewContainerView: UIView { didSet { loadIconsFromSources() } } + @objc var selectedIcons: [NSDictionary]? { + didSet { loadSelectedIconsFromSources() } + } + @objc var labeled: NSNumber? { didSet { rebuildViewControllers() } } @@ -109,11 +118,13 @@ class RCTTabViewContainerView: UIView { } @objc var activeTintColor: UIColor? { - didSet { updateTintColors() } + // Rebuild so the baked selectedImage tint follows light/dark theme changes. + didSet { updateTintColors(); rebuildViewControllers() } } @objc var inactiveTintColor: UIColor? { - didSet { updateTabBarAppearance() } + // Rebuild so the baked unselected-image tint follows theme changes. + didSet { updateTabBarAppearance(); rebuildViewControllers() } } @objc var rippleColor: UIColor? // Android only @@ -291,6 +302,7 @@ class RCTTabViewContainerView: UIView { // Configure tab bar item let icon = loadedIcons[originalIndex] + let selectedIcon = loadedSelectedIcons[originalIndex] let sfSymbol = tabData.sfSymbol ?? "" var tabImage: UIImage? @@ -300,9 +312,29 @@ class RCTTabViewContainerView: UIView { tabImage = UIImage(systemName: sfSymbol) } + // Focused (solid) artwork for the selected state. Fall back to the + // unfocused image when no selected icon was provided so the selected + // tab still renders something. + var selectedTabImage: UIImage? + if let selectedIcon { + selectedTabImage = selectedIcon + } else if !sfSymbol.isEmpty { + selectedTabImage = UIImage(systemName: sfSymbol) + } + + // Bake the inactive/active colors directly into the images as + // .alwaysOriginal. iOS 26 Liquid Glass ignores appearance.normal.iconColor + // and washes every icon to a single near-white tint, which is why the + // unselected tabs never render the gray `iconSubdued`. Pre-tinting the + // artwork and marking it .alwaysOriginal stops the system from re-tinting, + // restoring the gray(unselected) → active(selected) color change natively. + let normalImage = bakeTint(tabImage, inactiveTintColor) + let selectedImage = bakeTint(selectedTabImage ?? tabImage, activeTintColor) + let isLabeled = labeled?.boolValue ?? true let title = isLabeled ? tabData.title : nil - vc.tabBarItem = UITabBarItem(title: title, image: tabImage, tag: originalIndex) + vc.tabBarItem = UITabBarItem(title: title, image: normalImage, selectedImage: selectedImage) + vc.tabBarItem.tag = originalIndex vc.tabBarItem.badgeValue = tabData.badge vc.tabBarItem.accessibilityIdentifier = tabData.testID @@ -359,6 +391,21 @@ class RCTTabViewContainerView: UIView { let filtered = filteredItems guard let index = filtered.firstIndex(where: { $0.key == selectedPage }) else { return } + // Common case: the user tapped the tab. UIKit's delegate already selected + // it optimistically and started the iOS 26 Liquid Glass capsule animation. + // Re-assigning selectedIndex here — especially wrapped in + // performWithoutAnimation — would cancel that in-flight animation, which is + // what made the icon/color change lag a beat behind the glass. When the + // selection is already correct, just refresh tint and leave the native + // animation alone. + if tbc.selectedIndex == index { + updateTintColors() + return + } + + // Programmatic selection (deep link, external navigation, preventsDefault + // tabs): keep the existing no-animation behavior to avoid the historically + // reverted tab-switch animation regression. if disablePageAnimations { UIView.performWithoutAnimation { tbc.selectedIndex = index @@ -525,11 +572,25 @@ class RCTTabViewContainerView: UIView { } } + // MARK: - Icon tinting + + /// Returns a copy of `image` recolored to `color` and marked + /// `.alwaysOriginal`, so iOS 26 Liquid Glass won't override the icon color. + /// Falls back to the untinted image when no color is provided. + private func bakeTint(_ image: UIImage?, _ color: UIColor?) -> UIImage? { + guard let image else { return nil } + guard let color else { return image } + return image + .withRenderingMode(.alwaysTemplate) + .withTintColor(color, renderingMode: .alwaysOriginal) + } + // MARK: - Icon loading @objc func setImageLoader(_ loader: RCTImageLoaderProtocol) { self.imageLoader = loader loadIconsFromSources() + loadSelectedIconsFromSources() } private func loadIconsFromSources() { @@ -566,6 +627,40 @@ class RCTTabViewContainerView: UIView { } } + private func loadSelectedIconsFromSources() { + guard let imageLoader, let iconDicts = selectedIcons else { return } + + let iconSources = iconDicts.map { IconSource(dict: $0) } + + for (index, source) in iconSources.enumerated() { + guard !source.uri.isEmpty else { continue } + + let url = URL(string: source.uri) + guard let url else { continue } + + let request = URLRequest(url: url) + let size = CGSize(width: source.width, height: source.height) + let scale = CGFloat(source.scale) + + imageLoader.loadImage( + with: request, + size: size, + scale: scale, + clipped: true, + resizeMode: RCTResizeMode.contain, + progressBlock: { _, _ in }, + partialLoad: { _ in }, + completionBlock: { [weak self] error, image in + if error != nil { return } + guard let image, let self else { return } + DispatchQueue.main.async { + self.loadedSelectedIcons[index] = image.resizeImageTo(size: self.iconSize) + self.rebuildViewControllers() + } + }) + } + } + // MARK: - Tab delegate callbacks func handleTabSelected(at index: Int) { diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index d6111f47..127b8f8c 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.67", + "version": "3.0.68", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-tab-view/src/TabView.tsx b/native-views/react-native-tab-view/src/TabView.tsx index c990391d..ad7afed1 100644 --- a/native-views/react-native-tab-view/src/TabView.tsx +++ b/native-views/react-native-tab-view/src/TabView.tsx @@ -260,7 +260,22 @@ const TabView = ({ setLoaded((loaded) => [...loaded, focusedKey]); } - const icons = React.useMemo( + const isIOS = Platform.OS === 'ios'; + + // Unfocused (outline) icons — independent of focusedKey so the array stays + // stable across tab switches and the native side never reloads on switch. + const unfocusedIcons = React.useMemo( + () => trimmedRoutes.map((route) => getIcon({ route, focused: false })), + [getIcon, trimmedRoutes] + ); + // Focused (solid) icons — also stable; handed to iOS as selectedImage. + const focusedIcons = React.useMemo( + () => trimmedRoutes.map((route) => getIcon({ route, focused: true })), + [getIcon, trimmedRoutes] + ); + // Legacy single array driven by focusedKey. Android still swaps the icon in + // JS (it has no selectedImage equivalent). + const focusKeyedIcons = React.useMemo( () => trimmedRoutes.map((route) => getIcon({ @@ -271,6 +286,11 @@ const TabView = ({ [focusedKey, getIcon, trimmedRoutes] ); + // iOS hands both states to UIKit (image + selectedImage) so the focused + // artwork swaps natively, in sync with the selection animation, instead of + // waiting for a JS round-trip. Android keeps the focusedKey-driven array. + const icons = isIOS ? unfocusedIcons : focusKeyedIcons; + const items: TabViewItems[number][] = React.useMemo( () => trimmedRoutes.map((route, index) => { @@ -336,6 +356,26 @@ const TabView = ({ [icons] ); + // Focused (solid) assets passed to iOS as selectedImage. iOS only — Android + // ignores selectedIcons and keeps swapping via the focusedKey array above. + const resolvedSelectedIconAssets = React.useMemo( + () => + focusedIcons.map((icon) => { + if (icon && !isAppleSymbol(icon)) { + // @ts-ignore - resolveAssetSource accepts ImageSourcePropType + const resolved = Image.resolveAssetSource(icon); + return { + uri: resolved?.uri ?? '', + width: resolved?.width ?? 0, + height: resolved?.height ?? 0, + scale: resolved?.scale ?? 1, + }; + } + return { uri: '', width: 0, height: 0, scale: 1 }; + }), + [focusedIcons] + ); + const jumpTo = useLatestCallback((key: string) => { const index = trimmedRoutes.findIndex((route) => route.key === key); if (index === -1) { @@ -394,6 +434,9 @@ const TabView = ({ style={styles.fullWidth} items={items} icons={renderCustomTabBar ? undefined : resolvedIconAssets} + selectedIcons={ + isIOS && !renderCustomTabBar ? resolvedSelectedIconAssets : undefined + } selectedPage={focusedKey} tabBarHidden={props.tabBarHidden ?? !!renderCustomTabBar} onTabLongPress={handleTabLongPress} diff --git a/native-views/react-native-tab-view/src/TabViewNativeComponent.ts b/native-views/react-native-tab-view/src/TabViewNativeComponent.ts index d2f745a1..b9044262 100644 --- a/native-views/react-native-tab-view/src/TabViewNativeComponent.ts +++ b/native-views/react-native-tab-view/src/TabViewNativeComponent.ts @@ -44,6 +44,7 @@ export interface TabViewProps extends ViewProps { onTabBarMeasured?: DirectEventHandler; onNativeLayout?: DirectEventHandler; icons?: ReadonlyArray; + selectedIcons?: ReadonlyArray; tabBarHidden?: boolean; labeled?: boolean; sidebarAdaptable?: boolean;