diff --git a/.gitignore b/.gitignore
index 963de906..c728d051 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,4 +60,6 @@ GoogleService-Info.plist
android/app/google-services.json
ios/BitSleuthWallet/GoogleService-Info.plist
ios/GoogleService-Info.plist
-ios/BitSleuthWallet.xcodeproj/GoogleService-Info.plist
\ No newline at end of file
+ios/BitSleuthWallet.xcodeproj/GoogleService-Info.plist
+# Build archives
+*.tar.gz
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa080642..dcb99c09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.2.2] - 2026-06-10
+
### Added
- **Open Source Release**: BitSleuth Wallet is now open source under AGPL-3.0 license
- GitHub Actions workflows for CI/CD (lint, build verification, security scanning)
@@ -18,6 +20,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Security scanning with dependency review and secret detection
- GitHub Discussions for community support
- Issue and PR templates for structured contributions
+- **Satoshi API Fee Fallback**: Added a secondary fee-estimate source so fee recommendations remain available when the Esplora `/fee-estimates` endpoint fails
+- **Firebase Performance Monitoring**: Enabled app performance tracking alongside Crashlytics (crash reporting and performance only — no analytics)
+- **Automated GitHub Releases**: Pushing a version tag now creates the GitHub release automatically with notes extracted from this changelog
+- **UI Polish**: Premium animations, haptic feedback, and micro-interactions across wallet flows, plus refined theme and typography
+
+### Changed
+- Pinned `react-native-reanimated` to exact version 4.1.6 (with patch-package patch) to keep Android EAS builds reproducible
+- `patch-package` now fails loudly on local installs so broken patches are caught early
+- Raised the Node.js engine requirement to >=20.19.4 for Metro compatibility
+- Downgraded Gradle to 9.3 for Android build stability
+- Aligned dependency versions with Expo SDK 54 expectations
+- Updated `@react-native-firebase` packages to 23.8.x
+- Updated iOS liquid glass tab documentation and checks from iOS 18+ to iOS 26+
+- Routine dependency updates (yaml, tar, lodash, hono, react-native-svg, lucide, Gradle wrapper, firebase-crashlytics-gradle, GitHub Actions)
+
+### Fixed
+- **iOS Build Error**: Fixed build failure caused by non-modular header includes in React Native Firebase
+ - Resolves Xcode 26 build errors: "include of non-modular header inside framework module 'RNFBApp...'"
+ - Added `ios.forceStaticLinking` for `RNFBApp`, `RNFBCrashlytics` and `RNFBPerf` via `expo-build-properties` so the Firebase pods build as static libraries instead of static frameworks (Expo SDK 54 mechanism, expo/expo#39742)
+ - Removed the `@react-native-firebase` patch-package patches that previously attempted to work around the header errors (ineffective under Xcode 26 explicitly-built modules)
+ - Removed unused `use_modular_headers!` podfile property from app.json
+- **Android EAS Build**: Fixed build failure caused by Android build artifacts accidentally included in the reanimated patch
+- **Receive Tab Performance**: Fixed QR code rendering performance and New Address refresh behavior
+- **Wallet Import Speed**: Optimized transaction fetching during wallet import
+- **Address Validation**: Bitcoin address validation now performs proper checksum verification
+- Fixed TypeScript build errors in `AnimatedNumber` under Reanimated v4 and improved type safety in `rbf-service.ts`
### Security
- **CVE-2025-55182**: Updated React to version 19.1.2 to address security vulnerability
@@ -26,12 +54,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated `@types/react` from ~19.1.10 to ~19.1.2 to align with React version
- All peer dependencies remain compatible (React Native 0.81.5, Expo SDK 54, @tanstack/react-query, zustand, React Native Reanimated, Expo Router, NativeWind)
- No breaking changes or compatibility issues
-
-### Fixed
-- **iOS Build Error**: Fixed build failure caused by non-modular header includes in React Native Firebase
- - Removed `use_modular_headers!` from Podfile which was causing RNFBApp to fail when importing React-Core headers
- - Firebase works correctly with static frameworks without requiring modular headers
- - Resolves Xcode build errors: "include of non-modular header inside framework module"
+- **CVE-2026-2391**: Upgraded `qs` from 6.14.1 to 6.15.0
+- Forced `@xmldom/xmldom` (pinned to ~0.8.13) and `postcss` to patched versions via npm overrides
+- Resolved remaining high-severity advisories via `npm audit fix`
+- Removed Firebase config files and API keys from the repository; added example templates and setup documentation
+- Removed the debug keystore from the repository and added keystore patterns to `.gitignore`
## [1.2.0] - 2025-11-05
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c74064c4..5cf2f7cd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -93,7 +93,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
- versionName "1.2.1"
+ versionName "1.2.2"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
diff --git a/app.json b/app.json
index 6ada3dce..6ee114aa 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "BitSleuth Wallet",
"slug": "bitsleuth-wallet",
- "version": "1.2.1",
+ "version": "1.2.2",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
@@ -67,9 +67,7 @@
},
"ios": {
"useFrameworks": "static",
- "podfileProperties": {
- "use_modular_headers!": true
- }
+ "forceStaticLinking": ["RNFBApp", "RNFBCrashlytics", "RNFBPerf"]
}
}
],
diff --git a/eas.json b/eas.json
index 57ef7ab6..0a435ce2 100644
--- a/eas.json
+++ b/eas.json
@@ -7,7 +7,7 @@
"development": {
"developmentClient": true,
"distribution": "internal",
- "node": "20.18.1",
+ "node": "20.19.4",
"android": {
"image": "ubuntu-22.04-jdk-17-ndk-r25b"
},
@@ -18,7 +18,7 @@
},
"preview": {
"distribution": "internal",
- "node": "20.18.1",
+ "node": "20.19.4",
"android": {
"image": "ubuntu-22.04-jdk-17-ndk-r25b"
},
@@ -29,7 +29,7 @@
},
"production": {
"autoIncrement": true,
- "node": "20.18.1",
+ "node": "20.19.4",
"android": {
"image": "ubuntu-22.04-jdk-17-ndk-r25b"
},
diff --git a/ios/BitSleuthWallet/Info.plist b/ios/BitSleuthWallet/Info.plist
index 05684519..10ecf26d 100644
--- a/ios/BitSleuthWallet/Info.plist
+++ b/ios/BitSleuthWallet/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.2.1
+ 1.2.2
CFBundleSignature
????
CFBundleURLTypes
diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json
index a7d5f684..70ed43fd 100644
--- a/ios/Podfile.properties.json
+++ b/ios/Podfile.properties.json
@@ -3,6 +3,6 @@
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"newArchEnabled": "true",
"ios.useFrameworks": "static",
- "ios.forceStaticLinking": "[]",
+ "ios.forceStaticLinking": "[\"RNFBApp\",\"RNFBCrashlytics\",\"RNFBPerf\"]",
"apple.privacyManifestAggregationEnabled": "true"
}
diff --git a/package-lock.json b/package-lock.json
index fd3bbec9..fade3d69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "bitsleuth-wallet",
- "version": "1.2.1",
+ "version": "1.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bitsleuth-wallet",
- "version": "1.2.1",
+ "version": "1.2.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -78,7 +78,7 @@
"react-native-get-random-values": "^1.11.0",
"react-native-polyfill-globals": "^3.1.0",
"react-native-qrcode-svg": "^6.3.21",
- "react-native-reanimated": "~4.1.6",
+ "react-native-reanimated": "4.1.6",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
@@ -105,7 +105,7 @@
"typescript": "~5.9.2"
},
"engines": {
- "node": ">=20.18.0"
+ "node": ">=20.19.4"
}
},
"node_modules/@0no-co/graphql.web": {
@@ -2066,6 +2066,16 @@
"wonka": "^6.3.2"
}
},
+ "node_modules/@expo/build-tools/node_modules/@xmldom/xmldom": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/@expo/build-tools/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -3065,6 +3075,15 @@
"xmlbuilder": "^15.1.1"
}
},
+ "node_modules/@expo/plist/node_modules/@xmldom/xmldom": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/@expo/prebuild-config": {
"version": "54.0.8",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz",
@@ -6709,15 +6728,6 @@
"@urql/core": "^5.0.0"
}
},
- "node_modules/@xmldom/xmldom": {
- "version": "0.9.10",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
- "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
- "license": "MIT",
- "engines": {
- "node": ">=14.6"
- }
- },
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
@@ -15485,6 +15495,15 @@
"node": ">=10.4.0"
}
},
+ "node_modules/plist/node_modules/@xmldom/xmldom": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -16698,24 +16717,25 @@
}
},
"node_modules/react-native-reanimated": {
- "version": "4.1.7",
- "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz",
- "integrity": "sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==",
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
+ "integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
"license": "MIT",
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1",
- "semver": "^7.7.2"
+ "semver": "7.7.2"
},
"peerDependencies": {
+ "@babel/core": "^7.0.0-0",
"react": "*",
- "react-native": "0.78 - 0.82",
- "react-native-worklets": "0.5 - 0.8"
+ "react-native": "*",
+ "react-native-worklets": ">=0.5.0"
}
},
"node_modules/react-native-reanimated/node_modules/semver": {
- "version": "7.8.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
- "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
diff --git a/package.json b/package.json
index c39511d0..f81744b0 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "bitsleuth-wallet",
"main": "index.js",
- "version": "1.2.1",
+ "version": "1.2.2",
"license": "AGPL-3.0",
"packageManager": "npm@10.2.4",
"engines": {
- "node": ">=20.18.0"
+ "node": ">=20.19.4"
},
"scripts": {
"start": "expo start",
@@ -17,7 +17,7 @@
"ios": "expo run:ios --dev-client",
"android-debug": "expo run:android --dev-client --clear",
"ios-debug": "expo run:ios --dev-client --clear",
- "postinstall": "patch-package"
+ "postinstall": "patch-package --error-on-fail"
},
"dependencies": {
"@babel/core": "^7.29.0",
@@ -88,7 +88,7 @@
"react-native-get-random-values": "^1.11.0",
"react-native-polyfill-globals": "^3.1.0",
"react-native-qrcode-svg": "^6.3.21",
- "react-native-reanimated": "~4.1.6",
+ "react-native-reanimated": "4.1.6",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
@@ -127,7 +127,7 @@
"overrides": {
"react-native-randombytes": "^3.6.2",
"react-native-renderer": "19.1.0",
- "@xmldom/xmldom": ">=0.8.13",
+ "@xmldom/xmldom": "~0.8.13",
"postcss": ">=8.5.10"
},
"resolutions": {
diff --git a/patches/@react-native-firebase+crashlytics+23.8.6.patch b/patches/@react-native-firebase+crashlytics+23.8.6.patch
deleted file mode 100644
index 93e29855..00000000
--- a/patches/@react-native-firebase+crashlytics+23.8.6.patch
+++ /dev/null
@@ -1,38 +0,0 @@
-diff --git a/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.h b/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.h
-index c7b1739..bc8d61a 100644
---- a/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.h
-+++ b/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.h
-@@ -17,7 +17,13 @@
-
- #import
-
-+@import RNFBApp;
-+
-+#if __has_include()
-+#import
-+#else
- #import
-+#endif
-
- @interface RNFBCrashlyticsModule : NSObject
-
-diff --git a/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m b/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m
-index ada982b..3bd1a0d 100644
---- a/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m
-+++ b/node_modules/@react-native-firebase/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsModule.m
-@@ -18,9 +18,15 @@
- #include
- #include
-
-+#if __has_include()
-+#import
-+#import
-+#import
-+#else
- #import
- #import
- #import
-+#endif
-
- #import
- #import "RNFBApp/RNFBSharedUtils.h"
diff --git a/patches/react-native-reanimated+4.1.6.patch b/patches/react-native-reanimated+4.1.6.patch
new file mode 100644
index 00000000..71470178
--- /dev/null
+++ b/patches/react-native-reanimated+4.1.6.patch
@@ -0,0 +1,939 @@
+diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp
+index bd8d43a..ee43dd1 100644
+--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp
++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp
+@@ -32,18 +32,6 @@ void UpdatesRegistry::flushUpdates(UpdatesBatch &updatesBatch) {
+ }
+ }
+
+-UpdatesBatch UpdatesRegistry::getPendingUpdates() {
+- auto lock = std::lock_guard{mutex_};
+- flushUpdatesToRegistry(updatesBatch_);
+-
+- UpdatesBatch updatesBatch;
+- for (const auto &[tag, pair] : updatesRegistry_) {
+- const auto &[shadowNode, props] = pair;
+- updatesBatch.emplace_back(shadowNode, props);
+- }
+- return updatesBatch;
+-}
+-
+ void UpdatesRegistry::collectProps(PropsMap &propsMap) {
+ std::lock_guard lock{mutex_};
+
+diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h
+index db03aa5..6f32763 100644
+--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h
++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h
+@@ -49,7 +49,6 @@ class UpdatesRegistry {
+
+ void flushUpdates(UpdatesBatch &updatesBatch);
+ void collectProps(PropsMap &propsMap);
+- UpdatesBatch getPendingUpdates();
+
+ protected:
+ mutable std::mutex mutex_;
+diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
+index 081fddf..05acd4f 100644
+--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
+@@ -35,82 +35,6 @@ static inline std::shared_ptr shadowNodeFromValue(
+ }
+ #endif
+
+-namespace {
+-
+-#ifdef ANDROID
+-constexpr bool shouldUseSynchronousUpdatesInPerformOperations() {
+- return StaticFeatureFlags::getFlag("ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS");
+-}
+-#else
+-constexpr bool shouldUseSynchronousUpdatesInPerformOperations() {
+- return false;
+-}
+-#endif
+-
+-std::pair partitionUpdates(
+- const UpdatesBatch &updatesBatch,
+- const std::unordered_set &synchronousPropNames,
+- const bool shouldRequireIntegerColors = false,
+- const bool allowPartialViews = false) {
+- UpdatesBatch synchronousUpdatesBatch;
+- UpdatesBatch shadowTreeUpdatesBatch;
+-
+- for (const auto &[shadowNode, props] : updatesBatch) {
+- if (allowPartialViews) {
+- folly::dynamic synchronousProps = folly::dynamic::object();
+- folly::dynamic shadowTreeProps = folly::dynamic::object();
+-
+- for (const auto &[key, value] : props.items()) {
+- const auto keyStr = key.asString();
+- const bool isColorProp =
+- keyStr == "color" || keyStr.find("Color") != std::string::npos;
+- const bool isSynchronous = synchronousPropNames.contains(keyStr) &&
+- (!shouldRequireIntegerColors || !isColorProp || value.isInt());
+- if (isSynchronous) {
+- synchronousProps[keyStr] = value;
+- } else {
+- shadowTreeProps[keyStr] = value;
+- }
+- }
+-
+- if (!synchronousProps.empty()) {
+- synchronousUpdatesBatch.emplace_back(
+- shadowNode, std::move(synchronousProps));
+- }
+-
+- if (!shadowTreeProps.empty()) {
+- shadowTreeUpdatesBatch.emplace_back(
+- shadowNode, std::move(shadowTreeProps));
+- }
+- } else {
+- bool hasOnlySynchronousProps = true;
+-
+- for (const auto &[key, value] : props.items()) {
+- const auto keyStr = key.asString();
+- const bool isColorProp =
+- keyStr == "color" || keyStr.find("Color") != std::string::npos;
+- const bool isSynchronous = synchronousPropNames.contains(keyStr) &&
+- (!shouldRequireIntegerColors || !isColorProp || value.isInt());
+- if (!isSynchronous) {
+- hasOnlySynchronousProps = false;
+- break;
+- }
+- }
+-
+- if (hasOnlySynchronousProps) {
+- synchronousUpdatesBatch.emplace_back(shadowNode, props);
+- } else {
+- shadowTreeUpdatesBatch.emplace_back(shadowNode, props);
+- }
+- }
+- }
+-
+- return {
+- std::move(synchronousUpdatesBatch), std::move(shadowTreeUpdatesBatch)};
+-}
+-
+-} // namespace
+-
+ ReanimatedModuleProxy::ReanimatedModuleProxy(
+ const std::shared_ptr &workletsModuleProxy,
+ jsi::Runtime &rnRuntime,
+@@ -768,428 +692,434 @@ void ReanimatedModuleProxy::performOperations() {
+
+ shouldUpdateCssAnimations_ = false;
+
+- if constexpr (shouldUseSynchronousUpdatesInPerformOperations()) {
+- applySynchronousUpdates(updatesBatch);
+- }
+-
+- if ((updatesBatch.size() > 0) &&
+- updatesRegistryManager_->shouldReanimatedSkipCommit()) {
+- updatesRegistryManager_->pleaseCommitAfterPause();
+- }
+- }
+-
+- if (updatesRegistryManager_->shouldReanimatedSkipCommit()) {
+- // It may happen that `performOperations` is called on the UI thread
+- // while React Native tries to commit a new tree on the JS thread.
+- // In this case, we should skip the commit here and let React Native do
+- // it. The commit will include the current values from the updates manager
+- // which will be applied in ReanimatedCommitHook.
+- return;
+- }
+-
+- commitUpdates(rt, updatesBatch);
+-
+- // Clear the entire cache after the commit
+- // (we don't know if the view is updated from outside of Reanimated
+- // so we have to clear the entire cache)
+- viewStylesRepository_->clearNodesCache();
+-}
+-
+-void ReanimatedModuleProxy::performNonLayoutOperations() {
+- ReanimatedSystraceSection s(
+- "ReanimatedModuleProxy::performNonLayoutOperations");
+-
+- UpdatesBatch updatesBatch = animatedPropsRegistry_->getPendingUpdates();
+-
+- applySynchronousUpdates(updatesBatch, true);
+-}
+-
+-void ReanimatedModuleProxy::applySynchronousUpdates(
+- UpdatesBatch &updatesBatch,
+- const bool allowPartialUpdates) {
+ #ifdef ANDROID
+- static const std::unordered_set synchronousProps = {
+- "opacity",
+- "elevation",
+- "zIndex",
+- // "shadowOpacity", // not supported on Android
+- // "shadowRadius", // not supported on Android
+- "backgroundColor",
+- // "color", // TODO: fix animating color of Animated.Text,
+- "tintColor",
+- "borderRadius",
+- "borderTopLeftRadius",
+- "borderTopRightRadius",
+- "borderTopStartRadius",
+- "borderTopEndRadius",
+- "borderBottomLeftRadius",
+- "borderBottomRightRadius",
+- "borderBottomStartRadius",
+- "borderBottomEndRadius",
+- "borderStartStartRadius",
+- "borderStartEndRadius",
+- "borderEndStartRadius",
+- "borderEndEndRadius",
+- "borderColor",
+- "borderTopColor",
+- "borderBottomColor",
+- "borderLeftColor",
+- "borderRightColor",
+- "borderStartColor",
+- "borderEndColor",
+- "transform",
+- };
+-
+- // NOTE: Keep in sync with NativeProxy.java
+- static constexpr auto CMD_START_OF_VIEW = 1;
+- static constexpr auto CMD_START_OF_TRANSFORM = 2;
+- static constexpr auto CMD_END_OF_TRANSFORM = 3;
+- static constexpr auto CMD_END_OF_VIEW = 4;
+-
+- static constexpr auto CMD_OPACITY = 10;
+- static constexpr auto CMD_ELEVATION = 11;
+- static constexpr auto CMD_Z_INDEX = 12;
+- static constexpr auto CMD_SHADOW_OPACITY = 13;
+- static constexpr auto CMD_SHADOW_RADIUS = 14;
+- static constexpr auto CMD_BACKGROUND_COLOR = 15;
+- static constexpr auto CMD_COLOR = 16;
+- static constexpr auto CMD_TINT_COLOR = 17;
+-
+- static constexpr auto CMD_BORDER_RADIUS = 20;
+- static constexpr auto CMD_BORDER_TOP_LEFT_RADIUS = 21;
+- static constexpr auto CMD_BORDER_TOP_RIGHT_RADIUS = 22;
+- static constexpr auto CMD_BORDER_TOP_START_RADIUS = 23;
+- static constexpr auto CMD_BORDER_TOP_END_RADIUS = 24;
+- static constexpr auto CMD_BORDER_BOTTOM_LEFT_RADIUS = 25;
+- static constexpr auto CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26;
+- static constexpr auto CMD_BORDER_BOTTOM_START_RADIUS = 27;
+- static constexpr auto CMD_BORDER_BOTTOM_END_RADIUS = 28;
+- static constexpr auto CMD_BORDER_START_START_RADIUS = 29;
+- static constexpr auto CMD_BORDER_START_END_RADIUS = 30;
+- static constexpr auto CMD_BORDER_END_START_RADIUS = 31;
+- static constexpr auto CMD_BORDER_END_END_RADIUS = 32;
+-
+- static constexpr auto CMD_BORDER_COLOR = 40;
+- static constexpr auto CMD_BORDER_TOP_COLOR = 41;
+- static constexpr auto CMD_BORDER_BOTTOM_COLOR = 42;
+- static constexpr auto CMD_BORDER_LEFT_COLOR = 43;
+- static constexpr auto CMD_BORDER_RIGHT_COLOR = 44;
+- static constexpr auto CMD_BORDER_START_COLOR = 45;
+- static constexpr auto CMD_BORDER_END_COLOR = 46;
+-
+- static constexpr auto CMD_TRANSFORM_TRANSLATE_X = 100;
+- static constexpr auto CMD_TRANSFORM_TRANSLATE_Y = 101;
+- static constexpr auto CMD_TRANSFORM_SCALE = 102;
+- static constexpr auto CMD_TRANSFORM_SCALE_X = 103;
+- static constexpr auto CMD_TRANSFORM_SCALE_Y = 104;
+- static constexpr auto CMD_TRANSFORM_ROTATE = 105;
+- static constexpr auto CMD_TRANSFORM_ROTATE_X = 106;
+- static constexpr auto CMD_TRANSFORM_ROTATE_Y = 107;
+- static constexpr auto CMD_TRANSFORM_ROTATE_Z = 108;
+- static constexpr auto CMD_TRANSFORM_SKEW_X = 109;
+- static constexpr auto CMD_TRANSFORM_SKEW_Y = 110;
+- static constexpr auto CMD_TRANSFORM_MATRIX = 111;
+- static constexpr auto CMD_TRANSFORM_PERSPECTIVE = 112;
++ if constexpr (StaticFeatureFlags::getFlag(
++ "ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS")) {
++ static const std::unordered_set synchronousProps = {
++ "opacity",
++ "elevation",
++ "zIndex",
++ // "shadowOpacity", // not supported on Android
++ // "shadowRadius", // not supported on Android
++ "backgroundColor",
++ // "color", // TODO: fix animating color of Animated.Text
++ "tintColor",
++ "borderRadius",
++ "borderTopLeftRadius",
++ "borderTopRightRadius",
++ "borderTopStartRadius",
++ "borderTopEndRadius",
++ "borderBottomLeftRadius",
++ "borderBottomRightRadius",
++ "borderBottomStartRadius",
++ "borderBottomEndRadius",
++ "borderStartStartRadius",
++ "borderStartEndRadius",
++ "borderEndStartRadius",
++ "borderEndEndRadius",
++ "borderColor",
++ "borderTopColor",
++ "borderBottomColor",
++ "borderLeftColor",
++ "borderRightColor",
++ "borderStartColor",
++ "borderEndColor",
++ "transform",
++ };
+
+- static constexpr auto CMD_UNIT_DEG = 200;
+- static constexpr auto CMD_UNIT_RAD = 201;
+- static constexpr auto CMD_UNIT_PX = 202;
+- static constexpr auto CMD_UNIT_PERCENT = 203;
++ // NOTE: Keep in sync with NativeProxy.java
++ static constexpr auto CMD_START_OF_VIEW = 1;
++ static constexpr auto CMD_START_OF_TRANSFORM = 2;
++ static constexpr auto CMD_END_OF_TRANSFORM = 3;
++ static constexpr auto CMD_END_OF_VIEW = 4;
++
++ static constexpr auto CMD_OPACITY = 10;
++ static constexpr auto CMD_ELEVATION = 11;
++ static constexpr auto CMD_Z_INDEX = 12;
++ static constexpr auto CMD_SHADOW_OPACITY = 13;
++ static constexpr auto CMD_SHADOW_RADIUS = 14;
++ static constexpr auto CMD_BACKGROUND_COLOR = 15;
++ static constexpr auto CMD_COLOR = 16;
++ static constexpr auto CMD_TINT_COLOR = 17;
++
++ static constexpr auto CMD_BORDER_RADIUS = 20;
++ static constexpr auto CMD_BORDER_TOP_LEFT_RADIUS = 21;
++ static constexpr auto CMD_BORDER_TOP_RIGHT_RADIUS = 22;
++ static constexpr auto CMD_BORDER_TOP_START_RADIUS = 23;
++ static constexpr auto CMD_BORDER_TOP_END_RADIUS = 24;
++ static constexpr auto CMD_BORDER_BOTTOM_LEFT_RADIUS = 25;
++ static constexpr auto CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26;
++ static constexpr auto CMD_BORDER_BOTTOM_START_RADIUS = 27;
++ static constexpr auto CMD_BORDER_BOTTOM_END_RADIUS = 28;
++ static constexpr auto CMD_BORDER_START_START_RADIUS = 29;
++ static constexpr auto CMD_BORDER_START_END_RADIUS = 30;
++ static constexpr auto CMD_BORDER_END_START_RADIUS = 31;
++ static constexpr auto CMD_BORDER_END_END_RADIUS = 32;
++
++ static constexpr auto CMD_BORDER_COLOR = 40;
++ static constexpr auto CMD_BORDER_TOP_COLOR = 41;
++ static constexpr auto CMD_BORDER_BOTTOM_COLOR = 42;
++ static constexpr auto CMD_BORDER_LEFT_COLOR = 43;
++ static constexpr auto CMD_BORDER_RIGHT_COLOR = 44;
++ static constexpr auto CMD_BORDER_START_COLOR = 45;
++ static constexpr auto CMD_BORDER_END_COLOR = 46;
++
++ static constexpr auto CMD_TRANSFORM_TRANSLATE_X = 100;
++ static constexpr auto CMD_TRANSFORM_TRANSLATE_Y = 101;
++ static constexpr auto CMD_TRANSFORM_SCALE = 102;
++ static constexpr auto CMD_TRANSFORM_SCALE_X = 103;
++ static constexpr auto CMD_TRANSFORM_SCALE_Y = 104;
++ static constexpr auto CMD_TRANSFORM_ROTATE = 105;
++ static constexpr auto CMD_TRANSFORM_ROTATE_X = 106;
++ static constexpr auto CMD_TRANSFORM_ROTATE_Y = 107;
++ static constexpr auto CMD_TRANSFORM_ROTATE_Z = 108;
++ static constexpr auto CMD_TRANSFORM_SKEW_X = 109;
++ static constexpr auto CMD_TRANSFORM_SKEW_Y = 110;
++ static constexpr auto CMD_TRANSFORM_MATRIX = 111;
++ static constexpr auto CMD_TRANSFORM_PERSPECTIVE = 112;
+
+- const auto propNameToCommand = [](const std::string &name) {
+- if (name == "opacity")
+- return CMD_OPACITY;
++ static constexpr auto CMD_UNIT_DEG = 200;
++ static constexpr auto CMD_UNIT_RAD = 201;
++ static constexpr auto CMD_UNIT_PX = 202;
++ static constexpr auto CMD_UNIT_PERCENT = 203;
+
+- if (name == "elevation")
+- return CMD_ELEVATION;
++ const auto propNameToCommand = [](const std::string &name) {
++ if (name == "opacity")
++ return CMD_OPACITY;
+
+- if (name == "zIndex")
+- return CMD_Z_INDEX;
++ if (name == "elevation")
++ return CMD_ELEVATION;
+
+- if (name == "shadowOpacity")
+- return CMD_SHADOW_OPACITY;
++ if (name == "zIndex")
++ return CMD_Z_INDEX;
+
+- if (name == "shadowRadius")
+- return CMD_SHADOW_RADIUS;
++ if (name == "shadowOpacity")
++ return CMD_SHADOW_OPACITY;
+
+- if (name == "backgroundColor")
+- return CMD_BACKGROUND_COLOR;
++ if (name == "shadowRadius")
++ return CMD_SHADOW_RADIUS;
+
+- if (name == "color")
+- return CMD_COLOR;
++ if (name == "backgroundColor")
++ return CMD_BACKGROUND_COLOR;
+
+- if (name == "tintColor")
+- return CMD_TINT_COLOR;
++ if (name == "color")
++ return CMD_COLOR;
+
+- if (name == "borderRadius")
+- return CMD_BORDER_RADIUS;
++ if (name == "tintColor")
++ return CMD_TINT_COLOR;
+
+- if (name == "borderTopLeftRadius")
+- return CMD_BORDER_TOP_LEFT_RADIUS;
++ if (name == "borderRadius")
++ return CMD_BORDER_RADIUS;
+
+- if (name == "borderTopRightRadius")
+- return CMD_BORDER_TOP_RIGHT_RADIUS;
++ if (name == "borderTopLeftRadius")
++ return CMD_BORDER_TOP_LEFT_RADIUS;
+
+- if (name == "borderTopStartRadius")
+- return CMD_BORDER_TOP_START_RADIUS;
++ if (name == "borderTopRightRadius")
++ return CMD_BORDER_TOP_RIGHT_RADIUS;
+
+- if (name == "borderTopEndRadius")
+- return CMD_BORDER_TOP_END_RADIUS;
++ if (name == "borderTopStartRadius")
++ return CMD_BORDER_TOP_START_RADIUS;
+
+- if (name == "borderBottomLeftRadius")
+- return CMD_BORDER_BOTTOM_LEFT_RADIUS;
++ if (name == "borderTopEndRadius")
++ return CMD_BORDER_TOP_END_RADIUS;
+
+- if (name == "borderBottomRightRadius")
+- return CMD_BORDER_BOTTOM_RIGHT_RADIUS;
+-
+- if (name == "borderBottomStartRadius")
+- return CMD_BORDER_BOTTOM_START_RADIUS;
++ if (name == "borderBottomLeftRadius")
++ return CMD_BORDER_BOTTOM_LEFT_RADIUS;
+
+- if (name == "borderBottomEndRadius")
+- return CMD_BORDER_BOTTOM_END_RADIUS;
++ if (name == "borderBottomRightRadius")
++ return CMD_BORDER_BOTTOM_RIGHT_RADIUS;
++
++ if (name == "borderBottomStartRadius")
++ return CMD_BORDER_BOTTOM_START_RADIUS;
+
+- if (name == "borderStartStartRadius")
+- return CMD_BORDER_START_START_RADIUS;
++ if (name == "borderBottomEndRadius")
++ return CMD_BORDER_BOTTOM_END_RADIUS;
+
+- if (name == "borderStartEndRadius")
+- return CMD_BORDER_START_END_RADIUS;
++ if (name == "borderStartStartRadius")
++ return CMD_BORDER_START_START_RADIUS;
+
+- if (name == "borderEndStartRadius")
+- return CMD_BORDER_END_START_RADIUS;
++ if (name == "borderStartEndRadius")
++ return CMD_BORDER_START_END_RADIUS;
+
+- if (name == "borderEndEndRadius")
+- return CMD_BORDER_END_END_RADIUS;
++ if (name == "borderEndStartRadius")
++ return CMD_BORDER_END_START_RADIUS;
+
+- if (name == "borderColor")
+- return CMD_BORDER_COLOR;
++ if (name == "borderEndEndRadius")
++ return CMD_BORDER_END_END_RADIUS;
+
+- if (name == "borderTopColor")
+- return CMD_BORDER_TOP_COLOR;
++ if (name == "borderColor")
++ return CMD_BORDER_COLOR;
+
+- if (name == "borderBottomColor")
+- return CMD_BORDER_BOTTOM_COLOR;
++ if (name == "borderTopColor")
++ return CMD_BORDER_TOP_COLOR;
+
+- if (name == "borderLeftColor")
+- return CMD_BORDER_LEFT_COLOR;
++ if (name == "borderBottomColor")
++ return CMD_BORDER_BOTTOM_COLOR;
+
+- if (name == "borderRightColor")
+- return CMD_BORDER_RIGHT_COLOR;
++ if (name == "borderLeftColor")
++ return CMD_BORDER_LEFT_COLOR;
+
+- if (name == "borderStartColor")
+- return CMD_BORDER_START_COLOR;
++ if (name == "borderRightColor")
++ return CMD_BORDER_RIGHT_COLOR;
+
+- if (name == "borderEndColor")
+- return CMD_BORDER_END_COLOR;
++ if (name == "borderStartColor")
++ return CMD_BORDER_START_COLOR;
+
+- if (name == "transform")
+- return CMD_START_OF_TRANSFORM; // TODO: use CMD_TRANSFORM?
++ if (name == "borderEndColor")
++ return CMD_BORDER_END_COLOR;
+
+- throw std::runtime_error("[Reanimated] Unsupported style: " + name);
+- };
++ if (name == "transform")
++ return CMD_START_OF_TRANSFORM; // TODO: use CMD_TRANSFORM?
+
+- const auto transformNameToCommand = [](const std::string &name) {
+- if (name == "translateX")
+- return CMD_TRANSFORM_TRANSLATE_X;
++ throw std::runtime_error("[Reanimated] Unsupported style: " + name);
++ };
+
+- if (name == "translateY")
+- return CMD_TRANSFORM_TRANSLATE_Y;
++ const auto transformNameToCommand = [](const std::string &name) {
++ if (name == "translateX")
++ return CMD_TRANSFORM_TRANSLATE_X;
+
+- if (name == "scale")
+- return CMD_TRANSFORM_SCALE;
++ if (name == "translateY")
++ return CMD_TRANSFORM_TRANSLATE_Y;
+
+- if (name == "scaleX")
+- return CMD_TRANSFORM_SCALE_X;
++ if (name == "scale")
++ return CMD_TRANSFORM_SCALE;
+
+- if (name == "scaleY")
+- return CMD_TRANSFORM_SCALE_Y;
++ if (name == "scaleX")
++ return CMD_TRANSFORM_SCALE_X;
+
+- if (name == "rotate")
+- return CMD_TRANSFORM_ROTATE;
++ if (name == "scaleY")
++ return CMD_TRANSFORM_SCALE_Y;
+
+- if (name == "rotateX")
+- return CMD_TRANSFORM_ROTATE_X;
++ if (name == "rotate")
++ return CMD_TRANSFORM_ROTATE;
+
+- if (name == "rotateY")
+- return CMD_TRANSFORM_ROTATE_Y;
++ if (name == "rotateX")
++ return CMD_TRANSFORM_ROTATE_X;
+
+- if (name == "rotateZ")
+- return CMD_TRANSFORM_ROTATE_Z;
++ if (name == "rotateY")
++ return CMD_TRANSFORM_ROTATE_Y;
+
+- if (name == "skewX")
+- return CMD_TRANSFORM_SKEW_X;
++ if (name == "rotateZ")
++ return CMD_TRANSFORM_ROTATE_Z;
+
+- if (name == "skewY")
+- return CMD_TRANSFORM_SKEW_Y;
++ if (name == "skewX")
++ return CMD_TRANSFORM_SKEW_X;
+
+- if (name == "matrix")
+- return CMD_TRANSFORM_MATRIX;
++ if (name == "skewY")
++ return CMD_TRANSFORM_SKEW_Y;
+
+- if (name == "perspective")
+- return CMD_TRANSFORM_PERSPECTIVE;
++ if (name == "matrix")
++ return CMD_TRANSFORM_MATRIX;
+
+- throw std::runtime_error("[Reanimated] Unsupported transform: " + name);
+- };
++ if (name == "perspective")
++ return CMD_TRANSFORM_PERSPECTIVE;
+
+- auto [synchronousUpdatesBatch, shadowTreeUpdatesBatch] = partitionUpdates(
+- updatesBatch, synchronousProps, true, allowPartialUpdates);
+-
+- if (!synchronousUpdatesBatch.empty()) {
+- std::vector intBuffer;
+- std::vector doubleBuffer;
+- intBuffer.reserve(1024);
+- doubleBuffer.reserve(1024);
+-
+- for (const auto &[shadowNode, props] : synchronousUpdatesBatch) {
+- intBuffer.push_back(CMD_START_OF_VIEW);
+- intBuffer.push_back(shadowNode->getTag());
+- for (const auto &[key, value] : props.items()) {
+- const auto command = propNameToCommand(key.getString());
+- switch (command) {
+- case CMD_OPACITY:
+- case CMD_ELEVATION:
+- case CMD_Z_INDEX:
+- case CMD_SHADOW_OPACITY:
+- case CMD_SHADOW_RADIUS:
+- intBuffer.push_back(command);
+- doubleBuffer.push_back(value.asDouble());
+- break;
++ throw std::runtime_error("[Reanimated] Unsupported transform: " + name);
++ };
+
+- case CMD_BACKGROUND_COLOR:
+- case CMD_COLOR:
+- case CMD_TINT_COLOR:
+- case CMD_BORDER_COLOR:
+- case CMD_BORDER_TOP_COLOR:
+- case CMD_BORDER_BOTTOM_COLOR:
+- case CMD_BORDER_LEFT_COLOR:
+- case CMD_BORDER_RIGHT_COLOR:
+- case CMD_BORDER_START_COLOR:
+- case CMD_BORDER_END_COLOR:
+- intBuffer.push_back(command);
+- intBuffer.push_back(value.asInt());
+- break;
++ UpdatesBatch synchronousUpdatesBatch, shadowTreeUpdatesBatch;
+
+- case CMD_BORDER_RADIUS:
+- case CMD_BORDER_TOP_LEFT_RADIUS:
+- case CMD_BORDER_TOP_RIGHT_RADIUS:
+- case CMD_BORDER_TOP_START_RADIUS:
+- case CMD_BORDER_TOP_END_RADIUS:
+- case CMD_BORDER_BOTTOM_LEFT_RADIUS:
+- case CMD_BORDER_BOTTOM_RIGHT_RADIUS:
+- case CMD_BORDER_BOTTOM_START_RADIUS:
+- case CMD_BORDER_BOTTOM_END_RADIUS:
+- case CMD_BORDER_START_START_RADIUS:
+- case CMD_BORDER_START_END_RADIUS:
+- case CMD_BORDER_END_START_RADIUS:
+- case CMD_BORDER_END_END_RADIUS:
+- intBuffer.push_back(command);
+- if (value.isDouble()) {
+- intBuffer.push_back(CMD_UNIT_PX);
+- doubleBuffer.push_back(value.getDouble());
+- } else if (value.isString()) {
+- const auto &valueStr = value.getString();
+- if (!valueStr.ends_with("%")) {
+- throw std::runtime_error(
+- "[Reanimated] Border radius string must be a percentage");
+- }
+- intBuffer.push_back(CMD_UNIT_PERCENT);
+- doubleBuffer.push_back(std::stof(valueStr.substr(0, -1)));
+- } else {
+- throw std::runtime_error(
+- "[Reanimated] Border radius value must be either a number or a string");
+- }
++ for (const auto &[shadowNode, props] : updatesBatch) {
++ bool hasOnlySynchronousProps = true;
++ for (const auto &key : props.keys()) {
++ const auto keyStr = key.asString();
++ if (!synchronousProps.contains(keyStr)) {
++ hasOnlySynchronousProps = false;
+ break;
++ }
++ }
++ if (hasOnlySynchronousProps) {
++ synchronousUpdatesBatch.emplace_back(shadowNode, props);
++ } else {
++ shadowTreeUpdatesBatch.emplace_back(shadowNode, props);
++ }
++ }
+
+- case CMD_START_OF_TRANSFORM:
+- intBuffer.push_back(command);
+- react_native_assert(
+- value.isArray() &&
+- "[Reanimated] Transform value must be an array");
+- for (const auto &item : value) {
+- react_native_assert(
+- item.isObject() &&
+- "[Reanimated] Transform array item must be an object");
+- react_native_assert(
+- item.size() == 1 &&
+- "[Reanimated] Transform array item must have exactly one key-value pair");
+- const auto transformCommand =
+- transformNameToCommand(item.keys().begin()->getString());
+- const auto &transformValue = *item.values().begin();
+- switch (transformCommand) {
+- case CMD_TRANSFORM_SCALE:
+- case CMD_TRANSFORM_SCALE_X:
+- case CMD_TRANSFORM_SCALE_Y:
+- case CMD_TRANSFORM_PERSPECTIVE: {
+- intBuffer.push_back(transformCommand);
+- doubleBuffer.push_back(transformValue.asDouble());
+- break;
+- }
+- case CMD_TRANSFORM_TRANSLATE_X:
+- case CMD_TRANSFORM_TRANSLATE_Y: {
+- intBuffer.push_back(transformCommand);
+- if (transformValue.isDouble()) {
+- intBuffer.push_back(CMD_UNIT_PX);
+- doubleBuffer.push_back(transformValue.getDouble());
+- } else if (transformValue.isString()) {
+- const auto &transformValueStr = transformValue.getString();
+- if (!transformValueStr.ends_with("%")) {
+- throw std::runtime_error(
+- "[Reanimated] String translate must be a percentage");
+- }
+- intBuffer.push_back(CMD_UNIT_PERCENT);
+- doubleBuffer.push_back(
+- std::stof(transformValueStr.substr(0, -1)));
+- } else {
++ if (!synchronousUpdatesBatch.empty()) {
++ std::vector intBuffer;
++ std::vector doubleBuffer;
++ intBuffer.reserve(1024);
++ doubleBuffer.reserve(1024);
++
++ for (const auto &[shadowNode, props] : synchronousUpdatesBatch) {
++ intBuffer.push_back(CMD_START_OF_VIEW);
++ intBuffer.push_back(shadowNode->getTag());
++ for (const auto &[key, value] : props.items()) {
++ const auto command = propNameToCommand(key.getString());
++ switch (command) {
++ case CMD_OPACITY:
++ case CMD_ELEVATION:
++ case CMD_Z_INDEX:
++ case CMD_SHADOW_OPACITY:
++ case CMD_SHADOW_RADIUS:
++ intBuffer.push_back(command);
++ doubleBuffer.push_back(value.asDouble());
++ break;
++
++ case CMD_BACKGROUND_COLOR:
++ case CMD_COLOR:
++ case CMD_TINT_COLOR:
++ case CMD_BORDER_COLOR:
++ case CMD_BORDER_TOP_COLOR:
++ case CMD_BORDER_BOTTOM_COLOR:
++ case CMD_BORDER_LEFT_COLOR:
++ case CMD_BORDER_RIGHT_COLOR:
++ case CMD_BORDER_START_COLOR:
++ case CMD_BORDER_END_COLOR:
++ intBuffer.push_back(command);
++ intBuffer.push_back(value.asInt());
++ break;
++
++ case CMD_BORDER_RADIUS:
++ case CMD_BORDER_TOP_LEFT_RADIUS:
++ case CMD_BORDER_TOP_RIGHT_RADIUS:
++ case CMD_BORDER_TOP_START_RADIUS:
++ case CMD_BORDER_TOP_END_RADIUS:
++ case CMD_BORDER_BOTTOM_LEFT_RADIUS:
++ case CMD_BORDER_BOTTOM_RIGHT_RADIUS:
++ case CMD_BORDER_BOTTOM_START_RADIUS:
++ case CMD_BORDER_BOTTOM_END_RADIUS:
++ case CMD_BORDER_START_START_RADIUS:
++ case CMD_BORDER_START_END_RADIUS:
++ case CMD_BORDER_END_START_RADIUS:
++ case CMD_BORDER_END_END_RADIUS:
++ intBuffer.push_back(command);
++ if (value.isDouble()) {
++ intBuffer.push_back(CMD_UNIT_PX);
++ doubleBuffer.push_back(value.getDouble());
++ } else if (value.isString()) {
++ const auto &valueStr = value.getString();
++ if (!valueStr.ends_with("%")) {
+ throw std::runtime_error(
+- "[Reanimated] Translate value must be either a number or a string");
++ "[Reanimated] Border radius string must be a percentage");
+ }
+- break;
++ intBuffer.push_back(CMD_UNIT_PERCENT);
++ doubleBuffer.push_back(std::stof(valueStr.substr(0, -1)));
++ } else {
++ throw std::runtime_error(
++ "[Reanimated] Border radius value must be either a number or a string");
+ }
+- case CMD_TRANSFORM_ROTATE:
+- case CMD_TRANSFORM_ROTATE_X:
+- case CMD_TRANSFORM_ROTATE_Y:
+- case CMD_TRANSFORM_ROTATE_Z:
+- case CMD_TRANSFORM_SKEW_X:
+- case CMD_TRANSFORM_SKEW_Y: {
+- const auto &transformValueStr = transformValue.getString();
+- intBuffer.push_back(transformCommand);
+- if (transformValueStr.ends_with("deg")) {
+- intBuffer.push_back(CMD_UNIT_DEG);
+- } else if (transformValueStr.ends_with("rad")) {
+- intBuffer.push_back(CMD_UNIT_RAD);
+- } else {
+- throw std::runtime_error(
+- "[Reanimated] Unsupported rotation unit: " +
+- transformValueStr);
+- }
+- doubleBuffer.push_back(
+- std::stof(transformValueStr.substr(0, -3)));
+- break;
+- }
+- case CMD_TRANSFORM_MATRIX: {
+- intBuffer.push_back(transformCommand);
++ break;
++
++ case CMD_START_OF_TRANSFORM:
++ intBuffer.push_back(command);
++ react_native_assert(
++ value.isArray() &&
++ "[Reanimated] Transform value must be an array");
++ for (const auto &item : value) {
++ react_native_assert(
++ item.isObject() &&
++ "[Reanimated] Transform array item must be an object");
+ react_native_assert(
+- transformValue.isArray() &&
+- "[Reanimated] Matrix must be an array");
+- int size = transformValue.size();
+- intBuffer.push_back(size);
+- for (int i = 0; i < size; i++) {
+- doubleBuffer.push_back(transformValue[i].asDouble());
++ item.size() == 1 &&
++ "[Reanimated] Transform array item must have exactly one key-value pair");
++ const auto transformCommand =
++ transformNameToCommand(item.keys().begin()->getString());
++ const auto &transformValue = *item.values().begin();
++ switch (transformCommand) {
++ case CMD_TRANSFORM_SCALE:
++ case CMD_TRANSFORM_SCALE_X:
++ case CMD_TRANSFORM_SCALE_Y:
++ case CMD_TRANSFORM_PERSPECTIVE: {
++ intBuffer.push_back(transformCommand);
++ doubleBuffer.push_back(transformValue.asDouble());
++ break;
++ }
++
++ case CMD_TRANSFORM_TRANSLATE_X:
++ case CMD_TRANSFORM_TRANSLATE_Y: {
++ intBuffer.push_back(transformCommand);
++ if (transformValue.isDouble()) {
++ intBuffer.push_back(CMD_UNIT_PX);
++ doubleBuffer.push_back(transformValue.getDouble());
++ } else if (transformValue.isString()) {
++ const auto &transformValueStr =
++ transformValue.getString();
++ if (!transformValueStr.ends_with("%")) {
++ throw std::runtime_error(
++ "[Reanimated] String translate must be a percentage");
++ }
++ intBuffer.push_back(CMD_UNIT_PERCENT);
++ doubleBuffer.push_back(
++ std::stof(transformValueStr.substr(0, -1)));
++ } else {
++ throw std::runtime_error(
++ "[Reanimated] Translate value must be either a number or a string");
++ }
++ break;
++ }
++
++ case CMD_TRANSFORM_ROTATE:
++ case CMD_TRANSFORM_ROTATE_X:
++ case CMD_TRANSFORM_ROTATE_Y:
++ case CMD_TRANSFORM_ROTATE_Z:
++ case CMD_TRANSFORM_SKEW_X:
++ case CMD_TRANSFORM_SKEW_Y: {
++ const auto &transformValueStr =
++ transformValue.getString();
++ intBuffer.push_back(transformCommand);
++ if (transformValueStr.ends_with("deg")) {
++ intBuffer.push_back(CMD_UNIT_DEG);
++ } else if (transformValueStr.ends_with("rad")) {
++ intBuffer.push_back(CMD_UNIT_RAD);
++ } else {
++ throw std::runtime_error(
++ "[Reanimated] Unsupported rotation unit: " +
++ transformValueStr);
++ }
++ doubleBuffer.push_back(
++ std::stof(transformValueStr.substr(0, -3)));
++ break;
++ }
++
++ case CMD_TRANSFORM_MATRIX: {
++ intBuffer.push_back(transformCommand);
++ react_native_assert(
++ transformValue.isArray() &&
++ "[Reanimated] Matrix must be an array");
++ int size = transformValue.size();
++ intBuffer.push_back(size);
++ for (int i = 0; i < size; i++) {
++ doubleBuffer.push_back(transformValue[i].asDouble());
++ }
++ break;
++ }
+ }
+- break;
+ }
+- }
++ intBuffer.push_back(CMD_END_OF_TRANSFORM);
++ break;
+ }
+- intBuffer.push_back(CMD_END_OF_TRANSFORM);
+- break;
++ }
++ intBuffer.push_back(CMD_END_OF_VIEW);
+ }
++ synchronouslyUpdateUIPropsFunction_(intBuffer, doubleBuffer);
+ }
+- intBuffer.push_back(CMD_END_OF_VIEW);
++
++ updatesBatch = std::move(shadowTreeUpdatesBatch);
++ }
++#endif // ANDROID
++
++ if ((updatesBatch.size() > 0) &&
++ updatesRegistryManager_->shouldReanimatedSkipCommit()) {
++ updatesRegistryManager_->pleaseCommitAfterPause();
+ }
+- synchronouslyUpdateUIPropsFunction_(intBuffer, doubleBuffer);
+ }
+
+- updatesBatch = std::move(shadowTreeUpdatesBatch);
+-#endif // ANDROID
++ if (updatesRegistryManager_->shouldReanimatedSkipCommit()) {
++ // It may happen that `performOperations` is called on the UI thread
++ // while React Native tries to commit a new tree on the JS thread.
++ // In this case, we should skip the commit here and let React Native do
++ // it. The commit will include the current values from the updates manager
++ // which will be applied in ReanimatedCommitHook.
++ return;
++ }
++
++ commitUpdates(rt, updatesBatch);
++
++ // Clear the entire cache after the commit
++ // (we don't know if the view is updated from outside of Reanimated
++ // so we have to clear the entire cache)
++ viewStylesRepository_->clearNodesCache();
+ }
+
+ void ReanimatedModuleProxy::requestFlushRegistry() {
+diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h
+index 4d86766..9e0599a 100644
+--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h
++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h
+@@ -118,7 +118,6 @@ class ReanimatedModuleProxy
+ double getCssTimestamp();
+
+ void performOperations();
+- void performNonLayoutOperations();
+
+ void setViewStyle(
+ jsi::Runtime &rt,
+@@ -219,9 +218,6 @@ class ReanimatedModuleProxy
+
+ private:
+ void commitUpdates(jsi::Runtime &rt, const UpdatesBatch &updatesBatch);
+- void applySynchronousUpdates(
+- UpdatesBatch &updatesBatch,
+- bool allowPartialUpdates = false);
+
+ const bool isReducedMotion_;
+ bool shouldFlushRegistry_ = false;