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;