From fbdbf4ad6b30351e53b2643aa05cd5db907256f2 Mon Sep 17 00:00:00 2001 From: React Native Bot Date: Tue, 5 May 2026 10:22:52 -0700 Subject: [PATCH 001/548] Add changelog for v0.85.3 (#56685) Summary: Add Changelog for 0.85.3 ## Changelog: [Internal] - Add Changelog for 0.85.3 Pull Request resolved: https://github.com/facebook/react-native/pull/56685 Test Plan: N/A Reviewed By: cortinico Differential Revision: D103860564 Pulled By: fabriziocucci fbshipit-source-id: d7c3cef6a1b4fa29d00884b4349fb6dc687df3ce --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1f6978618..3bdee390e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## v0.85.3 + +### Changed + +- **React Native DevTools**: Update debugger-frontend from 8edd9be...194d3f8 ([9966cbdf4d](https://github.com/facebook/react-native/commit/9966cbdf4da99ee036a75bec4da9bb2e1ee7a9c4) by [@motiz88](https://github.com/motiz88)) + +### Fixed + +- **Build**: Use pinned Hermes version when version.properties is not 1000.0.0 ([5e3a3ba995](https://github.com/facebook/react-native/commit/5e3a3ba995026b42c2ea0ad7ee4758fcf73005e6) by [@cipolleschi](https://github.com/cipolleschi)) + +#### iOS specific + +- **Build**: Fix silent tar extraction failure on EdenFS in replace-rncore-version.js by extracting to a temp directory first ([9bc7d38be0](https://github.com/facebook/react-native/commit/9bc7d38be0771ae4006206fcb174956f53adf508) by [@motiz88](https://github.com/motiz88)) +- **Build**: Skip prebuilds for DynamicFrameworks CI jobs ([753c19bea4](https://github.com/facebook/react-native/commit/753c19bea4ab854eb8000b2b5bab814261a46596) by [@motiz88](https://github.com/motiz88)) + ## v0.85.2 ### Added From 0d20210fbe98b8b6cbe1b4d8831585fabbc457bc Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Tue, 5 May 2026 14:59:29 -0700 Subject: [PATCH 002/548] clear up props registry in shared backend on js thread (#56677) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56677 ## Changelog: [Internal] [Fixed] - clear up props registry in shared backend on js thread stopSurface is usually called from main thread, but `animatedPropsRegistry_->getMap` in animationBackendCommitHook is called from js thread. Removing the surface props entry on ui thread can cause race condition (getMap() result on js thread becomes undefined) https://www.internalfb.com/code/fbsource/[26d70084020e]/xplat/js/react-native-github/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackendCommitHook.cpp?lines=26-31 Reviewed By: christophpurrer Differential Revision: D103752709 fbshipit-source-id: b779237cce7eb22d5da608232361909a0fa9628a --- .../react/renderer/uimanager/UIManager.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 3e48dabc6ff..b0760e64438 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -275,10 +275,6 @@ ShadowTree::Unique UIManager::stopSurface(SurfaceId surfaceId) const { // Stop any ongoing layout animations. stopSurfaceForAnimationDelegate(surfaceId); - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { - animationBackend_->clearRegistryOnSurfaceStop(surfaceId); - } - // Waiting for all concurrent commits to be finished and unregistering the // `ShadowTree`. auto shadowTree = getShadowTreeRegistry().remove(surfaceId); @@ -295,6 +291,18 @@ ShadowTree::Unique UIManager::stopSurface(SurfaceId surfaceId) const { leakChecker_->stopSurface(surfaceId); } } + + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + runtimeExecutor_( + [surfaceId, + animationBackendWeak = std::weak_ptr( + animationBackend_)](jsi::Runtime& /*runtime*/) { + if (auto animationBackend = animationBackendWeak.lock()) { + animationBackend->clearRegistryOnSurfaceStop(surfaceId); + } + }); + } + return shadowTree; } From c33582bca086d1ad55079791259f2e6e6888afff Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Tue, 5 May 2026 14:59:29 -0700 Subject: [PATCH 003/548] Add animationBackend_ nullcheck before calling clearRegistry() (#56679) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56679 ## Changelog: [Internal] [Fixed] - Add animationBackend_ nullcheck before calling clearRegistry() Reviewed By: christophpurrer Differential Revision: D103759679 fbshipit-source-id: f42a19071570fea62260a7472e487108dfd64c69 --- .../ReactCommon/react/renderer/uimanager/UIManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index b0760e64438..bce35dd6fa2 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -210,7 +210,8 @@ void UIManager::completeSurface( // after we commit a specific one. lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision(surfaceId); - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (ReactNativeFeatureFlags::useSharedAnimatedBackend() && + animationBackend_) { animationBackend_->clearRegistry(surfaceId); } } From c4f94c7054a873779e45c88c81b9fb47b040e51c Mon Sep 17 00:00:00 2001 From: Panos Vekris Date: Tue, 5 May 2026 17:45:15 -0700 Subject: [PATCH 004/548] Deploy 0.312.1 to xplat (#56681) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56681 [changelog](https://github.com/facebook/flow/blob/main/Changelog.md) Changelog: [Internal] Reviewed By: gkz Differential Revision: D103747946 fbshipit-source-id: dcb0e90a9e6532a4a81fcce4971202affd1b4072 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index c9eefdf2952..b166203831f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -95,4 +95,4 @@ untyped-import untyped-type-import [version] -^0.312.0 +^0.312.1 diff --git a/package.json b/package.json index 22804684b8d..8b2f95a3f86 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint-plugin-relay": "^1.8.3", "fb-dotslash": "0.5.8", "flow-api-translator": "0.36.0", - "flow-bin": "^0.312.0", + "flow-bin": "^0.312.1", "hermes-eslint": "0.36.0", "hermes-transform": "0.36.0", "ini": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index a7335eb5357..9a123f0b29c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4749,10 +4749,10 @@ flow-api-translator@0.36.0: hermes-transform "0.36.0" typescript "5.3.2" -flow-bin@^0.312.0: - version "0.312.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.312.0.tgz#4962c29a740beff72eb4b89b0f9625213b3a82d8" - integrity sha512-SVjNNbWt8d69cj6dLXy5P8pq3p6dpje8cL3PvPEqY2KoloiYdpxjOOEZTFvliZV7gxKzGB83iGLA1ct6TH/7hw== +flow-bin@^0.312.1: + version "0.312.1" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.312.1.tgz#727f40d94b319ad418e97e45a729860b641cee79" + integrity sha512-jIo4SzaqMpnj1SjTPg8+QHPFOs6RY98mbzGw7uRJn6fHfaqly9hM30n5PwAhRPk8F5LFawNteeW3IOtr/7UJ3A== flow-enums-runtime@^0.0.6: version "0.0.6" From 5b3a1a48aa71d1d392b4decee23cb7a641a81546 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 6 May 2026 05:20:57 -0700 Subject: [PATCH 005/548] Return actual loaded bundle URL from SourceCodeModule (#56698) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56698 The C++ `SourceCodeModule` (ReactCxxPlatform) always derived `scriptURL` from `DevServerHelper::getBundleUrl()`, coupling the module to dev infrastructure. This diff replaces that dependency with a simple `std::string` parameter passed at construction time. `ReactHost` and `FoxReactHost` store the source URL in a `shared_ptr` that is updated on each successful dev-server bundle load. When `SourceCodeModule` is instantiated (lazily, after the bundle is loaded), the TurboModule provider dereferences the current value and passes it through as a plain string. For file-based bundle loading the URL is cleared, matching the previous behavior. This decouples `SourceCodeModule` from `DevServerHelper` without introducing lambdas, context container lookups, or shared mutable state in the module itself. Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D102669868 fbshipit-source-id: 02aec01d22e6ea429431e079ea29bf63246530ae --- .../react/devsupport/SourceCodeModule.cpp | 9 +-------- .../react/devsupport/SourceCodeModule.h | 10 +++------- .../react/runtime/ReactCxxTurboModuleProvider.cpp | 9 ++++++--- .../react/runtime/ReactCxxTurboModuleProvider.h | 4 +++- .../ReactCxxPlatform/react/runtime/ReactHost.cpp | 5 ++++- .../ReactCxxPlatform/react/runtime/ReactHost.h | 1 + 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp index ba2bc8d3df3..d7ccc105142 100644 --- a/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp @@ -7,17 +7,10 @@ #include "SourceCodeModule.h" -#include -#include - namespace facebook::react { SourceCodeConstants SourceCodeModule::getConstants(jsi::Runtime& /*rt*/) { - std::string scriptURL; - if (auto devServerHelper = devServerHelper_.lock()) { - scriptURL = devServerHelper->getBundleUrl(); - } - return SourceCodeConstants{.scriptURL = scriptURL}; + return SourceCodeConstants{.scriptURL = sourceURL_}; } } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h index 9582cba32e0..7924c5ad1b2 100644 --- a/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/SourceCodeModule.h @@ -13,8 +13,6 @@ namespace facebook::react { -class DevServerHelper; - using SourceCodeConstants = NativeSourceCodeSourceCodeConstants; template <> @@ -22,17 +20,15 @@ struct Bridging : NativeSourceCodeSourceCodeConstantsBridgi class SourceCodeModule : public NativeSourceCodeCxxSpec { public: - explicit SourceCodeModule( - std::shared_ptr jsInvoker, - std::shared_ptr devServerHelper = nullptr) - : NativeSourceCodeCxxSpec(jsInvoker), devServerHelper_(devServerHelper) + explicit SourceCodeModule(std::shared_ptr jsInvoker, std::string sourceURL = "") + : NativeSourceCodeCxxSpec(jsInvoker), sourceURL_(std::move(sourceURL)) { } SourceCodeConstants getConstants(jsi::Runtime &rt); private: - std::weak_ptr devServerHelper_; + std::string sourceURL_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp index b86ddd2c0ce..ab2af66e29e 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp @@ -38,7 +38,8 @@ ReactCxxTurboModuleProvider::ReactCxxTurboModuleProvider( std::shared_ptr logBoxSurfaceDelegate, HttpClientFactory httpClientFactory, WebSocketClientFactory webSocketClientFactory, - std::function liveReloadCallback) + std::function liveReloadCallback, + std::shared_ptr sourceURL) : turboModuleProviders_(std::move(turboModuleProviders)), jsInvoker_(std::move(jsInvoker)), onJsError_(std::move(onJsError)), @@ -48,7 +49,8 @@ ReactCxxTurboModuleProvider::ReactCxxTurboModuleProvider( logBoxSurfaceDelegate_(std::move(logBoxSurfaceDelegate)), httpClientFactory_(std::move(httpClientFactory)), webSocketClientFactory_(std::move(webSocketClientFactory)), - liveReloadCallback_(std::move(liveReloadCallback)) {} + liveReloadCallback_(std::move(liveReloadCallback)), + sourceURL_(std::move(sourceURL)) {} std::shared_ptr ReactCxxTurboModuleProvider::operator()( const std::string& name) const { @@ -82,7 +84,8 @@ std::shared_ptr ReactCxxTurboModuleProvider::operator()( } else if (name == ImageLoaderModule::kModuleName) { return std::make_shared(jsInvoker_); } else if (name == SourceCodeModule::kModuleName) { - return std::make_shared(jsInvoker_, devServerHelper_); + return std::make_shared( + jsInvoker_, sourceURL_ ? *sourceURL_ : ""); } else if (name == WebSocketModule::kModuleName) { return std::make_shared( jsInvoker_, webSocketClientFactory_); diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h b/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h index c031fbb5b61..cb2c6e7edcd 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h @@ -33,7 +33,8 @@ class ReactCxxTurboModuleProvider final { std::shared_ptr logBoxSurfaceDelegate = nullptr, HttpClientFactory httpClientFactory = nullptr, WebSocketClientFactory webSocketClientFactory = nullptr, - std::function liveReloadCallback = nullptr); + std::function liveReloadCallback = nullptr, + std::shared_ptr sourceURL = nullptr); std::shared_ptr operator()(const std::string &name) const; @@ -48,6 +49,7 @@ class ReactCxxTurboModuleProvider final { HttpClientFactory httpClientFactory_; WebSocketClientFactory webSocketClientFactory_; std::function liveReloadCallback_; + std::shared_ptr sourceURL_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index 60fde138554..a770ba8196b 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -261,7 +261,8 @@ void ReactHost::createReactInstance() { reactInstanceData_->logBoxSurfaceDelegate, httpClientFactory, webSocketClientFactory, - std::move(liveReloadCallback)); + std::move(liveReloadCallback), + sourceURL_); reactInstance_->initializeRuntime( { @@ -392,6 +393,7 @@ bool ReactHost::loadScriptFromDevServer() { }) .get(); auto script = std::make_unique(std::move(response)); + *sourceURL_ = bundleUrl; reactInstance_->loadScript(std::move(script), bundleUrl); devServerHelper_->setupHMRClient(); return true; @@ -408,6 +410,7 @@ bool ReactHost::loadScriptFromBundlePath(const std::string& bundlePath) { try { LOG(INFO) << "Loading JS bundle from bundle path: " << bundlePath; auto script = ResourceLoader::getFileContents(bundlePath); + *sourceURL_ = ""; reactInstance_->loadScript(std::move(script), bundlePath); LOG(INFO) << "Loaded JS bundle from bundle path: " << bundlePath; return true; diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h index 47645e4a119..9ff4a1d6894 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.h @@ -110,6 +110,7 @@ class ReactHost { std::unique_ptr surfaceManager_; std::shared_ptr devServerHelper_; + std::shared_ptr sourceURL_ = std::make_shared(); std::shared_ptr inspector_; std::unique_ptr packagerConnection_; From 882be91144b1fc8161096d6a2745e7427eea8d82 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 6 May 2026 05:33:57 -0700 Subject: [PATCH 006/548] Auto-reconnect PackagerConnection to Metro dev server (#56625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56625 When MWA starts before Metro is running, PackagerConnection's WebSocket connection to Metro's `/message` endpoint silently fails with no retry. The developer must restart MWA after starting Metro to get HMR working. This diff adds reconnection logic to PackagerConnection so it automatically retries when Metro becomes available. On initial connect failure or WebSocket disconnect, PackagerConnection schedules a retry after 5 seconds using the existing `WebSocketClientFactory` to create a fresh client. On successful reconnect, it fires `liveReloadCallback_()` which triggers `FoxReactHost::reloadReactInstance()` to load the bundle from Metro and set up HMR. The reconnection is event-driven: each retry is a short-lived thread that sleeps 5 seconds then calls `attemptConnection()`. The connect callback handles success/failure — no long-lived polling thread. The `active_` flag is set to false in the destructor to stop retries during shutdown. Combined with D97570592's push-based route invalidation, this completes the dev loop: Metro can start at any time, PackagerConnection auto-reconnects, HMR keeps the bundle fresh, and route changes propagate automatically. Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D97823787 fbshipit-source-id: b195b8f67713e690279f4f96fb95264b125630c4 --- .../react/devsupport/PackagerConnection.cpp | 65 ++++++++++++++++--- .../react/devsupport/PackagerConnection.h | 14 +++- .../react/runtime/ReactHost.cpp | 1 - 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp index 05676162f68..d17bae6ce7d 100644 --- a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp @@ -9,22 +9,40 @@ #include #include -#include namespace facebook::react { PackagerConnection::PackagerConnection( - const WebSocketClientFactory& webSocketClientFactory, - const std::string& packagerConnectionUrl, + WebSocketClientFactory webSocketClientFactory, + std::string packagerConnectionUrl, LiveReloadCallback&& liveReloadCallback, ShowDevMenuCallback&& showDevMenuCallback) - : liveReloadCallback_(std::move(liveReloadCallback)), + : webSocketClientFactory_(std::move(webSocketClientFactory)), + packagerConnectionUrl_(std::move(packagerConnectionUrl)), + liveReloadCallback_(std::move(liveReloadCallback)), showDevMenuCallback_(std::move(showDevMenuCallback)) { - websocket_ = webSocketClientFactory(); + attemptConnection(); +} + +PackagerConnection::~PackagerConnection() noexcept { + reconnectThread_.quit(); + if (websocket_) { + websocket_->setOnClosedCallback(nullptr); + websocket_->setOnMessageCallback(nullptr); + websocket_->close("PackagerConnection destroyed"); + } +} + +void PackagerConnection::attemptConnection() { + if (websocket_) { + websocket_->setOnClosedCallback(nullptr); + websocket_->close("reconnecting"); + } + websocket_ = webSocketClientFactory_(); websocket_->setOnMessageCallback([this](const std::string& message) { LOG(INFO) << "Received message from packager: " << message; - auto json = nlohmann::json::parse(message); - if (json.is_null() || json["version"] != 2) { + auto json = nlohmann::json::parse(message, nullptr, false); + if (json.is_discarded() || json.is_null() || json["version"] != 2) { return; } auto method = json["method"]; @@ -34,11 +52,38 @@ PackagerConnection::PackagerConnection( showDevMenuCallback_(); } }); - websocket_->connect(packagerConnectionUrl); + websocket_->setOnClosedCallback([this](const std::string& reason) { + LOG(INFO) << "PackagerConnection closed: " << reason; + scheduleReconnect(); + }); + websocket_->connect( + packagerConnectionUrl_, + [this](bool success, const std::string& /*error*/) { + if (success) { + if (!isInitialConnection_) { + LOG(INFO) + << "PackagerConnection connected to Metro - triggering live reload"; + liveReloadCallback_(); + } else { + LOG(INFO) << "PackagerConnection connected to Metro"; + } + } else { + scheduleReconnect(); + } + isInitialConnection_ = false; + }); } -PackagerConnection::~PackagerConnection() noexcept { - websocket_->close("PackagerConnection destroyed"); +void PackagerConnection::scheduleReconnect() { + if (reconnectPending_.exchange(true)) { + return; + } + reconnectThread_.runAsync( + [this]() { + reconnectPending_ = false; + attemptConnection(); + }, + std::chrono::milliseconds(5000)); } } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h index eee5149fd43..7e1596c0f2b 100644 --- a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h @@ -8,6 +8,8 @@ #pragma once #include +#include +#include #include #include #include @@ -20,8 +22,8 @@ class PackagerConnection { public: PackagerConnection( - const WebSocketClientFactory &webSocketClientFactory, - const std::string &packagerConnectionUrl, + WebSocketClientFactory webSocketClientFactory, + std::string packagerConnectionUrl, LiveReloadCallback &&liveReloadCallback, ShowDevMenuCallback &&showDevMenuCallback); ~PackagerConnection() noexcept; @@ -31,9 +33,17 @@ class PackagerConnection { PackagerConnection &operator=(PackagerConnection &&other) = delete; private: + void attemptConnection(); + void scheduleReconnect(); + + const WebSocketClientFactory webSocketClientFactory_; + const std::string packagerConnectionUrl_; const LiveReloadCallback liveReloadCallback_; const ShowDevMenuCallback showDevMenuCallback_; std::unique_ptr websocket_; + std::atomic isInitialConnection_{true}; + std::atomic reconnectPending_{false}; + TaskDispatchThread reconnectThread_{"PackagerReconnect"}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index a770ba8196b..8c9e0f4987f 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -398,7 +398,6 @@ bool ReactHost::loadScriptFromDevServer() { devServerHelper_->setupHMRClient(); return true; } catch (...) { - devServerHelper_->setSourcePath(""); LOG(WARNING) << "Unable to download JS bundle from Metro, falling back to prebuilt JS bundle. " << "To start Metro, run in command line: 'cd ~/fbsource/xplat/js && js1 run'"; From ca83ef3c04d63456e9e6ac76d539518aa82a7120 Mon Sep 17 00:00:00 2001 From: Andrew Datsenko Date: Wed, 6 May 2026 06:32:31 -0700 Subject: [PATCH 007/548] Introduce StatefulSpan and MutableSpannableLayout for safe span state isolation (#56688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56688 `PreparedLayoutTextView` can receive shared `PreparedLayout` instances from a C++ LRU cache. Multiple views rendering identical text share the same `Layout` → `Spannable` → span objects. Stateless spans are fine to share, but stateful spans like `SpoilerEffectSpan` (particles, dismiss state) would have one view's tap-dismiss corrupt all other views sharing that layout. Introduces a `StatefulSpan` marker interface whose presence tells `PreparedLayoutTextView` to clone the spannable with fresh span instances. The Layout is reused via a delegating subclass (`MutableSpannableLayout`) that passes the cloned Spannable to `Layout`'s protected constructor and delegates all line metrics to the original. The mutable Spannable can be useful for spans which affect display, but do not alter existing layout calculations. No StaticLayout rebuild needed. Performance: Only views with stateful spans pay the cost. `getSpans()` is O(spans), `SpannableString` copy is O(text+spans). Views without `StatefulSpan` are completely unchanged. Changelog: [internal] Reviewed By: alanleedev Differential Revision: D97415850 fbshipit-source-id: fd570daf839a299d2d12617a9fc2dbd57f1e4049 --- .../views/text/MutableSpannableLayout.kt | 107 ++++++++++++++++++ .../views/text/PreparedLayoutTextView.kt | 29 ++++- .../views/text/internal/span/StatefulSpan.kt | 21 ++++ 3 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/MutableSpannableLayout.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/MutableSpannableLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/MutableSpannableLayout.kt new file mode 100644 index 00000000000..c4e16d8718b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/MutableSpannableLayout.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import android.text.Layout +import android.text.SpannableString +import android.text.Spanned +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.views.text.internal.span.StatefulSpan + +/** + * A delegating [Layout] subclass that clones the spannable text from [delegate] and replaces all + * [StatefulSpan] instances with fresh clones. This gives each [PreparedLayoutTextView] independent + * mutable span state (e.g. particle animation, dismiss state) even when the underlying [Layout] is + * shared from a cache. + * + * The mutable [Spannable] can be useful for spans which affect display, but do not alter existing + * layout calculations. + * + * Line metrics are delegated to [delegate] so no expensive [StaticLayout] rebuild is needed. + * [Layout.getText] is final and returns `mText` set by the protected constructor, so the cloned + * [SpannableString] is passed there directly. + */ +internal class MutableSpannableLayout +private constructor( + private val delegate: Layout, + clonedText: SpannableString, +) : + Layout( + clonedText, + delegate.paint, + delegate.width, + delegate.alignment, + delegate.spacingMultiplier, + delegate.spacingAdd, + ) { + + companion object { + /** Returns a [MutableSpannableLayout] if [layout] contains stateful spans, else null. */ + @OptIn(UnstableReactNativeAPI::class) + fun createIfNeeded(layout: Layout): MutableSpannableLayout? { + val spanned = layout.text as? Spanned ?: return null + val statefulSpans = spanned.getSpans(0, spanned.length, StatefulSpan::class.java) + if (statefulSpans.isEmpty()) { + return null + } + + val cloned = SpannableString(spanned) + for (oldSpan in statefulSpans) { + val start = cloned.getSpanStart(oldSpan) + val end = cloned.getSpanEnd(oldSpan) + val flags = cloned.getSpanFlags(oldSpan) + cloned.removeSpan(oldSpan) + cloned.setSpan(oldSpan.clone(), start, end, flags) + } + return MutableSpannableLayout(layout, cloned) + } + } + + // --- 10 abstract methods — delegate to original --- + + override fun getLineCount(): Int = delegate.lineCount + + override fun getLineTop(line: Int): Int = delegate.getLineTop(line) + + override fun getLineDescent(line: Int): Int = delegate.getLineDescent(line) + + override fun getLineStart(line: Int): Int = delegate.getLineStart(line) + + override fun getLineContainsTab(line: Int): Boolean = delegate.getLineContainsTab(line) + + override fun getLineDirections(line: Int): Directions = delegate.getLineDirections(line) + + override fun getTopPadding(): Int = delegate.topPadding + + override fun getBottomPadding(): Int = delegate.bottomPadding + + override fun getEllipsisStart(line: Int): Int = delegate.getEllipsisStart(line) + + override fun getEllipsisCount(line: Int): Int = delegate.getEllipsisCount(line) + + override fun getParagraphDirection(line: Int): Int = delegate.getParagraphDirection(line) + + // --- Non-abstract overrides for performance/correctness --- + // StaticLayout overrides these with optimized implementations. Delegating + // ensures we get the original's fast paths rather than Layout's base + // implementations that recompute from scratch. + + override fun getEllipsizedWidth(): Int = delegate.ellipsizedWidth + + override fun getLineMax(line: Int): Float = delegate.getLineMax(line) + + override fun getLineWidth(line: Int): Float = delegate.getLineWidth(line) + + override fun getLineLeft(line: Int): Float = delegate.getLineLeft(line) + + override fun getLineRight(line: Int): Float = delegate.getLineRight(line) + + // Only called by the framework on API 33+ + @android.annotation.SuppressLint("NewApi") + override fun isFallbackLineSpacingEnabled(): Boolean = delegate.isFallbackLineSpacingEnabled +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt index 1b4e659a2a8..e3a0c122e4a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt @@ -48,10 +48,14 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re var preparedLayout: PreparedLayout? = null set(value) { if (field != value) { + val effectiveValue = value?.maybeProxyStatefulSpans() val lastSelection = selection if (lastSelection != null) { - if (value != null && field?.layout?.text.toString() == value.layout.text.toString()) { - value.layout.getSelectionPath( + if ( + effectiveValue != null && + field?.layout?.text.toString() == effectiveValue.layout.text.toString() + ) { + effectiveValue.layout.getSelectionPath( lastSelection.start, lastSelection.end, lastSelection.path, @@ -61,9 +65,10 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } } - clickableSpans = value?.layout?.text?.let { filterClickableSpans(it) } ?: emptyList() + clickableSpans = + effectiveValue?.layout?.text?.let { filterClickableSpans(it) } ?: emptyList() - field = value + field = effectiveValue invalidate() } } @@ -393,5 +398,21 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re return spans } + + /** + * If the layout contains [StatefulSpan]s, returns a new [PreparedLayout] whose spannable has + * independent clones of those spans. Otherwise returns the receiver unchanged. + */ + private fun PreparedLayout.maybeProxyStatefulSpans(): PreparedLayout { + val proxyLayout = MutableSpannableLayout.createIfNeeded(layout) ?: return this + return PreparedLayout( + proxyLayout, + maximumNumberOfLines, + verticalOffset, + reactTags, + textBreakStrategy, + justificationMode, + ) + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt new file mode 100644 index 00000000000..cc8f09284ef --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.internal.span + +import com.facebook.react.common.annotations.UnstableReactNativeAPI + +/** + * Marker interface for spans that hold per-view mutable state (e.g. animation particles, dismiss + * flags). When a [PreparedLayout] contains stateful spans, [PreparedLayoutTextView] clones the + * spannable so that each view gets independent state even when layouts are shared from a cache. + */ +@UnstableReactNativeAPI +public interface StatefulSpan { + /** Returns a fresh instance with the same configuration but independent mutable state. */ + public fun clone(): StatefulSpan +} From 97fa2a4ba7f95c5ddb7b96d74c9b831410ca4b46 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 6 May 2026 06:40:23 -0700 Subject: [PATCH 008/548] Move event dispatch logic from FabricUIManager into SurfaceMountingManager (#56659) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56659 Refactor: consolidate the event dispatch decision (enqueue vs direct dispatch) into `SurfaceMountingManager.dispatchEvent`, removing the 3-way branching from `FabricUIManager.receiveEvent`. The sync event path remains in `FabricUIManager`. This moves `getEventEmitter`, `getViewExists`, and `enqueuePendingEvent` calls behind a single `dispatchEvent` entry point, making the ordering logic fully encapsulated in `SurfaceMountingManager`. Changelog: [Internal] Reviewed By: zeyap Differential Revision: D103007280 fbshipit-source-id: 15eed04c0d936836d2fedeaff09254266c0f0528 --- .../react/fabric/FabricUIManager.java | 44 +++++-------------- .../react/fabric/mounting/MountingManager.kt | 16 +++---- .../fabric/mounting/SurfaceMountingManager.kt | 39 ++++++++++++++++ 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 9c29ee6213d..066dd9d8b29 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -1199,42 +1199,22 @@ public void receiveEvent( return; } - EventEmitterWrapper eventEmitter = mMountingManager.getEventEmitter(surfaceId, reactTag); - if (eventEmitter == null) { - if (mMountingManager.getViewExists(reactTag)) { - // The view is pre-allocated and created. However, it hasn't been mounted yet. We will have - // access to the event emitter later when the view is mounted. For now just save the event - // in the view state and trigger it later. - mMountingManager.enqueuePendingEvent( - surfaceId, - reactTag, - eventName, - canCoalesceEvent, - params, - eventCategory, - eventTimestamp); - } else { - // This can happen if the view has disappeared from the screen (because of async events) - FLog.i(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag); - } - return; - } - if (experimentalIsSynchronous) { UiThreadUtil.assertOnUiThread(); - // add() returns true only if there are no equivalent events already in the set - boolean firstEventForFrame = - mSynchronousEvents.add(new SynchronousEvent(surfaceId, reactTag, eventName)); - if (firstEventForFrame) { - eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp); - } - } else { - if (canCoalesceEvent) { - eventEmitter.dispatchUnique(eventName, params, eventTimestamp); - } else { - eventEmitter.dispatch(eventName, params, eventCategory, eventTimestamp); + EventEmitterWrapper eventEmitter = mMountingManager.getEventEmitter(surfaceId, reactTag); + if (eventEmitter != null) { + // add() returns true only if there are no equivalent events already in the set + boolean firstEventForFrame = + mSynchronousEvents.add(new SynchronousEvent(surfaceId, reactTag, eventName)); + if (firstEventForFrame) { + eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp); + } + return; } } + + mMountingManager.dispatchEvent( + surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory, eventTimestamp); } @Override diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt index aa0365037ec..60c11be0be6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt @@ -327,7 +327,7 @@ internal class MountingManager( attachmentsPositions, ) - fun enqueuePendingEvent( + fun dispatchEvent( surfaceId: Int, reactTag: Int, eventName: String, @@ -338,22 +338,16 @@ internal class MountingManager( ) { val smm = getSurfaceMountingManager(surfaceId, reactTag) if (smm == null) { - FLog.d( + FLog.i( TAG, - "Cannot queue event without valid surface mounting manager for tag: %d, surfaceId: %d", + "Unable to invoke event %s for tag [%d] in surfaceId [%d]", + eventName, reactTag, surfaceId, ) return } - smm.enqueuePendingEvent( - reactTag, - eventName, - canCoalesceEvent, - params, - eventCategory, - eventTimestamp, - ) + smm.dispatchEvent(reactTag, eventName, canCoalesceEvent, params, eventCategory, eventTimestamp) } private fun getSurfaceMountingManager(surfaceId: Int, reactTag: Int): SurfaceMountingManager? = diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 9e53dc8c365..8b2feba623c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -1241,6 +1241,45 @@ internal constructor( } } + @AnyThread + internal fun dispatchEvent( + reactTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + eventTimestamp: Long, + ) { + val viewState = registryGet(reactTag) + if (viewState == null) { + // This can happen if the view has disappeared from the screen (because of async events) + FLog.i(TAG, "Unable to invoke event: %s for reactTag: %d", eventName, reactTag) + return + } + + val eventEmitter = viewState.eventEmitter + if (eventEmitter == null) { + // The view is pre-allocated and created. However, it hasn't been mounted yet. We will have + // access to the event emitter later when the view is mounted. For now just save the event + // in the view state and trigger it later. + enqueuePendingEvent( + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + eventTimestamp, + ) + return + } + + if (canCoalesceEvent) { + eventEmitter.dispatchUnique(eventName, params, eventTimestamp) + } else { + eventEmitter.dispatch(eventName, params, eventCategory, eventTimestamp) + } + } + public fun markActiveTouchForTag(reactTag: Int): Unit { viewsWithActiveTouches.add(reactTag) } From 71a0d5da1403f680ee3208fc834152ba3d080b4f Mon Sep 17 00:00:00 2001 From: Andrew Datsenko Date: Wed, 6 May 2026 08:40:26 -0700 Subject: [PATCH 009/548] Add support for animated effects on spans of text (#56702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56702 Adds `AnimatedEffectSpan`, a new span type for animated effects drawn on top of text in `PreparedLayoutTextView`. Unlike `DrawCommandSpan` which provides static `onPreDraw`/`onDraw` hooks, `AnimatedEffectSpan` supports animation via a `requestAnimationFrame`-style API where the span receives a time delta each frame and returns whether it wants another frame. Key design decisions: - Independent from `DrawCommandSpan` — does not subclass it - Does not implement `UpdateAppearance` — animated effects don't affect text measurement or paint state - Implements `ReactSpan` for integration with RN's span management - Annotated `UnstableReactNativeAPI` — callers must opt in - Zero overhead for non-animated text: delta computation and frame scheduling only happen when animated spans exist - Frame timing resets on visibility changes and view recycling to prevent delta spikes Changelog: [Internal] Reviewed By: alanleedev Differential Revision: D97399151 fbshipit-source-id: da2015c1b49d122c522dae5c49f6c83ca1979daf --- .../views/text/PreparedLayoutTextView.kt | 45 +++++++++++++++++++ .../text/internal/span/AnimatedEffectSpan.kt | 37 +++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/AnimatedEffectSpan.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt index e3a0c122e4a..9acc47f4de1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt @@ -18,15 +18,18 @@ import android.text.Spanned import android.text.style.ClickableSpan import android.view.KeyEvent import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import androidx.annotation.ColorInt import androidx.annotation.DoNotInline import androidx.annotation.RequiresApi import androidx.core.view.ViewCompat import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.ReactCompoundView import com.facebook.react.uimanager.style.Overflow +import com.facebook.react.views.text.internal.span.AnimatedEffectSpan import com.facebook.react.views.text.internal.span.DrawCommandSpan import com.facebook.react.views.text.internal.span.ReactFragmentIndexSpan import com.facebook.react.views.text.internal.span.ReactLinkSpan @@ -44,6 +47,7 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re private var clickableSpans: List = emptyList() private var selection: TextSelection? = null + private var lastFrameTimeNanos: Long = 0L var preparedLayout: PreparedLayout? = null set(value) { @@ -99,9 +103,18 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re clickableSpans = emptyList() selection = null selectionColor = null + lastFrameTimeNanos = 0L preparedLayout = null } + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + if (visibility != VISIBLE) { + lastFrameTimeNanos = 0L + } + } + + @OptIn(UnstableReactNativeAPI::class) override fun onDraw(canvas: Canvas) { if (overflow != Overflow.VISIBLE) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas) @@ -151,6 +164,38 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re ) } } + + if (spanned != null) { + val animatedEffectSpans = + spanned.getSpans(0, spanned.length, AnimatedEffectSpan::class.java) + + if (animatedEffectSpans.isNotEmpty()) { + val now = System.nanoTime() + val deltaNanos = if (lastFrameTimeNanos == 0L) 0L else now - lastFrameTimeNanos + lastFrameTimeNanos = now + + var needsNextFrame = false + for (span in animatedEffectSpans) { + if ( + span.onDraw( + spanned.getSpanStart(span), + spanned.getSpanEnd(span), + canvas, + layout, + deltaNanos, + ) + ) { + needsNextFrame = true + } + } + + if (needsNextFrame) { + postInvalidateOnAnimation() + } else { + lastFrameTimeNanos = 0L + } + } + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/AnimatedEffectSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/AnimatedEffectSpan.kt new file mode 100644 index 00000000000..24b985f1bfd --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/AnimatedEffectSpan.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text.internal.span + +import android.graphics.Canvas +import android.text.Layout +import com.facebook.react.common.annotations.UnstableReactNativeAPI + +/** + * A span which draws an animated effect on top of text. Each frame, [onDraw] is called with the + * time since the last frame. Return true to request another frame, false to stop animating. + */ +@UnstableReactNativeAPI +public interface AnimatedEffectSpan : StatefulSpan { + /** + * Called each frame to draw an animated effect on top of text. + * + * @param start the start offset of this span within the text + * @param end the end offset of this span within the text + * @param canvas the canvas to draw on + * @param layout the text layout + * @param deltaNanos nanoseconds since the last frame, or 0 on the first frame + * @return true to request another frame, false to stop animating + */ + public fun onDraw( + start: Int, + end: Int, + canvas: Canvas, + layout: Layout, + deltaNanos: Long, + ): Boolean +} From 7ae9b664e9001e0467647506913b417967ed2c4f Mon Sep 17 00:00:00 2001 From: Andrew Datsenko Date: Wed, 6 May 2026 10:27:55 -0700 Subject: [PATCH 010/548] Rename DrawCommandSpan to CanvasEffectSpan and remove UpdateAppearance (#56705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56705 Changelog: [Internal] Renames `DrawCommandSpan` to `CanvasEffectSpan` for consistency with the new `AnimatedEffectSpan`, and removes the `UpdateAppearance` interface. `UpdateAppearance` is a marker interface that tells Android's text layout system the span affects text appearance (triggering re-measurement). Since `CanvasEffectSpan` only draws on top of text during `PreparedLayoutTextView`'s draw pass and never modifies paint state or text measurement, implementing `UpdateAppearance` was incorrect — the same reasoning applied when designing `AnimatedEffectSpan` without it. Reviewed By: mdvacca Differential Revision: D97399655 fbshipit-source-id: 3865b4143d696d519abdff9a6471dbe3a9867d6f --- .../react/views/text/PreparedLayoutTextView.kt | 10 +++++----- .../span/{DrawCommandSpan.kt => CanvasEffectSpan.kt} | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) rename packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/{DrawCommandSpan.kt => CanvasEffectSpan.kt} (73%) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt index 9acc47f4de1..9a2e1c7b7a9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt @@ -30,7 +30,7 @@ import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.ReactCompoundView import com.facebook.react.uimanager.style.Overflow import com.facebook.react.views.text.internal.span.AnimatedEffectSpan -import com.facebook.react.views.text.internal.span.DrawCommandSpan +import com.facebook.react.views.text.internal.span.CanvasEffectSpan import com.facebook.react.views.text.internal.span.ReactFragmentIndexSpan import com.facebook.react.views.text.internal.span.ReactLinkSpan import kotlin.collections.ArrayList @@ -134,11 +134,11 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } val spanned = text as? Spanned - val drawCommandSpans = - spanned?.getSpans(0, spanned.length, DrawCommandSpan::class.java) ?: emptyArray() + val canvasEffectSpans = + spanned?.getSpans(0, spanned.length, CanvasEffectSpan::class.java) ?: emptyArray() if (spanned != null) { - for (span in drawCommandSpans) { + for (span in canvasEffectSpans) { span.onPreDraw( spanned.getSpanStart(span), spanned.getSpanEnd(span), @@ -155,7 +155,7 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } if (spanned != null) { - for (span in drawCommandSpans) { + for (span in canvasEffectSpans) { span.onDraw( spanned.getSpanStart(span), spanned.getSpanEnd(span), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt similarity index 73% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt index 21d4f8cd30b..1ec5761780d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt @@ -9,13 +9,12 @@ package com.facebook.react.views.text.internal.span import android.graphics.Canvas import android.text.Layout -import android.text.style.UpdateAppearance /** - * May be overridden to implement character styles which are applied by [PreparedLayoutTextView] - * during the drawing of text, against the underlying Android canvas + * A span which draws a static effect on top of text. [onPreDraw] and [onDraw] hooks are called + * during [PreparedLayoutTextView] drawing, providing glyph layout information for custom rendering. */ -public abstract class DrawCommandSpan : UpdateAppearance, ReactSpan { +public abstract class CanvasEffectSpan { /** * Called before the text is drawn. This happens after the Paragraph component has drawn its * background, but may be called before text spans with their own background color are drawn. From f85a777cb7782dc892ce3c3eb92ca6aa9d438c44 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Wed, 6 May 2026 17:07:54 -0700 Subject: [PATCH 011/548] Fix UBSan float-cast-overflow in BridgingTest.numberTest (#56693) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56693 Changelog: [Internal] - Remove UB-relying assertion from BridgingTest.numberTest Reviewed By: shwanton Differential Revision: D103954447 fbshipit-source-id: d97205a7646f7c7b1306f22b368573423c38494d --- .../ReactCommon/react/bridging/tests/BridgingTest.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp b/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp index 8faa3979131..a2f8374ae17 100644 --- a/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp +++ b/packages/react-native/ReactCommon/react/bridging/tests/BridgingTest.cpp @@ -69,11 +69,6 @@ TEST_F(BridgingTest, numberTest) { -42, static_cast( bridging::toJs(rt, static_cast(-42)).asNumber())); - - EXPECT_FALSE( - -42 == - static_cast( - bridging::toJs(rt, static_cast(-42)).asNumber())); } TEST_F(BridgingTest, stringTest) { From dd03f889ef3f9e77fb94904c289cc233815d1c8c Mon Sep 17 00:00:00 2001 From: Panos Vekris Date: Wed, 6 May 2026 18:55:49 -0700 Subject: [PATCH 012/548] Deploy 0.313.0 to xplat (#56712) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56712 [changelog](https://github.com/facebook/flow/blob/main/Changelog.md) Changelog: [Internal] Reviewed By: SamChou19815, marcoww6 Differential Revision: D104101843 fbshipit-source-id: 57316144950b66c27884c1858645d87ce23ba633 --- .flowconfig | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.flowconfig b/.flowconfig index b166203831f..b48ff208ac6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -95,4 +95,4 @@ untyped-import untyped-type-import [version] -^0.312.1 +^0.313.0 diff --git a/package.json b/package.json index 8b2f95a3f86..23974d9f0de 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint-plugin-relay": "^1.8.3", "fb-dotslash": "0.5.8", "flow-api-translator": "0.36.0", - "flow-bin": "^0.312.1", + "flow-bin": "^0.313.0", "hermes-eslint": "0.36.0", "hermes-transform": "0.36.0", "ini": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 9a123f0b29c..bd096e8aee9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4749,10 +4749,10 @@ flow-api-translator@0.36.0: hermes-transform "0.36.0" typescript "5.3.2" -flow-bin@^0.312.1: - version "0.312.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.312.1.tgz#727f40d94b319ad418e97e45a729860b641cee79" - integrity sha512-jIo4SzaqMpnj1SjTPg8+QHPFOs6RY98mbzGw7uRJn6fHfaqly9hM30n5PwAhRPk8F5LFawNteeW3IOtr/7UJ3A== +flow-bin@^0.313.0: + version "0.313.0" + resolved "https://registry.facebook.net/flow-bin/-/flow-bin-0.313.0.tgz#d2bc31db6395f239b2787d3e427856cc8e2ffd54" + integrity sha512-foqZwKykbfdvEyWI4yIpuJU0I0d06562GogwA+8eRN7Bt/nwqepGLIrZHG6KSTcS79iyAU7ZiQvP8pA90dEhfQ== flow-enums-runtime@^0.0.6: version "0.0.6" From ba204faa75ab89150396f7ad69e755187ec47a58 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 6 May 2026 19:22:44 -0700 Subject: [PATCH 013/548] Use software snapshot capture unless featureflag (#56684) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56684 ## Changelog: [Android] [Added] - Use software snapshot capture unless featureflag Use software rendering (View.draw()) for view transition snapshots by default instead of PixelCopy. PixelCopy captures from the window surface, which includes content from overlapping views, popup layers, and hardware compositor surfaces that may visually obstruct the target view. Software rendering draws the view's subtree in isolation to an offscreen canvas, producing a snapshot of exactly the target view's content without interference from other views in the hierarchy. Reviewed By: christophpurrer Differential Revision: D103718490 fbshipit-source-id: cd2b3bebd00ea071d37ca0291050f1b510e6d49c --- .../fabric/ViewTransitionSnapshotManager.kt | 26 +++++++++++-------- .../featureflags/ReactNativeFeatureFlags.kt | 8 +++++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++++++++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 ++- .../ReactNativeFeatureFlagsDefaults.kt | 4 ++- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +++++++++- .../ReactNativeFeatureFlagsProvider.kt | 4 ++- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++++++++++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +++- .../featureflags/ReactNativeFeatureFlags.cpp | 6 ++++- .../featureflags/ReactNativeFeatureFlags.h | 7 ++++- .../ReactNativeFeatureFlagsAccessor.cpp | 22 ++++++++++++++-- .../ReactNativeFeatureFlagsAccessor.h | 6 +++-- .../ReactNativeFeatureFlagsDefaults.h | 6 ++++- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +++++++- .../ReactNativeFeatureFlagsProvider.h | 3 ++- .../NativeReactNativeFeatureFlags.cpp | 7 ++++- .../NativeReactNativeFeatureFlags.h | 4 ++- .../ReactNativeFeatureFlags.config.js | 11 ++++++++ .../featureflags/ReactNativeFeatureFlags.js | 7 ++++- .../specs/NativeReactNativeFeatureFlags.js | 3 ++- 21 files changed, 153 insertions(+), 32 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt index 29c987a4960..4e62a431816 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/ViewTransitionSnapshotManager.kt @@ -25,6 +25,7 @@ import com.facebook.react.bridge.UIManagerListener import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags /** * Manages bitmap snapshots of views during view transitions. Captures bitmaps from old views and @@ -76,9 +77,10 @@ internal class ViewTransitionSnapshotManager( } /** - * Captures a bitmap snapshot of the view identified by the given tag. On API 26+, uses PixelCopy - * to capture directly from the GPU-composited surface (faster for complex views, captures - * hardware-accelerated content). Falls back to View.draw() on older APIs. + * Captures a bitmap snapshot of the view identified by the given tag. When + * [ReactNativeFeatureFlags.viewTransitionUseHardwareBitmapAndroid] is enabled and API 26+, uses + * PixelCopy to capture directly from the GPU-composited surface. Otherwise falls back to + * View.draw() which runs synchronously. */ fun captureViewSnapshot(reactTag: Int, surfaceId: Int) { UiThreadUtil.runOnUiThread { @@ -87,15 +89,17 @@ internal class ViewTransitionSnapshotManager( val view = smm.getView(reactTag) if (view.width <= 0 || view.height <= 0) return@runOnUiThread - val window = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if ( + ReactNativeFeatureFlags.viewTransitionUseHardwareBitmapAndroid() && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ) { + val window = (view.context as? com.facebook.react.bridge.ReactContext)?.getCurrentActivity()?.window - } else { - null - } - - if (window != null) { - captureHardwareBitmap(view, reactTag, window) + if (window != null) { + captureHardwareBitmap(view, reactTag, window) + } else { + captureSoftwareBitmap(view)?.let { onBitmapCaptured(reactTag, it) } + } } else { // Software fallback runs synchronously, so onBitmapCaptured always // completes before setViewSnapshot is called. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index c0a1a9a5cd7..aa4f77a83ea 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1eca66b21554b00725f2a9be894a0db9>> + * @generated SignedSource<<61221e9b185b4e9b9f0db0c99c7c9dc1>> */ /** @@ -576,6 +576,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun viewTransitionEnabled(): Boolean = accessor.viewTransitionEnabled() + /** + * Use hardware bitmaps for view transition snapshots on Android. + */ + @JvmStatic + public fun viewTransitionUseHardwareBitmapAndroid(): Boolean = accessor.viewTransitionUseHardwareBitmapAndroid() + /** * Initial prerender ratio for VirtualView. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index a138abe7be3..d5e22105d27 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<76d977ea53cb2a37fc2ea8549e31cebd>> + * @generated SignedSource<> */ /** @@ -111,6 +111,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var useUnorderedMapInDifferentiatorCache: Boolean? = null private var viewCullingOutsetRatioCache: Double? = null private var viewTransitionEnabledCache: Boolean? = null + private var viewTransitionUseHardwareBitmapAndroidCache: Boolean? = null private var virtualViewPrerenderRatioCache: Double? = null override fun commonTestFlag(): Boolean { @@ -932,6 +933,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun viewTransitionUseHardwareBitmapAndroid(): Boolean { + var cached = viewTransitionUseHardwareBitmapAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.viewTransitionUseHardwareBitmapAndroid() + viewTransitionUseHardwareBitmapAndroidCache = cached + } + return cached + } + override fun virtualViewPrerenderRatio(): Double { var cached = virtualViewPrerenderRatioCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 24abbbaa92e..66bfdcee70d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -210,6 +210,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun viewTransitionEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun viewTransitionUseHardwareBitmapAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun virtualViewPrerenderRatio(): Double @DoNotStrip @JvmStatic public external fun override(provider: Any) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 4075e0ac58d..ac2110bb726 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9e5b3192d1bec953c116d959ad63283d>> + * @generated SignedSource<<4ba79f6843185d86bc9ad487b99d86a4>> */ /** @@ -205,5 +205,7 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun viewTransitionEnabled(): Boolean = false + override fun viewTransitionUseHardwareBitmapAndroid(): Boolean = false + override fun virtualViewPrerenderRatio(): Double = 5.0 } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 1284abcf7a4..807ab7243fb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<378d6a8de497d26ebbbf55885be27a21>> + * @generated SignedSource<<60a15dc6eb548f56216f284421c1e4da>> */ /** @@ -115,6 +115,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var useUnorderedMapInDifferentiatorCache: Boolean? = null private var viewCullingOutsetRatioCache: Double? = null private var viewTransitionEnabledCache: Boolean? = null + private var viewTransitionUseHardwareBitmapAndroidCache: Boolean? = null private var virtualViewPrerenderRatioCache: Double? = null override fun commonTestFlag(): Boolean { @@ -1027,6 +1028,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun viewTransitionUseHardwareBitmapAndroid(): Boolean { + var cached = viewTransitionUseHardwareBitmapAndroidCache + if (cached == null) { + cached = currentProvider.viewTransitionUseHardwareBitmapAndroid() + accessedFeatureFlags.add("viewTransitionUseHardwareBitmapAndroid") + viewTransitionUseHardwareBitmapAndroidCache = cached + } + return cached + } + override fun virtualViewPrerenderRatio(): Double { var cached = virtualViewPrerenderRatioCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 13a3b26839b..d5bb0b001bd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<42e555a40da280b24f84e3ee5b45051d>> + * @generated SignedSource<> */ /** @@ -205,5 +205,7 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun viewTransitionEnabled(): Boolean + @DoNotStrip public fun viewTransitionUseHardwareBitmapAndroid(): Boolean + @DoNotStrip public fun virtualViewPrerenderRatio(): Double } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index f586f2e3fcb..06bb75010bf 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<93d9279dba687d7f660a6797549c0819>> */ /** @@ -585,6 +585,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool viewTransitionUseHardwareBitmapAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("viewTransitionUseHardwareBitmapAndroid"); + return method(javaProvider_); + } + double virtualViewPrerenderRatio() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("virtualViewPrerenderRatio"); @@ -1050,6 +1056,11 @@ bool JReactNativeFeatureFlagsCxxInterop::viewTransitionEnabled( return ReactNativeFeatureFlags::viewTransitionEnabled(); } +bool JReactNativeFeatureFlagsCxxInterop::viewTransitionUseHardwareBitmapAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::viewTransitionUseHardwareBitmapAndroid(); +} + double JReactNativeFeatureFlagsCxxInterop::virtualViewPrerenderRatio( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::virtualViewPrerenderRatio(); @@ -1359,6 +1370,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "viewTransitionEnabled", JReactNativeFeatureFlagsCxxInterop::viewTransitionEnabled), + makeNativeMethod( + "viewTransitionUseHardwareBitmapAndroid", + JReactNativeFeatureFlagsCxxInterop::viewTransitionUseHardwareBitmapAndroid), makeNativeMethod( "virtualViewPrerenderRatio", JReactNativeFeatureFlagsCxxInterop::virtualViewPrerenderRatio), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 2b3741c71e8..67a3268e4b8 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -303,6 +303,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool viewTransitionEnabled( facebook::jni::alias_ref); + static bool viewTransitionUseHardwareBitmapAndroid( + facebook::jni::alias_ref); + static double virtualViewPrerenderRatio( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index a8bddab5889..cf765092f97 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<0a3c6a3353d22cf5909fd5a0f98a970e>> */ /** @@ -390,6 +390,10 @@ bool ReactNativeFeatureFlags::viewTransitionEnabled() { return getAccessor().viewTransitionEnabled(); } +bool ReactNativeFeatureFlags::viewTransitionUseHardwareBitmapAndroid() { + return getAccessor().viewTransitionUseHardwareBitmapAndroid(); +} + double ReactNativeFeatureFlags::virtualViewPrerenderRatio() { return getAccessor().virtualViewPrerenderRatio(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index b0c8f5bb492..cfa92a26c55 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8b4288e3f5a8b26951150a3c75ad4356>> + * @generated SignedSource<<1756053d723019847c7c52bb0f663281>> */ /** @@ -494,6 +494,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool viewTransitionEnabled(); + /** + * Use hardware bitmaps for view transition snapshots on Android. + */ + RN_EXPORT static bool viewTransitionUseHardwareBitmapAndroid(); + /** * Initial prerender ratio for VirtualView. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 0f1a8ac663c..baf879dd3b6 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -1667,6 +1667,24 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { + auto flagValue = viewTransitionUseHardwareBitmapAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(91, "viewTransitionUseHardwareBitmapAndroid"); + + flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); + viewTransitionUseHardwareBitmapAndroid_ = flagValue; + } + + return flagValue.value(); +} + double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { auto flagValue = virtualViewPrerenderRatio_.load(); @@ -1676,7 +1694,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "virtualViewPrerenderRatio"); + markFlagAsAccessed(92, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 67a5012fe00..a38cc5fa756 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17621e5f711352812633c125b7858537>> + * @generated SignedSource<> */ /** @@ -123,6 +123,7 @@ class ReactNativeFeatureFlagsAccessor { bool useUnorderedMapInDifferentiator(); double viewCullingOutsetRatio(); bool viewTransitionEnabled(); + bool viewTransitionUseHardwareBitmapAndroid(); double virtualViewPrerenderRatio(); void override(std::unique_ptr provider); @@ -135,7 +136,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 92> accessedFeatureFlags_; + std::array, 93> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -228,6 +229,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> useUnorderedMapInDifferentiator_; std::atomic> viewCullingOutsetRatio_; std::atomic> viewTransitionEnabled_; + std::atomic> viewTransitionUseHardwareBitmapAndroid_; std::atomic> virtualViewPrerenderRatio_; }; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index ecc79d832fe..f81c9bc9e61 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3d1718afe5b2ed63a918ffe09a4a36b1>> + * @generated SignedSource<<86113211dcbece29ea894a2fcfa7dce5>> */ /** @@ -391,6 +391,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool viewTransitionUseHardwareBitmapAndroid() override { + return false; + } + double virtualViewPrerenderRatio() override { return 5.0; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 2bc41176d59..2406ba36536 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<23e79788b0820f61ba95288412367247>> + * @generated SignedSource<<416b514a4b9708ea8264d209bc717f46>> */ /** @@ -864,6 +864,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::viewTransitionEnabled(); } + bool viewTransitionUseHardwareBitmapAndroid() override { + auto value = values_["viewTransitionUseHardwareBitmapAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::viewTransitionUseHardwareBitmapAndroid(); + } + double virtualViewPrerenderRatio() override { auto value = values_["virtualViewPrerenderRatio"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 400e4bea814..dbae3c49e88 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<87316e34516ebbbb34ae384e5a61376b>> + * @generated SignedSource<<561644576bbbb42833a2366e7b91e4e4>> */ /** @@ -116,6 +116,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool useUnorderedMapInDifferentiator() = 0; virtual double viewCullingOutsetRatio() = 0; virtual bool viewTransitionEnabled() = 0; + virtual bool viewTransitionUseHardwareBitmapAndroid() = 0; virtual double virtualViewPrerenderRatio() = 0; }; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 8c49f21652b..4fe1a185b08 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<80e1d4551c51800c138f16228cfae9f6>> + * @generated SignedSource<> */ /** @@ -499,6 +499,11 @@ bool NativeReactNativeFeatureFlags::viewTransitionEnabled( return ReactNativeFeatureFlags::viewTransitionEnabled(); } +bool NativeReactNativeFeatureFlags::viewTransitionUseHardwareBitmapAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::viewTransitionUseHardwareBitmapAndroid(); +} + double NativeReactNativeFeatureFlags::virtualViewPrerenderRatio( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::virtualViewPrerenderRatio(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index a3703ec3607..f9059e7a212 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1df283aec4b56f36271a187341e7103d>> + * @generated SignedSource<<616585aab3d23395dad8164afe614da6>> */ /** @@ -218,6 +218,8 @@ class NativeReactNativeFeatureFlags bool viewTransitionEnabled(jsi::Runtime& runtime); + bool viewTransitionUseHardwareBitmapAndroid(jsi::Runtime& runtime); + double virtualViewPrerenderRatio(jsi::Runtime& runtime); }; diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index e930a4f0089..8ae401843b8 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -1019,6 +1019,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + viewTransitionUseHardwareBitmapAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-05-04', + description: + 'Use hardware bitmaps for view transition snapshots on Android.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, virtualViewPrerenderRatio: { defaultValue: 5, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 9be1938a644..c026bfd1c02 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<349263e08085c42598f13da74ffbf584>> + * @generated SignedSource<<6af41ac4c6f410c4896e511a7741cbe4>> * @flow strict * @noformat */ @@ -138,6 +138,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ useUnorderedMapInDifferentiator: Getter, viewCullingOutsetRatio: Getter, viewTransitionEnabled: Getter, + viewTransitionUseHardwareBitmapAndroid: Getter, virtualViewPrerenderRatio: Getter, }>; @@ -569,6 +570,10 @@ export const viewCullingOutsetRatio: Getter = createNativeFlagGetter('vi * Enable the View Transition API for animating transitions between views. */ export const viewTransitionEnabled: Getter = createNativeFlagGetter('viewTransitionEnabled', false); +/** + * Use hardware bitmaps for view transition snapshots on Android. + */ +export const viewTransitionUseHardwareBitmapAndroid: Getter = createNativeFlagGetter('viewTransitionUseHardwareBitmapAndroid', false); /** * Initial prerender ratio for VirtualView. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 77b4f560fdf..d026fc82e13 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1fe579457854f95d09dd24e4578dbc65>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -116,6 +116,7 @@ export interface Spec extends TurboModule { +useUnorderedMapInDifferentiator?: () => boolean; +viewCullingOutsetRatio?: () => number; +viewTransitionEnabled?: () => boolean; + +viewTransitionUseHardwareBitmapAndroid?: () => boolean; +virtualViewPrerenderRatio?: () => number; } From 8ac88f481a51cec28adfe985382ac7b3648921e5 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 7 May 2026 01:54:35 -0700 Subject: [PATCH 014/548] Remove 'unspecified' from ColorSchemeName (#56686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56686 Removes `'unspecified'` from the return type of `Appearance.getColorScheme()` and `useColorScheme()`, splitting the setter input into a separate `ColorSchemeOverride` type. This resolves a longstanding misalignment between what native returns and what the types promise. **Motivation** `'unspecified'` is only meaningful as an input to `setColorScheme()` — neither iOS nor Android ever returns it from `getColorScheme()`. When `setColorScheme('unspecified')` is called, the JS layer re-queries the native module and caches the resolved system value. After this change: - `Appearance.getColorScheme()` returns `'light' | 'dark'` (no longer `'unspecified'`) - `Appearance.setColorScheme()` receives `'light' | 'dark' | 'unspecified'` Paired with docs updates: - https://github.com/facebook/react-native-website/pull/5060 - https://github.com/facebook/react-native-website/pull/5069 **History of this API** - The TurboModule spec originally typed these methods as plain `string` because codegen didn't support union types (T52919652). - When support landed, D63681874 upgraded to `ColorSchemeName = 'light' | 'dark' | 'unspecified'` — a type-level cleanup that inadvertently widened return types to include `'unspecified'`, a value native never returns. This caused `$FlowFixMe` suppressions across downstream callers. - D80705652 later aligned the `.d.ts` and fixed a bug where `setColorScheme('unspecified')` threw an incorrect invariant. Changelog: [General][Breaking] - `useColorScheme()` no longer returns `'unspecified'` (this was always the case, but is a breaking type change) Reviewed By: cipolleschi Differential Revision: D102527387 fbshipit-source-id: 3fc19add59450cb07b9d0a962c63c4fa208faa63 --- .../Libraries/Utilities/Appearance.d.ts | 35 +++++++++++-------- .../Libraries/Utilities/Appearance.js | 35 +++++++++++++------ .../Libraries/Utilities/useColorScheme.js | 8 +++++ packages/react-native/ReactNativeApi.d.ts | 15 ++++---- .../modules/NativeAppearance.js | 6 ++-- .../examples/Appearance/AppearanceExample.js | 7 ++-- 6 files changed, 70 insertions(+), 36 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index fe02b80b972..a271aa02cae 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -9,7 +9,9 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter'; -type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +type ColorSchemeName = 'light' | 'dark'; + +type ColorSchemeOverride = ColorSchemeName | 'unspecified'; export namespace Appearance { type AppearancePreferences = { @@ -19,25 +21,30 @@ export namespace Appearance { type AppearanceListener = (preferences: AppearancePreferences) => void; /** - * Note: Although color scheme is available immediately, it may change at any - * time. Any rendering logic or styles that depend on this should try to call - * this function on every render, rather than caching the value (for example, - * using inline styles rather than setting a value in a `StyleSheet`). + * Returns the active color scheme (`'light'` or `'dark'`). This value may + * change at runtime, either at the system level (e.g. scheduled color scheme + * change at sunrise or sunset) or when overridden at the app level via + * `setColorScheme()`. + * + * Prefer `useColorScheme()` in React components. * - * Example: `const colorScheme = Appearance.getColorScheme();` + * Notes: + * - `null` will only be returned if the native Appearance module is + * unavailable (out of tree platforms). */ export function getColorScheme(): ColorSchemeName | null | undefined; /** - * Set the color scheme preference. This is useful for overriding the default - * color scheme preference for the app. Note that this will not change the - * appearance of the system UI, only the appearance of the app. - * Only available on iOS 13+ and Android 10+. + * Force the application to always adopt a light or dark interface style. Pass + * `'unspecified'` to reset and follow the system default (removes any + * override). This does not affect the system UI, only the application. */ - export function setColorScheme(scheme: ColorSchemeName): void; + export function setColorScheme(scheme: ColorSchemeOverride): void; /** - * Add an event handler that is fired when appearance preferences change. + * Subscribe to color scheme changes. The listener receives the new appearance + * preferences whenever the color scheme changes, whether from a system event + * or a call to `setColorScheme()`. */ export function addChangeListener( listener: AppearanceListener, @@ -45,7 +52,7 @@ export namespace Appearance { } /** - * A new useColorScheme hook is provided as the preferred way of accessing - * the user's preferred color scheme (e.g. Dark Mode). + * Returns the active color scheme (`'light'` or `'dark'`). Automatically + * re-renders the component when the color scheme changes. */ export function useColorScheme(): ColorSchemeName; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 8b2bdba03c8..05b32bb2dbb 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -9,13 +9,17 @@ */ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; -import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance'; +import type { + AppearancePreferences, + ColorSchemeName, + ColorSchemeOverride, +} from './NativeAppearance'; import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; -export type {AppearancePreferences}; +export type {AppearancePreferences, ColorSchemeName, ColorSchemeOverride}; type Appearance = { colorScheme: ?ColorSchemeName, @@ -69,9 +73,16 @@ function getState(): NonNullable { } /** - * Returns the current color scheme preference. This value may change, so the - * value should not be cached without either listening to changes or using - * the `useColorScheme` hook. + * Returns the active color scheme (`'light'` or `'dark'`). This value may + * change at runtime, either at the system level (e.g. scheduled color scheme + * change at sunrise or sunset) or when overridden at the app level via + * `setColorScheme()`. + * + * Prefer `useColorScheme()` in React components. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). */ export function getColorScheme(): ?ColorSchemeName { let colorScheme = null; @@ -91,26 +102,28 @@ export function getColorScheme(): ?ColorSchemeName { } /** - * Updates the current color scheme to the supplied value. + * Force the application to always adopt a light or dark interface style. Pass + * `'unspecified'` to reset and follow the system default (removes any + * override). This does not affect the system UI, only the application. */ -export function setColorScheme(colorScheme: ColorSchemeName): void { +export function setColorScheme(colorScheme: ColorSchemeOverride): void { const state = getState(); const {NativeAppearance} = state; if (NativeAppearance != null) { NativeAppearance.setColorScheme(colorScheme); state.appearance = { - // When setting to 'unspecified', get the actual system color scheme. - // Fall back to the passed value if getColorScheme() returns null. colorScheme: colorScheme === 'unspecified' - ? (NativeAppearance.getColorScheme() ?? colorScheme) + ? (NativeAppearance.getColorScheme() ?? null) : colorScheme, }; } } /** - * Add an event handler that is fired when appearance preferences change. + * Subscribe to color scheme changes. The listener receives the new appearance + * preferences whenever the color scheme changes, whether from a system event + * or a call to `setColorScheme()`. */ export function addChangeListener( listener: ({colorScheme: ?ColorSchemeName}) => void, diff --git a/packages/react-native/Libraries/Utilities/useColorScheme.js b/packages/react-native/Libraries/Utilities/useColorScheme.js index ab5f6de5256..45eb00fa8ca 100644 --- a/packages/react-native/Libraries/Utilities/useColorScheme.js +++ b/packages/react-native/Libraries/Utilities/useColorScheme.js @@ -20,6 +20,14 @@ const subscribe = (onStoreChange: () => void) => { return () => appearanceSubscription.remove(); }; +/** + * Returns the active color scheme (`'light'` or `'dark'`). Automatically + * re-renders the component when the color scheme changes. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). + */ export default function useColorScheme(): ?ColorSchemeName { return useSyncExternalStore(subscribe, getColorScheme); } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 13c53f3f59a..c3f957f6688 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1c8637ab03a5fec9d39704d1ae305595>> + * @generated SignedSource<<4950f1efd16fed02b526f83325c8351d>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1630,6 +1630,8 @@ declare namespace Appearance { setColorScheme, addChangeListener, AppearancePreferences, + ColorSchemeName, + ColorSchemeOverride, } } declare type AppearancePreferences = { @@ -1856,7 +1858,8 @@ declare namespace CodegenTypes { } } declare type ColorListenerCallback = (value: ColorValue) => unknown -declare type ColorSchemeName = "dark" | "light" | "unspecified" +declare type ColorSchemeName = "dark" | "light" +declare type ColorSchemeOverride = "dark" | "light" | "unspecified" declare type ColorValue = ____ColorValue_Internal declare type ComponentProvider = () => React.ComponentType declare type ComponentProviderInstrumentationHook = ( @@ -4709,7 +4712,7 @@ declare type Separators = { updateProps: (select: "leading" | "trailing", newProps: Object) => void } declare type sequence = typeof sequence -declare function setColorScheme(colorScheme: ColorSchemeName): void +declare function setColorScheme(colorScheme: ColorSchemeOverride): void declare function setComponentProviderInstrumentationHook( hook: ComponentProviderInstrumentationHook, ): void @@ -6070,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // 00cbaa0a + Appearance, // df9545f9 AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 @@ -6080,7 +6083,7 @@ export { ButtonProps, // 0df9cb59 Clipboard, // 41addb89 CodegenTypes, // 0b8108a8 - ColorSchemeName, // 31a4350e + ColorSchemeName, // 6615edd6 ColorValue, // 98989a8f ComponentProvider, // b5c60ddd ComponentProviderInstrumentationHook, // 9f640048 @@ -6336,7 +6339,7 @@ export { useAnimatedColor, // e3511f81 useAnimatedValue, // b18adb63 useAnimatedValueXY, // c7ee2332 - useColorScheme, // c216d6f7 + useColorScheme, // 29a517d5 usePressability, // b4e21b46 useWindowDimensions, // bb4b683f } diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index 29296fe8792..dca2c6f8a08 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -12,7 +12,9 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; -export type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +export type ColorSchemeName = 'light' | 'dark'; + +export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified'; export type AppearancePreferences = { colorScheme?: ?ColorSchemeName, @@ -20,7 +22,7 @@ export type AppearancePreferences = { export interface Spec extends TurboModule { +getColorScheme: () => ?ColorSchemeName; - +setColorScheme: (colorScheme: ColorSchemeName) => void; + +setColorScheme: (colorScheme: ColorSchemeOverride) => void; // RCTEventEmitter +addListener: (eventName: string) => void; diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 7285806793a..7fd44d84f22 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -18,7 +18,7 @@ import {useEffect, useState} from 'react'; import {Appearance, Button, Text, View, useColorScheme} from 'react-native'; function ColorSchemeSubscription() { - const [colorScheme, setColorScheme] = useState( + const [colorScheme, setColorScheme] = useState( Appearance.getColorScheme(), ); @@ -135,8 +135,9 @@ const ColorShowcase = (props: {themeName: string}) => ( ); const ToggleNativeAppearance = () => { - const [nativeColorScheme, setNativeColorScheme] = - useState('unspecified'); + const [nativeColorScheme, setNativeColorScheme] = useState< + ColorSchemeName | 'unspecified', + >('unspecified'); const colorScheme = useColorScheme(); useEffect(() => { From ef6463c25d78990386a15bb26319d1d4d224df14 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 7 May 2026 01:54:35 -0700 Subject: [PATCH 015/548] Align nullable types across Appearance API (#56687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56687 Simplify and align nullable values in the `Appearance` API. | API | Flow / Strict TS API | Manual `.d.ts` | |---|---|---| | `getColorScheme()` | `ColorSchemeName | null` (narrowed) | `ColorSchemeName | null` | | `useColorScheme()` | `ColorSchemeName | null` (narrowed) | `ColorSchemeName | null` (fixed) | At the native spec level, nullability is removed entirely — the module always returns a valid color scheme, and the module-level `?Spec` already covers the "module absent" case. Changelog: [General][Breaking] - Fix return type of `useColorScheme()` hook (now `ColorSchemeName | null`) - Release Crew note: This is a net fix/extension of D102527387 Reviewed By: cipolleschi Differential Revision: D103865622 fbshipit-source-id: a6391cfd86a634ea660734735614169d0601a830 --- .../Libraries/Utilities/Appearance.d.ts | 10 +++++++--- .../Libraries/Utilities/Appearance.js | 18 +++++++++++------- .../Libraries/Utilities/useColorScheme.js | 2 +- packages/react-native/ReactNativeApi.d.ts | 16 ++++++++-------- .../modules/NativeAppearance.js | 4 ++-- .../examples/Appearance/AppearanceExample.js | 2 +- .../api-snapshots/ReactAppleDebugCxx.api | 2 +- .../api-snapshots/ReactAppleReleaseCxx.api | 2 +- 8 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index a271aa02cae..c37a0ea577d 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -15,7 +15,7 @@ type ColorSchemeOverride = ColorSchemeName | 'unspecified'; export namespace Appearance { type AppearancePreferences = { - colorScheme: ColorSchemeName; + colorScheme: ColorSchemeName | null; }; type AppearanceListener = (preferences: AppearancePreferences) => void; @@ -32,7 +32,7 @@ export namespace Appearance { * - `null` will only be returned if the native Appearance module is * unavailable (out of tree platforms). */ - export function getColorScheme(): ColorSchemeName | null | undefined; + export function getColorScheme(): ColorSchemeName | null; /** * Force the application to always adopt a light or dark interface style. Pass @@ -54,5 +54,9 @@ export namespace Appearance { /** * Returns the active color scheme (`'light'` or `'dark'`). Automatically * re-renders the component when the color scheme changes. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). */ -export function useColorScheme(): ColorSchemeName; +export function useColorScheme(): ColorSchemeName | null; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 05b32bb2dbb..4af8fe2e6b3 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -10,7 +10,7 @@ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import type { - AppearancePreferences, + AppearancePreferences as NativeAppearancePreferences, ColorSchemeName, ColorSchemeOverride, } from './NativeAppearance'; @@ -19,10 +19,14 @@ import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; -export type {AppearancePreferences, ColorSchemeName, ColorSchemeOverride}; +export type {ColorSchemeName, ColorSchemeOverride}; + +export type AppearancePreferences = { + colorScheme: ColorSchemeName | null, +}; type Appearance = { - colorScheme: ?ColorSchemeName, + colorScheme: ColorSchemeName | null, }; let lazyState: ?{ @@ -60,7 +64,7 @@ function getState(): NonNullable { eventEmitter, }; new NativeEventEmitter<{ - appearanceChanged: [AppearancePreferences], + appearanceChanged: [NativeAppearancePreferences], }>(NativeAppearance).addListener('appearanceChanged', newAppearance => { state.appearance = { colorScheme: newAppearance.colorScheme, @@ -84,7 +88,7 @@ function getState(): NonNullable { * - `null` will only be returned if the native Appearance module is unavailable * (out of tree platforms). */ -export function getColorScheme(): ?ColorSchemeName { +export function getColorScheme(): ColorSchemeName | null { let colorScheme = null; const state = getState(); const {NativeAppearance} = state; @@ -114,7 +118,7 @@ export function setColorScheme(colorScheme: ColorSchemeOverride): void { state.appearance = { colorScheme: colorScheme === 'unspecified' - ? (NativeAppearance.getColorScheme() ?? null) + ? NativeAppearance.getColorScheme() : colorScheme, }; } @@ -126,7 +130,7 @@ export function setColorScheme(colorScheme: ColorSchemeOverride): void { * or a call to `setColorScheme()`. */ export function addChangeListener( - listener: ({colorScheme: ?ColorSchemeName}) => void, + listener: (preferences: AppearancePreferences) => void, ): EventSubscription { const {eventEmitter} = getState(); return eventEmitter.addListener('change', listener); diff --git a/packages/react-native/Libraries/Utilities/useColorScheme.js b/packages/react-native/Libraries/Utilities/useColorScheme.js index 45eb00fa8ca..39cec85a303 100644 --- a/packages/react-native/Libraries/Utilities/useColorScheme.js +++ b/packages/react-native/Libraries/Utilities/useColorScheme.js @@ -28,6 +28,6 @@ const subscribe = (onStoreChange: () => void) => { * - `null` will only be returned if the native Appearance module is unavailable * (out of tree platforms). */ -export default function useColorScheme(): ?ColorSchemeName { +export default function useColorScheme(): ColorSchemeName | null { return useSyncExternalStore(subscribe, getColorScheme); } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c3f957f6688..a2538b661fd 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4950f1efd16fed02b526f83325c8351d>> + * @generated SignedSource<<04955f1605996352fa3f3a3fc86ba937>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1218,7 +1218,7 @@ declare type ActivityIndicatorProps = Readonly< > declare type add = typeof add declare function addChangeListener( - listener: ($$PARAM_0$$: { colorScheme: ColorSchemeName | undefined }) => void, + listener: (preferences: AppearancePreferences) => void, ): EventSubscription declare class Alert { static alert( @@ -1629,13 +1629,13 @@ declare namespace Appearance { getColorScheme, setColorScheme, addChangeListener, - AppearancePreferences, ColorSchemeName, ColorSchemeOverride, + AppearancePreferences, } } declare type AppearancePreferences = { - colorScheme?: ColorSchemeName + colorScheme: ColorSchemeName | null } declare type AppParameters = { initialProps: { @@ -2468,7 +2468,7 @@ declare function get_2( name: string, ): null | T | undefined declare function getAppKeys(): ReadonlyArray -declare function getColorScheme(): ColorSchemeName | null | undefined +declare function getColorScheme(): ColorSchemeName | null declare function getEnforcing(name: string): T declare function getRegistry(): Registry declare function getRunnable(appKey: string): null | Runnable | undefined @@ -5738,7 +5738,7 @@ declare function useAnimatedValueXY( }, config?: Animated.AnimatedConfig | null | undefined, ): Animated.ValueXY -declare function useColorScheme(): ColorSchemeName | null | undefined +declare function useColorScheme(): ColorSchemeName | null declare function usePressability( config: null | PressabilityConfig | undefined, ): null | PressabilityEventHandlers @@ -6073,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // df9545f9 + Appearance, // 0e9bf4fe AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 @@ -6339,7 +6339,7 @@ export { useAnimatedColor, // e3511f81 useAnimatedValue, // b18adb63 useAnimatedValueXY, // c7ee2332 - useColorScheme, // 29a517d5 + useColorScheme, // d585efdb usePressability, // b4e21b46 useWindowDimensions, // bb4b683f } diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index dca2c6f8a08..e1f8835be6d 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -17,11 +17,11 @@ export type ColorSchemeName = 'light' | 'dark'; export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified'; export type AppearancePreferences = { - colorScheme?: ?ColorSchemeName, + colorScheme: ColorSchemeName, }; export interface Spec extends TurboModule { - +getColorScheme: () => ?ColorSchemeName; + +getColorScheme: () => ColorSchemeName; +setColorScheme: (colorScheme: ColorSchemeOverride) => void; // RCTEventEmitter diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 7fd44d84f22..06f315af801 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -24,7 +24,7 @@ function ColorSchemeSubscription() { useEffect(() => { const subscription = Appearance.addChangeListener( - ({colorScheme: newColorScheme}: {colorScheme: ?ColorSchemeName}) => { + ({colorScheme: newColorScheme}) => { setColorScheme(newColorScheme); }, ); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 62d88ff7546..a32188996a1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2653,7 +2653,7 @@ protocol NativeAppStateSpec : public NSObjectRCTBridgeModule, public RCTTurboMod } protocol NativeAppearanceSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { - public virtual NSString* _Nullable getColorScheme(); + public virtual NSString* getColorScheme(); public virtual void addListener:(NSString* eventName); public virtual void removeListeners:(double count); public virtual void setColorScheme:(NSString* colorScheme); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index b8feb8d7d21..d7818d3fe13 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2653,7 +2653,7 @@ protocol NativeAppStateSpec : public NSObjectRCTBridgeModule, public RCTTurboMod } protocol NativeAppearanceSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { - public virtual NSString* _Nullable getColorScheme(); + public virtual NSString* getColorScheme(); public virtual void addListener:(NSString* eventName); public virtual void removeListeners:(double count); public virtual void setColorScheme:(NSString* colorScheme); From 91702e50fe077ae73ab152da18719d6df98cb71f Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 7 May 2026 01:54:35 -0700 Subject: [PATCH 016/548] Add 'auto' to ColorSchemeOverride, deprecate 'unspecified' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: NOTE: 👋🏻 **This is an RFC**, additional and separate to the previous 2 diffs. Looking for feedback :) Proposes an API tweak to `Appearance.setColorScheme()` to make it more idiomatic/understandable. - Aligns with the CSS `color-scheme` property vocabulary, where `auto` means "defer to the system preference". - Replaces the ambiguous `unspecified` (now deprecated), which gave no indication of the resulting behaviour. See also: - History of this API + return type narrowing in D102527387. - Extended docs + diagram in https://github.com/facebook/react-native-website/pull/5060. **Alternative names considered** - `'reset'` - Implies reversing a change, not deferring to system - `'inherit'` - Weak — CSS inherit is element→parent, not app→OS Changelog: [General][Deprecated] - `Appearance.setColorScheme('unspecified')` is deprecated, use `'auto'` instead. Reviewed By: cipolleschi Differential Revision: D103841988 fbshipit-source-id: 474d0925ce9ee0ab79f87d0ff9ae86ae56ceb0aa --- .../react-native/Libraries/Utilities/Appearance.d.ts | 10 +++++++--- .../react-native/Libraries/Utilities/Appearance.js | 6 +++--- packages/react-native/React/Base/RCTConvert.mm | 1 + .../react/modules/appearance/AppearanceModule.kt | 1 + packages/react-native/ReactNativeApi.d.ts | 6 +++--- .../specs_DEPRECATED/modules/NativeAppearance.js | 2 +- .../js/examples/Appearance/AppearanceExample.js | 9 +++------ 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index c37a0ea577d..153f0386fdf 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -11,7 +11,11 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter' type ColorSchemeName = 'light' | 'dark'; -type ColorSchemeOverride = ColorSchemeName | 'unspecified'; +type ColorSchemeOverride = + | ColorSchemeName + | 'auto' + /** @deprecated Use 'auto' instead */ + | 'unspecified'; export namespace Appearance { type AppearancePreferences = { @@ -36,8 +40,8 @@ export namespace Appearance { /** * Force the application to always adopt a light or dark interface style. Pass - * `'unspecified'` to reset and follow the system default (removes any - * override). This does not affect the system UI, only the application. + * `'auto'` to reset and follow the system default (removes any override). + * This does not affect the system UI, only the application. */ export function setColorScheme(scheme: ColorSchemeOverride): void; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 4af8fe2e6b3..936b03f9385 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -107,8 +107,8 @@ export function getColorScheme(): ColorSchemeName | null { /** * Force the application to always adopt a light or dark interface style. Pass - * `'unspecified'` to reset and follow the system default (removes any - * override). This does not affect the system UI, only the application. + * `'auto'` to reset and follow the system default (removes any override). + * This does not affect the system UI, only the application. */ export function setColorScheme(colorScheme: ColorSchemeOverride): void { const state = getState(); @@ -117,7 +117,7 @@ export function setColorScheme(colorScheme: ColorSchemeOverride): void { NativeAppearance.setColorScheme(colorScheme); state.appearance = { colorScheme: - colorScheme === 'unspecified' + colorScheme === 'auto' || colorScheme === 'unspecified' ? NativeAppearance.getColorScheme() : colorScheme, }; diff --git a/packages/react-native/React/Base/RCTConvert.mm b/packages/react-native/React/Base/RCTConvert.mm index 0aacbd85610..9400163dd81 100644 --- a/packages/react-native/React/Base/RCTConvert.mm +++ b/packages/react-native/React/Base/RCTConvert.mm @@ -494,6 +494,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC RCT_ENUM_CONVERTER( UIUserInterfaceStyle, (@{ + @"auto" : @(UIUserInterfaceStyleUnspecified), @"unspecified" : @(UIUserInterfaceStyleUnspecified), @"light" : @(UIUserInterfaceStyleLight), @"dark" : @(UIUserInterfaceStyleDark), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt index 65f1088ef51..4028e2a9a20 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt @@ -87,6 +87,7 @@ constructor( when (style) { "dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + "auto", "unspecified" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index a2538b661fd..6e7dd111ae4 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<04955f1605996352fa3f3a3fc86ba937>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1859,7 +1859,7 @@ declare namespace CodegenTypes { } declare type ColorListenerCallback = (value: ColorValue) => unknown declare type ColorSchemeName = "dark" | "light" -declare type ColorSchemeOverride = "dark" | "light" | "unspecified" +declare type ColorSchemeOverride = "auto" | "dark" | "light" | "unspecified" declare type ColorValue = ____ColorValue_Internal declare type ComponentProvider = () => React.ComponentType declare type ComponentProviderInstrumentationHook = ( @@ -6073,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // 0e9bf4fe + Appearance, // 83e9641a AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index e1f8835be6d..24293ff900a 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -14,7 +14,7 @@ import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboMod export type ColorSchemeName = 'light' | 'dark'; -export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified'; +export type ColorSchemeOverride = 'light' | 'dark' | 'auto' | 'unspecified'; export type AppearancePreferences = { colorScheme: ColorSchemeName, diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 06f315af801..62bc5f9c297 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -136,8 +136,8 @@ const ColorShowcase = (props: {themeName: string}) => ( const ToggleNativeAppearance = () => { const [nativeColorScheme, setNativeColorScheme] = useState< - ColorSchemeName | 'unspecified', - >('unspecified'); + ColorSchemeName | 'auto', + >('auto'); const colorScheme = useColorScheme(); useEffect(() => { @@ -156,10 +156,7 @@ const ToggleNativeAppearance = () => { title="Set to dark" onPress={() => setNativeColorScheme('dark')} /> -