From 5e017653b134777178e6181410d2d45428ecbef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 11 Dec 2025 09:51:23 +0100 Subject: [PATCH 1/5] feat(example): add Rive to Reanimated shared value demo Demonstrates Rive animation driving React Native UI via data binding listeners. --- example/assets/rive/bouncing_ball.riv | Bin 0 -> 376 bytes .../exercisers/RiveToReactNativeExample.tsx | 220 ++++++++++++++++++ expo-example/assets/rive/bouncing_ball.riv | Bin 0 -> 376 bytes 3 files changed, 220 insertions(+) create mode 100644 example/assets/rive/bouncing_ball.riv create mode 100644 example/src/exercisers/RiveToReactNativeExample.tsx create mode 100644 expo-example/assets/rive/bouncing_ball.riv diff --git a/example/assets/rive/bouncing_ball.riv b/example/assets/rive/bouncing_ball.riv new file mode 100644 index 0000000000000000000000000000000000000000..d83155fa7251731bbbc68491b679c4d6cef6b2c3 GIT binary patch literal 376 zcmY*V%}PRH5Iy5PH#Nv0tT4h{)B{{Z!bLy+MM7;V+TZL#LTczGaM`wf^a8B{Nr+su zX(Jfw1M~uF{vhbGO??^!4V*b=hBGrTN6FKK6E`EfDgzm6k1Vmm8tZJZ$rjt}@RMEk z*yn&Fj)_T#!mpZ^SjMwfqT@P)C&esY)}xD)gazHX0epQcF|%P9#y1|+ zT#M4CbSR&cE~Q85QwEd~WlVurhk{@gYn!tt>5O%m&RPK@Tz1131Xe;G_~Qb9?1PGY zLHXZuBwTyyW=t8L+*4oK9sg^FHs_CR)3ySKX6j + {isLoading ? ( + + ) : riveFile ? ( + + ) : ( + {error || 'Unexpected error'} + )} + + ); +} + +function WithViewModelSetup({ file }: { file: RiveFile }) { + const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]); + const instance = useMemo( + () => viewModel?.createDefaultInstance(), + [viewModel] + ); + + if (!instance || !viewModel) { + return ( + + + {!viewModel + ? 'No view model found.' + : 'Failed to create view model instance'} + + + This demo requires a Rive file (bouncing_ball.riv) with:{'\n'} + {'\n'}• A ViewModel with a "ypos" number property{'\n'}• A bouncing + ball animation{'\n'}• Target-to-source binding from ball Y position to + ypos{'\n'} + {'\n'} + See Rive docs for data binding setup. + + + ); + } + + return ; +} + +function BouncingBallTracker({ + instance, + file, +}: { + instance: ViewModelInstance; + file: RiveFile; +}) { + const pointerY = useSharedValue(0); + + const yposProperty = useMemo( + () => instance.numberProperty('ypos'), + [instance] + ); + + useEffect(() => { + if (!yposProperty) return; + + yposProperty.addListener((value) => { + 'worklet'; + console.log('worklet:', _WORKLET, __RUNTIME_KIND); + pointerY.value = value; + return true; + }); + + return () => { + yposProperty.removeListeners(); + }; + }, [yposProperty, pointerY]); + + const pointerStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: pointerY.value }], + })); + + if (!yposProperty) { + return ( + + Property "ypos" not found + + Make sure the Rive file has a "ypos" number property in its ViewModel + with target-to-source binding from the ball's Y position. + + + ); + } + + return ( + + + Rive animation drives the ball position.{'\n'}React Native listens and + moves the blue pointer to track it. + + + No re-renders - using direct addListener + + + + + + + RN + + + + ); +} + +RiveToReactNativeExample.metadata = { + name: 'Rive → React Native', + description: + 'Demonstrates Rive animation driving React Native UI through data binding listeners', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + subtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginVertical: 10, + paddingHorizontal: 20, + }, + valueText: { + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 10, + color: '#333', + }, + contentContainer: { + position: 'relative', + height: 600, + width: 200, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: '#ccc', + }, + rive: { + width: 100, + height: 600, + }, + pointer: { + position: 'absolute', + top: -10, + right: 40, + flexDirection: 'row', + alignItems: 'center', + }, + pointerArrow: { + width: 0, + height: 0, + borderTopWidth: 10, + borderBottomWidth: 10, + borderRightWidth: 15, + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + borderRightColor: '#007AFF', + }, + pointerText: { + backgroundColor: '#007AFF', + color: '#fff', + fontSize: 12, + fontWeight: 'bold', + paddingHorizontal: 6, + paddingVertical: 4, + borderTopRightRadius: 4, + borderBottomRightRadius: 4, + }, + errorText: { + color: 'red', + textAlign: 'center', + fontSize: 16, + fontWeight: 'bold', + marginBottom: 10, + }, + instructionText: { + color: '#666', + textAlign: 'left', + fontSize: 14, + lineHeight: 22, + }, +}); diff --git a/expo-example/assets/rive/bouncing_ball.riv b/expo-example/assets/rive/bouncing_ball.riv new file mode 100644 index 0000000000000000000000000000000000000000..d83155fa7251731bbbc68491b679c4d6cef6b2c3 GIT binary patch literal 376 zcmY*V%}PRH5Iy5PH#Nv0tT4h{)B{{Z!bLy+MM7;V+TZL#LTczGaM`wf^a8B{Nr+su zX(Jfw1M~uF{vhbGO??^!4V*b=hBGrTN6FKK6E`EfDgzm6k1Vmm8tZJZ$rjt}@RMEk z*yn&Fj)_T#!mpZ^SjMwfqT@P)C&esY)}xD)gazHX0epQcF|%P9#y1|+ zT#M4CbSR&cE~Q85QwEd~WlVurhk{@gYn!tt>5O%m&RPK@Tz1131Xe;G_~Qb9?1PGY zLHXZuBwTyyW=t8L+*4oK9sg^FHs_CR)3ySKX6j Date: Mon, 22 Dec 2025 14:08:28 +0100 Subject: [PATCH 2/5] feat: add UI thread listener support for Reanimated shared values Add RiveWorkletBridge HybridObject that installs Nitro's dispatcher on Reanimated's UI runtime, enabling ViewModel property listeners to run on the UI thread and update SharedValues without blocking when JS is busy. - iOS: Uses GCD dispatch_async/dispatch_sync to main queue - Android: Uses Handler(Looper.getMainLooper()) via JNI bridge - Export installWorkletDispatcher() function from package - Update example to demonstrate JS thread blocking test --- RNRive.podspec | 11 +- android/CMakeLists.txt | 5 +- .../src/main/cpp/JRiveWorkletDispatcher.cpp | 81 +++++++++ .../src/main/cpp/JRiveWorkletDispatcher.hpp | 49 ++++++ android/src/main/cpp/cpp-adapter.cpp | 5 +- .../nitro/rive/RiveWorkletDispatcher.kt | 49 ++++++ cpp/HybridRiveWorkletBridge.hpp | 77 +++++++++ example/src/App.tsx | 5 + .../exercisers/RiveToReactNativeExample.tsx | 162 +++++++++++++++--- expo-example/app/_layout.tsx | 5 + expo-example/metro.config.js | 20 +++ expo-example/package.json | 2 +- nitro.json | 3 + .../generated/android/rive+autolinking.cmake | 1 + nitrogen/generated/android/riveOnLoad.cpp | 10 ++ nitrogen/generated/ios/RNRiveAutolinking.mm | 10 ++ .../c++/HybridRiveWorkletBridgeSpec.cpp | 21 +++ .../c++/HybridRiveWorkletBridgeSpec.hpp | 62 +++++++ src/core/WorkletBridge.ts | 47 +++++ src/index.tsx | 1 + src/specs/RiveWorkletBridge.nitro.ts | 13 ++ 21 files changed, 608 insertions(+), 31 deletions(-) create mode 100644 android/src/main/cpp/JRiveWorkletDispatcher.cpp create mode 100644 android/src/main/cpp/JRiveWorkletDispatcher.hpp create mode 100644 android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt create mode 100644 cpp/HybridRiveWorkletBridge.hpp create mode 100644 nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp create mode 100644 nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp create mode 100644 src/core/WorkletBridge.ts create mode 100644 src/specs/RiveWorkletBridge.nitro.ts diff --git a/RNRive.podspec b/RNRive.podspec index 99664968..3b896068 100644 --- a/RNRive.podspec +++ b/RNRive.podspec @@ -41,13 +41,20 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/rive-app/rive-nitro-react-native.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,swift}" + s.source_files = "ios/**/*.{h,m,mm,swift}", "cpp/**/*.{hpp,cpp}" s.public_header_files = ['ios/RCTSwiftLog.h'] + s.private_header_files = ['cpp/**/*.hpp'] + + # Set pod_target_xcconfig BEFORE add_nitrogen_files so it gets merged with Nitro's settings + s.pod_target_xcconfig = { + 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"' + } + load 'nitrogen/generated/ios/RNRive+autolinking.rb' add_nitrogen_files(s) s.dependency "RiveRuntime", rive_ios_version - install_modules_dependencies(s) + install_modules_dependencies(s) end diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 038c7a6e..8bbf93e1 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -6,7 +6,10 @@ set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_CXX_STANDARD 20) # Define C++ library and add all sources -add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) +add_library(${PACKAGE_NAME} SHARED + src/main/cpp/cpp-adapter.cpp + src/main/cpp/JRiveWorkletDispatcher.cpp +) # Add Nitrogen specs :) include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/rive+autolinking.cmake) diff --git a/android/src/main/cpp/JRiveWorkletDispatcher.cpp b/android/src/main/cpp/JRiveWorkletDispatcher.cpp new file mode 100644 index 00000000..f0745bfa --- /dev/null +++ b/android/src/main/cpp/JRiveWorkletDispatcher.cpp @@ -0,0 +1,81 @@ +#include "JRiveWorkletDispatcher.hpp" +#include + +namespace margelo::nitro::rive { + +using namespace facebook; + +JRiveWorkletDispatcher::JRiveWorkletDispatcher( + jni::alias_ref jThis) + : _javaPart(jni::make_global(jThis)) {} + +jni::local_ref JRiveWorkletDispatcher::initHybrid( + jni::alias_ref jThis) { + return makeCxxInstance(jThis); +} + +jni::local_ref JRiveWorkletDispatcher::create() { + return newObjectJavaArgs(); +} + +void JRiveWorkletDispatcher::trigger() { + std::unique_lock lock(_mutex); + while (!_jobs.empty()) { + auto job = std::move(_jobs.front()); + _jobs.pop(); + lock.unlock(); + job(); + lock.lock(); + } +} + +void JRiveWorkletDispatcher::scheduleTrigger() { + static const auto method = _javaPart->getClass()->getMethod("scheduleTrigger"); + method(_javaPart.get()); +} + +void JRiveWorkletDispatcher::runAsync(std::function&& function) { + std::unique_lock lock(_mutex); + _jobs.push(std::move(function)); + lock.unlock(); + scheduleTrigger(); +} + +void JRiveWorkletDispatcher::runSync(std::function&& function) { + std::mutex mtx; + std::condition_variable cv; + bool done = false; + + runAsync([&]() { + function(); + { + std::lock_guard lock(mtx); + done = true; + } + cv.notify_one(); + }); + + std::unique_lock lock(mtx); + cv.wait(lock, [&]{ return done; }); +} + +void JRiveWorkletDispatcher::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JRiveWorkletDispatcher::initHybrid), + makeNativeMethod("trigger", JRiveWorkletDispatcher::trigger), + }); +} + +AndroidMainThreadDispatcher::AndroidMainThreadDispatcher( + jni::local_ref javaDispatcher) + : _javaDispatcher(jni::make_global(javaDispatcher)) {} + +void AndroidMainThreadDispatcher::runAsync(std::function&& function) { + _javaDispatcher->cthis()->runAsync(std::move(function)); +} + +void AndroidMainThreadDispatcher::runSync(std::function&& function) { + _javaDispatcher->cthis()->runSync(std::move(function)); +} + +} // namespace margelo::nitro::rive diff --git a/android/src/main/cpp/JRiveWorkletDispatcher.hpp b/android/src/main/cpp/JRiveWorkletDispatcher.hpp new file mode 100644 index 00000000..280d1dd4 --- /dev/null +++ b/android/src/main/cpp/JRiveWorkletDispatcher.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace margelo::nitro::rive { + +using namespace facebook; + +class JRiveWorkletDispatcher : public jni::HybridClass { +public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/RiveWorkletDispatcher;"; + + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + static jni::local_ref create(); + + void runAsync(std::function&& function); + void runSync(std::function&& function); + +private: + friend HybridBase; + + void trigger(); + void scheduleTrigger(); + + jni::global_ref _javaPart; + std::queue> _jobs; + std::recursive_mutex _mutex; + + explicit JRiveWorkletDispatcher(jni::alias_ref jThis); +}; + +class AndroidMainThreadDispatcher : public Dispatcher { +public: + explicit AndroidMainThreadDispatcher(jni::local_ref javaDispatcher); + + void runAsync(std::function&& function) override; + void runSync(std::function&& function) override; + +private: + jni::global_ref _javaDispatcher; +}; + +} // namespace margelo::nitro::rive diff --git a/android/src/main/cpp/cpp-adapter.cpp b/android/src/main/cpp/cpp-adapter.cpp index 5116d53c..4c5af328 100644 --- a/android/src/main/cpp/cpp-adapter.cpp +++ b/android/src/main/cpp/cpp-adapter.cpp @@ -1,6 +1,9 @@ #include #include "riveOnLoad.hpp" +#include "JRiveWorkletDispatcher.hpp" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { - return margelo::nitro::rive::initialize(vm); + auto result = margelo::nitro::rive::initialize(vm); + margelo::nitro::rive::JRiveWorkletDispatcher::registerNatives(); + return result; } diff --git a/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt b/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt new file mode 100644 index 00000000..d6f08688 --- /dev/null +++ b/android/src/main/java/com/margelo/nitro/rive/RiveWorkletDispatcher.kt @@ -0,0 +1,49 @@ +package com.margelo.nitro.rive + +import android.os.Handler +import android.os.Looper +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import java.util.concurrent.atomic.AtomicBoolean + +@Suppress("JavaJniMissingFunction") +@Keep +@DoNotStrip +class RiveWorkletDispatcher { + @DoNotStrip + @Suppress("unused") + private val mHybridData: HybridData = initHybrid() + + private val mainHandler = Handler(Looper.getMainLooper()) + private val active = AtomicBoolean(true) + + private val triggerRunnable = Runnable { + synchronized(active) { + if (active.get()) { + trigger() + } + } + } + + private external fun initHybrid(): HybridData + private external fun trigger() + + @DoNotStrip + @Suppress("unused") + private fun scheduleTrigger() { + mainHandler.post(triggerRunnable) + } + + fun deactivate() { + synchronized(active) { + active.set(false) + } + } + + companion object { + init { + System.loadLibrary("rive") + } + } +} diff --git a/cpp/HybridRiveWorkletBridge.hpp b/cpp/HybridRiveWorkletBridge.hpp new file mode 100644 index 00000000..f58d8cc2 --- /dev/null +++ b/cpp/HybridRiveWorkletBridge.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "HybridRiveWorkletBridgeSpec.hpp" +#include + +#if __APPLE__ +#include +#include +#elif __ANDROID__ +#include "JRiveWorkletDispatcher.hpp" +#endif + +namespace margelo::nitro::rive { + +#if __APPLE__ + +/** + * iOS: A dispatcher that runs work on the main thread using GCD. + */ +class MainThreadDispatcher : public Dispatcher { +public: + void runAsync(std::function&& function) override { + __block auto func = std::move(function); + dispatch_async(dispatch_get_main_queue(), ^{ + func(); + }); + } + + void runSync(std::function&& function) override { + if (pthread_main_np() != 0) { + function(); + } else { + __block auto func = std::move(function); + dispatch_sync(dispatch_get_main_queue(), ^{ + func(); + }); + } + } +}; + +#endif + +class HybridRiveWorkletBridge : public HybridRiveWorkletBridgeSpec { +public: + HybridRiveWorkletBridge() : HybridObject(TAG) {} + + void install() override { + throw std::runtime_error("install() requires runtime access - use raw method"); + } + +protected: + void loadHybridMethods() override { + HybridObject::loadHybridMethods(); + registerHybrids(this, [](Prototype& prototype) { + prototype.registerRawHybridMethod("install", 0, &HybridRiveWorkletBridge::installRaw); + }); + } + +private: + jsi::Value installRaw(jsi::Runtime& runtime, + const jsi::Value& thisValue, + const jsi::Value* args, + size_t count) { +#if __APPLE__ + auto dispatcher = std::make_shared(); + Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher); +#elif __ANDROID__ + // Create the Java dispatcher instance and wrap it in the C++ dispatcher + auto javaDispatcher = JRiveWorkletDispatcher::create(); + auto dispatcher = std::make_shared(javaDispatcher); + Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher); +#endif + return jsi::Value::undefined(); + } +}; + +} // namespace margelo::nitro::rive diff --git a/example/src/App.tsx b/example/src/App.tsx index 0d193a0a..28913e9a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -11,11 +11,16 @@ import { import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { runOnUI } from 'react-native-reanimated'; +import { installWorkletDispatcher } from '@rive-app/react-native'; import { PagesList, type PageItem } from './PagesList'; import { HomeMenu } from './shared/HomeMenu'; const LAST_OPENED_KEY = '@rive_example_last_opened'; +// Install dispatcher on Reanimated's UI runtime for worklet-based listeners +installWorkletDispatcher(runOnUI); + type RootStackParamList = { Home: undefined; } & { diff --git a/example/src/exercisers/RiveToReactNativeExample.tsx b/example/src/exercisers/RiveToReactNativeExample.tsx index 336c88cd..cf7cfaf1 100644 --- a/example/src/exercisers/RiveToReactNativeExample.tsx +++ b/example/src/exercisers/RiveToReactNativeExample.tsx @@ -1,18 +1,75 @@ -import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; +import { + View, + Text, + StyleSheet, + ActivityIndicator, + Pressable, + Switch, +} from 'react-native'; +import { useEffect, useMemo, useState } from 'react'; import Animated, { + runOnUI, useSharedValue, useAnimatedStyle, + type SharedValue, } from 'react-native-reanimated'; -import { useEffect, useMemo } from 'react'; +import { NitroModules } from 'react-native-nitro-modules'; import { Fit, RiveView, useRiveFile, type RiveFile, type ViewModelInstance, + type ViewModelNumberProperty, } from '@rive-app/react-native'; import { type Metadata } from '../helpers/metadata'; +declare global { + var __callMicrotasks: () => void; +} + +/** + * Syncs a Rive ViewModel number property to a Reanimated SharedValue. + * @param useUIThread - If true, runs listener on UI thread (won't freeze when JS blocked). + * If false, runs on JS thread (will freeze when JS blocked). + */ +function useRiveNumberListener( + property: ViewModelNumberProperty | undefined, + sharedValue: SharedValue, + useUIThread: boolean +) { + useEffect(() => { + if (!property) return; + + if (useUIThread) { + // UI thread version - won't freeze when JS thread is blocked + const boxedProperty = NitroModules.box(property); + const sv = sharedValue; + + runOnUI(() => { + 'worklet'; + const prop = boxedProperty.unbox(); + prop.addListener((value: number) => { + 'worklet'; + sv.value = value; + global.__callMicrotasks(); + }); + })(); + + return () => { + property.removeListeners(); + }; + } else { + // JS thread version - will freeze when JS thread is blocked + const removeListener = property.addListener((value: number) => { + sharedValue.value = value; + }); + + return removeListener; + } + }, [property, sharedValue, useUIThread]); +} + export default function RiveToReactNativeExample() { const { riveFile, isLoading, error } = useRiveFile( require('../../assets/rive/bouncing_ball.riv') @@ -37,6 +94,7 @@ function WithViewModelSetup({ file }: { file: RiveFile }) { () => viewModel?.createDefaultInstance(), [viewModel] ); + const [useUIThread, setUseUIThread] = useState(true); if (!instance || !viewModel) { return ( @@ -58,15 +116,26 @@ function WithViewModelSetup({ file }: { file: RiveFile }) { ); } - return ; + return ( + + ); } function BouncingBallTracker({ instance, file, + useUIThread, + onToggle, }: { instance: ViewModelInstance; file: RiveFile; + useUIThread: boolean; + onToggle: (value: boolean) => void; }) { const pointerY = useSharedValue(0); @@ -75,20 +144,7 @@ function BouncingBallTracker({ [instance] ); - useEffect(() => { - if (!yposProperty) return; - - yposProperty.addListener((value) => { - 'worklet'; - console.log('worklet:', _WORKLET, __RUNTIME_KIND); - pointerY.value = value; - return true; - }); - - return () => { - yposProperty.removeListeners(); - }; - }, [yposProperty, pointerY]); + useRiveNumberListener(yposProperty, pointerY, useUIThread); const pointerStyle = useAnimatedStyle(() => ({ transform: [{ translateY: pointerY.value }], @@ -109,13 +165,16 @@ function BouncingBallTracker({ return ( - Rive animation drives the ball position.{'\n'}React Native listens and - moves the blue pointer to track it. - - - No re-renders - using direct addListener + Rive drives the ball position via data binding.{'\n'}React Native tracks + it with the blue pointer using addListener. + + JS Thread + + UI Thread + + RN + + ); } +function BlockJSThreadButton() { + const [isBlocking, setIsBlocking] = useState(false); + + const handlePress = () => { + setIsBlocking(true); + + // Use setTimeout to let the state update render before blocking + setTimeout(() => { + const start = Date.now(); + while (Date.now() - start < 2000) { + // Busy poll - blocks JS thread for 2 seconds + } + setIsBlocking(false); + }, 50); + }; + + return ( + + + {isBlocking ? 'JS Thread Blocked...' : 'Block JS Thread (2s)'} + + + ); +} + RiveToReactNativeExample.metadata = { name: 'Rive → React Native', description: @@ -157,11 +247,15 @@ const styles = StyleSheet.create({ marginVertical: 10, paddingHorizontal: 20, }, - valueText: { - fontSize: 18, - fontWeight: 'bold', - textAlign: 'center', + switchContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 10, marginBottom: 10, + }, + switchLabel: { + fontSize: 14, color: '#333', }, contentContainer: { @@ -217,4 +311,20 @@ const styles = StyleSheet.create({ fontSize: 14, lineHeight: 22, }, + blockButton: { + backgroundColor: '#4CAF50', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + marginTop: 20, + alignSelf: 'center', + }, + blockButtonActive: { + backgroundColor: '#f44336', + }, + blockButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, }); diff --git a/expo-example/app/_layout.tsx b/expo-example/app/_layout.tsx index d10c2b6e..752b4568 100644 --- a/expo-example/app/_layout.tsx +++ b/expo-example/app/_layout.tsx @@ -5,9 +5,14 @@ import { } from '@react-navigation/native'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; +import { runOnUI } from 'react-native-reanimated'; +import { installWorkletDispatcher } from '@rive-app/react-native'; import { useColorScheme } from '@/hooks/use-color-scheme'; +// Install dispatcher on Reanimated's UI runtime for worklet-based listeners +installWorkletDispatcher(runOnUI); + export default function RootLayout() { const colorScheme = useColorScheme(); diff --git a/expo-example/metro.config.js b/expo-example/metro.config.js index 0870b384..68f74851 100644 --- a/expo-example/metro.config.js +++ b/expo-example/metro.config.js @@ -15,6 +15,26 @@ const bobConfig = getConfig(config, { project: __dirname, }); +// Block resolution from example/node_modules to avoid version conflicts +// (expo-example uses different reanimated/worklets versions than example app) +const escapeRegex = (str) => str.replace(/[/\\]/g, '[/\\\\]'); +const exampleNodeModules = path.join(root, 'example', 'node_modules'); +const blockPatterns = [ + new RegExp( + escapeRegex(path.join(exampleNodeModules, 'react-native-reanimated')) + '.*' + ), + new RegExp( + escapeRegex(path.join(exampleNodeModules, 'react-native-worklets')) + '.*' + ), +]; +const existingBlockList = finalConfig.resolver.blockList; +if (existingBlockList) { + blockPatterns.push(existingBlockList); +} +finalConfig.resolver.blockList = new RegExp( + blockPatterns.map((r) => r.source).join('|') +); + /** * Resolves @example/* path aliases to the example/src/* directory. * Metro doesn't natively understand TypeScript path mappings, so this diff --git a/expo-example/package.json b/expo-example/package.json index 53894daf..d5cee8fd 100644 --- a/expo-example/package.json +++ b/expo-example/package.json @@ -40,7 +40,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", - "react-native-worklets": "0.6.1" + "react-native-worklets": "0.7.1" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/nitro.json b/nitro.json index 2213016c..c0bf4f1d 100644 --- a/nitro.json +++ b/nitro.json @@ -28,6 +28,9 @@ "RiveImageFactory": { "swift": "HybridRiveImageFactory", "kotlin": "HybridRiveImageFactory" + }, + "RiveWorkletBridge": { + "cpp": "HybridRiveWorkletBridge" } }, "ignorePaths": ["node_modules"] diff --git a/nitrogen/generated/android/rive+autolinking.cmake b/nitrogen/generated/android/rive+autolinking.cmake index a613c129..273be54b 100644 --- a/nitrogen/generated/android/rive+autolinking.cmake +++ b/nitrogen/generated/android/rive+autolinking.cmake @@ -41,6 +41,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridRiveImageFactorySpec.cpp ../nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp ../nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp + ../nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp ../nitrogen/generated/shared/c++/HybridViewModelPropertySpec.cpp diff --git a/nitrogen/generated/android/riveOnLoad.cpp b/nitrogen/generated/android/riveOnLoad.cpp index 8ffe3fd1..11f0f890 100644 --- a/nitrogen/generated/android/riveOnLoad.cpp +++ b/nitrogen/generated/android/riveOnLoad.cpp @@ -42,6 +42,7 @@ #include "JHybridViewModelListPropertySpec.hpp" #include "JHybridViewModelArtboardPropertySpec.hpp" #include +#include "HybridRiveWorkletBridge.hpp" namespace margelo::nitro::rive { @@ -120,6 +121,15 @@ int initialize(JavaVM* vm) { return instance->cthis()->shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RiveWorkletBridge", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRiveWorkletBridge\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); }); } diff --git a/nitrogen/generated/ios/RNRiveAutolinking.mm b/nitrogen/generated/ios/RNRiveAutolinking.mm index b606789c..daf807b5 100644 --- a/nitrogen/generated/ios/RNRiveAutolinking.mm +++ b/nitrogen/generated/ios/RNRiveAutolinking.mm @@ -15,6 +15,7 @@ #include "HybridRiveFileSpecSwift.hpp" #include "HybridRiveViewSpecSwift.hpp" #include "HybridRiveImageFactorySpecSwift.hpp" +#include "HybridRiveWorkletBridge.hpp" @interface RNRiveAutolinking : NSObject @end @@ -60,6 +61,15 @@ + (void) load { return hybridObject; } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "RiveWorkletBridge", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridRiveWorkletBridge\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); } @end diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp new file mode 100644 index 00000000..49a955c1 --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridRiveWorkletBridgeSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#include "HybridRiveWorkletBridgeSpec.hpp" + +namespace margelo::nitro::rive { + + void HybridRiveWorkletBridgeSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("install", &HybridRiveWorkletBridgeSpec::install); + }); + } + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp new file mode 100644 index 00000000..df955d2a --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp @@ -0,0 +1,62 @@ +/// +/// HybridRiveWorkletBridgeSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + + + +namespace margelo::nitro::rive { + + using namespace margelo::nitro; + + /** + * An abstract base class for `RiveWorkletBridge` + * Inherit this class to create instances of `HybridRiveWorkletBridgeSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridRiveWorkletBridge: public HybridRiveWorkletBridgeSpec { + * public: + * HybridRiveWorkletBridge(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridRiveWorkletBridgeSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridRiveWorkletBridgeSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridRiveWorkletBridgeSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual void install() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "RiveWorkletBridge"; + }; + +} // namespace margelo::nitro::rive diff --git a/src/core/WorkletBridge.ts b/src/core/WorkletBridge.ts new file mode 100644 index 00000000..9bf82706 --- /dev/null +++ b/src/core/WorkletBridge.ts @@ -0,0 +1,47 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { RiveWorkletBridge } from '../specs/RiveWorkletBridge.nitro'; + +let isInstalled = false; + +/** + * Install the Nitro Dispatcher on Reanimated's UI runtime. + * This enables using HybridObject callbacks (like addListener) from worklets + * and having shared value updates trigger useAnimatedStyle. + * + * Call this once at app startup. It will schedule the installation on the UI thread. + * + * @param runOnUI - The runOnUI function from react-native-reanimated + * + * @example + * ```tsx + * import { installWorkletDispatcher } from '@rive-app/react-native'; + * import { runOnUI } from 'react-native-reanimated'; + * + * // Call once at app startup + * installWorkletDispatcher(runOnUI); + * ``` + */ +export function installWorkletDispatcher( + runOnUI: ( + worklet: (...args: Args) => ReturnValue + ) => (...args: Args) => void +): void { + if (isInstalled) { + return; + } + isInstalled = true; + + // Create bridge on JS thread + const bridge = + NitroModules.createHybridObject('RiveWorkletBridge'); + + // Box it so we can use it in worklet + const boxedBridge = NitroModules.box(bridge); + + // Call install on Reanimated's UI runtime so dispatcher is installed there + runOnUI(() => { + 'worklet'; + const b = boxedBridge.unbox(); + b.install(); + })(); +} diff --git a/src/index.tsx b/src/index.tsx index f37bd9eb..0277ed52 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -56,3 +56,4 @@ export { useRiveFile } from './hooks/useRiveFile'; export { type RiveFileInput } from './hooks/useRiveFile'; export { type SetValueAction } from './types'; export { DataBindMode }; +export { installWorkletDispatcher } from './core/WorkletBridge'; diff --git a/src/specs/RiveWorkletBridge.nitro.ts b/src/specs/RiveWorkletBridge.nitro.ts new file mode 100644 index 00000000..fa26a331 --- /dev/null +++ b/src/specs/RiveWorkletBridge.nitro.ts @@ -0,0 +1,13 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +/** + * Bridge for installing Nitro Dispatcher on the worklets UI runtime. + */ +export interface RiveWorkletBridge + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + /** + * Install the dispatcher on the current runtime. + * Must be called from the UI runtime (via scheduleOnUI). + */ + install(): void; +} From 2b333fec664ecac5b91b9b6f9d9616361834de46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 22 Jan 2026 06:16:51 +0100 Subject: [PATCH 3/5] chore: test improvements and export RiveWorkletBridge type --- codecov.yml | 33 +++ example/__tests__/hooks.harness.tsx | 81 +++---- example/__tests__/worklet-bridge.harness.tsx | 216 +++++++++++++++++ example/ios/Podfile.lock | 4 +- .../exercisers/RiveToReactNativeExample.tsx | 2 +- example/src/tests/TestsPage.tsx | 6 +- .../c++/HybridRiveWorkletBridgeSpec.cpp | 2 +- .../c++/HybridRiveWorkletBridgeSpec.hpp | 2 +- src/index.tsx | 1 + yarn.lock | 218 ++++++++++++++++-- 10 files changed, 503 insertions(+), 62 deletions(-) create mode 100644 codecov.yml create mode 100644 example/__tests__/worklet-bridge.harness.tsx diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..d7bba5a4 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,33 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + +comment: + layout: "header, diff, flags, components" + behavior: default + require_changes: true + +flags: + unit: + paths: + - src/ + carryforward: true + harness: + paths: + - src/ + carryforward: true + +ignore: + - "**/*.nitro.ts" + - "**/index.ts" + - "**/index.tsx" + - "nitrogen/**" + - "example/**" + - "expo-example/**" + - "lib/**" diff --git a/example/__tests__/hooks.harness.tsx b/example/__tests__/hooks.harness.tsx index 7972db4a..0c32e363 100644 --- a/example/__tests__/hooks.harness.tsx +++ b/example/__tests__/hooks.harness.tsx @@ -16,7 +16,6 @@ import { } from '@rive-app/react-native'; import type { ViewModelInstance } from '@rive-app/react-native'; -const QUICK_START = require('../assets/rive/quick_start.riv'); const DATABINDING = require('../assets/rive/databinding.riv'); type UseRiveNumberContext = { @@ -45,7 +44,7 @@ function UseRiveNumberTestComponent({ instance: ViewModelInstance; context: UseRiveNumberContext; }) { - const { value, setValue, error } = useRiveNumber('health', instance); + const { value, setValue, error } = useRiveNumber('age', instance); useEffect(() => { context.value = value; @@ -94,54 +93,58 @@ function expectDefined(value: T): asserts value is NonNullable { describe('useRiveNumber Hook', () => { it('returns value from number property', async () => { - const file = await RiveFileFactory.fromSource(QUICK_START, undefined); - const vm = file.defaultArtboardViewModel(); + const file = await RiveFileFactory.fromSource(DATABINDING, undefined); + const vm = file.viewModelByName('Person'); expectDefined(vm); - const instance = vm.createDefaultInstance(); + const instance = vm.createInstanceByName('Gordon'); expectDefined(instance); const context = createUseRiveNumberContext(); - await render( - - ); - - await waitFor( - () => { - expect(context.error).toBeNull(); - expect(typeof context.value).toBe('number'); - }, - { timeout: 5000 } - ); - - cleanup(); + try { + await render( + + ); + + await waitFor( + () => { + expect(context.error).toBeNull(); + expect(typeof context.value).toBe('number'); + }, + { timeout: 5000 } + ); + } finally { + cleanup(); + } }); it('can set value via setValue', async () => { - const file = await RiveFileFactory.fromSource(QUICK_START, undefined); - const vm = file.defaultArtboardViewModel(); + const file = await RiveFileFactory.fromSource(DATABINDING, undefined); + const vm = file.viewModelByName('Person'); expectDefined(vm); - const instance = vm.createDefaultInstance(); + const instance = vm.createInstanceByName('Gordon'); expectDefined(instance); const context = createUseRiveNumberContext(); - await render( - - ); - - await waitFor( - () => { - expect(context.setValue).not.toBeNull(); - }, - { timeout: 5000 } - ); - - context.setValue!(42); - - const property = instance.numberProperty('health'); - expectDefined(property); - expect(property.value).toBe(42); - - cleanup(); + try { + await render( + + ); + + await waitFor( + () => { + expect(context.setValue).not.toBeNull(); + }, + { timeout: 5000 } + ); + + context.setValue!(42); + + const property = instance.numberProperty('age'); + expectDefined(property); + expect(property.value).toBe(42); + } finally { + cleanup(); + } }); }); diff --git a/example/__tests__/worklet-bridge.harness.tsx b/example/__tests__/worklet-bridge.harness.tsx new file mode 100644 index 00000000..0489a8a3 --- /dev/null +++ b/example/__tests__/worklet-bridge.harness.tsx @@ -0,0 +1,216 @@ +import { + describe, + it, + expect, + render, + waitFor, + cleanup, +} from 'react-native-harness'; +import { View } from 'react-native'; +import { useEffect, useMemo } from 'react'; +import { NitroModules } from 'react-native-nitro-modules'; +import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets'; + +import { + Fit, + RiveView, + RiveFileFactory, + useRiveFile, + useViewModelInstance, +} from '@rive-app/react-native'; +import type { RiveWorkletBridge } from '@rive-app/react-native'; + +const DATABINDING = require('../assets/rive/databinding.riv'); +const BOUNCING_BALL = require('../assets/rive/bouncing_ball.riv'); + +function expectDefined(value: T): asserts value is NonNullable { + expect(value).toBeDefined(); +} + +// Note: installWorkletDispatcher is already called in App.tsx at startup. +// UI thread listeners are designed for Rive-driven value changes (animation/data binding), +// not for JS-thread value changes. Testing the full UI thread listener flow requires +// a Rive file with animation that drives property changes. + +describe('Worklet Bridge', () => { + it('RiveWorkletBridge HybridObject can be created', () => { + const bridge = + NitroModules.createHybridObject('RiveWorkletBridge'); + expect(bridge).toBeDefined(); + }); + + it('property can be boxed for worklet use', async () => { + const file = await RiveFileFactory.fromSource(DATABINDING, undefined); + const vm = file.viewModelByName('Person'); + expectDefined(vm); + const instance = vm.createInstanceByName('Gordon'); + expectDefined(instance); + + const property = instance.numberProperty('age'); + expectDefined(property); + + // Verify boxing works (required for passing to worklets) + const boxedProperty = NitroModules.box(property); + expect(boxedProperty).toBeDefined(); + + // Verify the property value can be read + expect(property.value).toBe(30); // Gordon's age is 30 + }); + + // TODO: for some reason those wont run in harness environment + if (!global.RN_HARNESS) { + it('JS thread listener is called when Rive animation changes value', async () => { + // Listeners are notified when Rive animation/data binding changes values. + // This test uses bouncing_ball.riv which has an animation that drives ypos. + + let receivedValue: number | undefined; + + function ListenerTestComponent({ + onResult, + }: { + onResult: (value: number) => void; + }) { + const { riveFile } = useRiveFile(BOUNCING_BALL); + const instance = useViewModelInstance(riveFile); + + const property = useMemo( + () => instance?.numberProperty('ypos'), + [instance] + ); + + useEffect( + () => + property?.addListener((value) => { + onResult(value); + }), + [property, onResult] + ); + + if (!riveFile || !instance) { + return ; + } + + return ( + + ); + } + + try { + await render( + { + receivedValue = value; + }} + /> + ); + + await waitFor( + () => { + expect(receivedValue).toBeDefined(); + expect(typeof receivedValue).toBe('number'); + }, + { timeout: 5000 } + ); + } finally { + cleanup(); + } + }); + + it('UI thread listener is called when Rive animation changes value', async () => { + // Same as above but listener is registered via scheduleOnUI, so callback runs on UI thread + + type ListenerResult = { + calledOnUIThread: boolean; + receivedValue: number; + }; + + let result: ListenerResult | undefined; + + function UIThreadListenerTestComponent({ + onResult, + }: { + onResult: (r: ListenerResult) => void; + }) { + const { riveFile } = useRiveFile(BOUNCING_BALL); + const instance = useViewModelInstance(riveFile); + + const property = useMemo( + () => instance?.numberProperty('ypos'), + [instance] + ); + + useEffect(() => { + if (!property) return; + + const boxedProperty = NitroModules.box(property); + + const reportResult = (r: ListenerResult) => { + onResult(r); + }; + + scheduleOnUI(() => { + 'worklet'; + const prop = boxedProperty.unbox(); + prop.addListener((value: number) => { + 'worklet'; + // Check if we're on UI thread + const isOnUIThread = + typeof global._WORKLET !== 'undefined' && + global._WORKLET === true; + scheduleOnRN(reportResult, { + calledOnUIThread: isOnUIThread, + receivedValue: value, + }); + }); + }); + + return () => { + property.removeListeners(); + }; + }, [property, onResult]); + + if (!riveFile || !instance) { + return ; + } + + return ( + + ); + } + + try { + await render( + { + result = r; + }} + /> + ); + + // Wait for animation to trigger the listener (bouncing ball should change ypos quickly) + await waitFor( + () => { + expect(result).toBeDefined(); + expect(result!.calledOnUIThread).toBe(true); + expect(typeof result!.receivedValue).toBe('number'); + }, + { timeout: 5000 } + ); + } finally { + cleanup(); + } + }); + } +}); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b67556ef..9e618dcf 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1904,7 +1904,7 @@ PODS: - ReactCommon/turbomodule/core - RNWorklets - Yoga - - RNRive (0.1.5): + - RNRive (0.2.0): - DoubleConversion - glog - hermes-engine @@ -2330,7 +2330,7 @@ SPEC CHECKSUMS: RNCPicker: 83c74db2de8274d8a8f3e18d91dea174a708f8c4 RNGestureHandler: bff91bb5ab5688265c70f74180ef718b94f33fe3 RNReanimated: 9a24892f34ea317264883806d2e3de7ce34eab90 - RNRive: 73aa1ec7d3ef4da1030e81643808adb538bc05ca + RNRive: 509163d7fb4dcced11210670f0f6e7cb2f904e30 RNWorklets: ddf16938b1ed7e878563a4fc8a690968ef3d27f1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f diff --git a/example/src/exercisers/RiveToReactNativeExample.tsx b/example/src/exercisers/RiveToReactNativeExample.tsx index cf7cfaf1..1b8b284f 100644 --- a/example/src/exercisers/RiveToReactNativeExample.tsx +++ b/example/src/exercisers/RiveToReactNativeExample.tsx @@ -22,7 +22,7 @@ import { type ViewModelInstance, type ViewModelNumberProperty, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; declare global { var __callMicrotasks: () => void; diff --git a/example/src/tests/TestsPage.tsx b/example/src/tests/TestsPage.tsx index e1b3a078..3de819bb 100644 --- a/example/src/tests/TestsPage.tsx +++ b/example/src/tests/TestsPage.tsx @@ -7,6 +7,8 @@ import { ActivityIndicator, } from 'react-native'; import { getTestCollector } from 'react-native-harness'; +// @ts-expect-error - internal module not exported +import { TestComponentOverlay } from '@react-native-harness/runtime/dist/render/TestComponentOverlay'; import type { TestSuite, TestCase } from '@react-native-harness/bridge'; import { useState, useEffect } from 'react'; import type { Metadata } from '../shared/metadata'; @@ -85,10 +87,11 @@ export default function TestsPage() { await test.fn(); setTestStates((prev) => new Map(prev).set(key, { status: 'passed' })); } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); setTestStates((prev) => new Map(prev).set(key, { status: 'failed', - error: e instanceof Error ? e.message : String(e), + error: errorMessage, }) ); } @@ -202,6 +205,7 @@ export default function TestsPage() { ))} + ); } diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp index 49a955c1..aa30c195 100644 --- a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.cpp @@ -2,7 +2,7 @@ /// HybridRiveWorkletBridgeSpec.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2025 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "HybridRiveWorkletBridgeSpec.hpp" diff --git a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp index df955d2a..7946695d 100644 --- a/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveWorkletBridgeSpec.hpp @@ -2,7 +2,7 @@ /// HybridRiveWorkletBridgeSpec.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2025 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once diff --git a/src/index.tsx b/src/index.tsx index 0277ed52..323ae3d3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -57,3 +57,4 @@ export { type RiveFileInput } from './hooks/useRiveFile'; export { type SetValueAction } from './types'; export { DataBindMode }; export { installWorkletDispatcher } from './core/WorkletBridge'; +export type { RiveWorkletBridge } from './specs/RiveWorkletBridge.nitro'; diff --git a/yarn.lock b/yarn.lock index 8592988a..726e23a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/code-frame@npm:7.28.6" + dependencies: + "@babel/helper-validator-identifier": ^7.28.5 + js-tokens: ^4.0.0 + picocolors: ^1.1.1 + checksum: 6e98e47fd324b41c1919ff6d0fbf6fa5e991e5beff6b55803d9adaff9e11f4bc432803e52165f7b0d49af0f718209c3138a9b2fd51ff624b19d47704f11f8287 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.5": version: 7.28.5 resolution: "@babel/compat-data@npm:7.28.5" @@ -110,6 +121,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/generator@npm:7.28.6" + dependencies: + "@babel/parser": ^7.28.6 + "@babel/types": ^7.28.6 + "@jridgewell/gen-mapping": ^0.3.12 + "@jridgewell/trace-mapping": ^0.3.28 + jsesc: ^3.0.2 + checksum: 74f62f140e301c8c21652f7db3bc275008708272c0395f178ba6953297af50c4ea484874a44b3f292d242ce8a977fd3f31d9d3a3501c3aaca9cd46e3b1cded01 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -149,6 +173,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": ^7.27.3 + "@babel/helper-member-expression-to-functions": ^7.28.5 + "@babel/helper-optimise-call-expression": ^7.27.1 + "@babel/helper-replace-supers": ^7.28.6 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/traverse": ^7.28.6 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: f886ab302a83f8e410384aa635806b22374897fd9e3387c737ab9d91d1214bf9f7e57ae92619bd25dea63c9c0a49b25b44eb807873332e0eb9549219adc73639 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1": version: 7.28.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" @@ -233,6 +274,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: a0b4caab5e2180b215faa4d141ceac9e82fad9d446b8023eaeb8d82a6e62024726675b07fe8e616dd12f34e2bb59747e8d57aa8adab3e0717d1b8d691b118379 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" @@ -259,6 +307,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-replace-supers@npm:7.28.6" + dependencies: + "@babel/helper-member-expression-to-functions": ^7.28.5 + "@babel/helper-optimise-call-expression": ^7.27.1 + "@babel/traverse": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aa6530a52010883b6be88465e3b9e789509786a40203650a23a51c315f7442b196e5925fb8e2d66d1e3dc2c604cdc817bd8c5c170dbb322ab5ebc7486fd8a022 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" @@ -334,6 +395,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/parser@npm:7.28.6" + dependencies: + "@babel/types": ^7.28.6 + bin: + parser: ./bin/babel-parser.js + checksum: 2a35319792ceef9bc918f0ff854449bef0120707798fe147ef988b0606de226e2fbc3a562ba687148bfe5336c6c67358fb27e71a94e425b28482dcaf0b172fd6 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" @@ -668,6 +740,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-typescript@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -680,7 +763,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": +"@babel/plugin-transform-arrow-functions@npm:7.27.1, @babel/plugin-transform-arrow-functions@npm:^7.0.0-0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" dependencies: @@ -739,7 +822,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": +"@babel/plugin-transform-class-properties@npm:7.27.1, @babel/plugin-transform-class-properties@npm:^7.0.0-0, @babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" dependencies: @@ -763,7 +846,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": +"@babel/plugin-transform-classes@npm:7.28.4, @babel/plugin-transform-classes@npm:^7.0.0-0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-classes@npm:7.28.4" dependencies: @@ -1037,7 +1120,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" dependencies: @@ -1097,6 +1180,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c4428d31f182d724db6f10575669aad3dbccceb0dea26aa9071fa89f11b3456278da3097fcc78937639a13c105a82cd452dc0218ce51abdbcf7626a013b928a5 + languageName: node + linkType: hard + "@babel/plugin-transform-optional-chaining@npm:^7.0.0-0, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5" @@ -1277,7 +1372,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.0.0-0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": +"@babel/plugin-transform-shorthand-properties@npm:7.27.1, @babel/plugin-transform-shorthand-properties@npm:^7.0.0-0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1" dependencies: @@ -1322,7 +1417,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.0.0-0, @babel/plugin-transform-template-literals@npm:^7.27.1": +"@babel/plugin-transform-template-literals@npm:7.27.1, @babel/plugin-transform-template-literals@npm:^7.0.0-0, @babel/plugin-transform-template-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-template-literals@npm:7.27.1" dependencies: @@ -1359,6 +1454,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-transform-typescript@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": ^7.27.3 + "@babel/helper-create-class-features-plugin": ^7.28.6 + "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/plugin-syntax-typescript": ^7.28.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 029add39a37e4a1960a43c3a109680462f631bc63cc8457ea65add2cce3271c9fd4d6a1782177c65ea5f77731e2f8e2bc65a9aec9cc826346ba540ecd0b97e5a + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.27.1" @@ -1382,7 +1492,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": +"@babel/plugin-transform-unicode-regex@npm:7.27.1, @babel/plugin-transform-unicode-regex@npm:^7.0.0-0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1" dependencies: @@ -1515,6 +1625,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:7.27.1": + version: 7.27.1 + resolution: "@babel/preset-typescript@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-validator-option": ^7.27.1 + "@babel/plugin-syntax-jsx": ^7.27.1 + "@babel/plugin-transform-modules-commonjs": ^7.27.1 + "@babel/plugin-transform-typescript": ^7.27.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 38020f1b23e88ec4fbffd5737da455d8939244bddfb48a2516aef93fb5947bd9163fb807ce6eff3e43fa5ffe9113aa131305fef0fb5053998410bbfcfe6ce0ec + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.16.7, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.24.7": version: 7.28.5 resolution: "@babel/preset-typescript@npm:7.28.5" @@ -1548,6 +1673,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/template@npm:7.28.6" + dependencies: + "@babel/code-frame": ^7.28.6 + "@babel/parser": ^7.28.6 + "@babel/types": ^7.28.6 + checksum: 8ab6383053e226025d9491a6e795293f2140482d14f60c1244bece6bf53610ed1e251d5e164de66adab765629881c7d9416e1e540c716541d2fd0f8f36a013d7 + languageName: node + linkType: hard + "@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5": version: 7.28.5 resolution: "@babel/traverse@npm:7.28.5" @@ -1563,6 +1699,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/traverse@npm:7.28.6" + dependencies: + "@babel/code-frame": ^7.28.6 + "@babel/generator": ^7.28.6 + "@babel/helper-globals": ^7.28.0 + "@babel/parser": ^7.28.6 + "@babel/template": ^7.28.6 + "@babel/types": ^7.28.6 + debug: ^4.3.1 + checksum: 07bc23b720d111a20382fcdba776b800a7c1f94e35f8e4f417869f6769ba67c2b9573c8240924ca3b0ee5a88fa7ed048efb289e8b324f5cb4971e771174a0d32 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.28.5 resolution: "@babel/types@npm:7.28.5" @@ -1573,6 +1724,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/types@npm:7.28.6" + dependencies: + "@babel/helper-string-parser": ^7.27.1 + "@babel/helper-validator-identifier": ^7.28.5 + checksum: f76556cda59be337cc10dc68b2a9a947c10de018998bab41076e7b7e4489b28dd53299f98f22eec0774264c989515e6fdc56de91c73e3aa396367bb953200a6a + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -7105,7 +7266,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^2.0.0": +"convert-source-map@npm:2.0.0, convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 @@ -8638,7 +8799,7 @@ __metadata: react-native-safe-area-context: ~5.6.0 react-native-screens: ~4.16.0 react-native-web: ~0.21.0 - react-native-worklets: 0.6.1 + react-native-worklets: 0.7.1 typescript: ~5.9.2 languageName: unknown linkType: soft @@ -14544,6 +14705,29 @@ __metadata: languageName: node linkType: hard +"react-native-worklets@npm:0.7.1": + version: 0.7.1 + resolution: "react-native-worklets@npm:0.7.1" + dependencies: + "@babel/plugin-transform-arrow-functions": 7.27.1 + "@babel/plugin-transform-class-properties": 7.27.1 + "@babel/plugin-transform-classes": 7.28.4 + "@babel/plugin-transform-nullish-coalescing-operator": 7.27.1 + "@babel/plugin-transform-optional-chaining": 7.27.1 + "@babel/plugin-transform-shorthand-properties": 7.27.1 + "@babel/plugin-transform-template-literals": 7.27.1 + "@babel/plugin-transform-unicode-regex": 7.27.1 + "@babel/preset-typescript": 7.27.1 + convert-source-map: 2.0.0 + semver: 7.7.3 + peerDependencies: + "@babel/core": "*" + react: "*" + react-native: "*" + checksum: d6ca920ce53cad6ad45ac8379914adfaf73a92d76dc7c68d9b8a8a2913f7042ec8d60bbb6fcf72afe593993d087cbe6277c3f81927d2c0ade9a94acaf58b5dc3 + languageName: node + linkType: hard + "react-native@npm:0.79.2": version: 0.79.2 resolution: "react-native@npm:0.79.2" @@ -15297,21 +15481,21 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": - version: 6.3.1 - resolution: "semver@npm:6.3.1" +"semver@npm:7.7.3, semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1": + version: 7.7.3 + resolution: "semver@npm:7.7.3" bin: semver: bin/semver.js - checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 languageName: node linkType: hard -"semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1": - version: 7.7.3 - resolution: "semver@npm:7.7.3" +"semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" bin: semver: bin/semver.js - checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 languageName: node linkType: hard From 9d0011ddfc22d3a00b952136ec70cd508e88fbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 22 Jan 2026 07:00:39 +0100 Subject: [PATCH 4/5] chore(expo-example): update reanimated and fix metro config Update react-native-reanimated to 4.2.1 and fix metro config variable reference. --- expo-example/metro.config.js | 4 ++-- expo-example/package.json | 2 +- yarn.lock | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/expo-example/metro.config.js b/expo-example/metro.config.js index 68f74851..a3ae10fc 100644 --- a/expo-example/metro.config.js +++ b/expo-example/metro.config.js @@ -27,11 +27,11 @@ const blockPatterns = [ escapeRegex(path.join(exampleNodeModules, 'react-native-worklets')) + '.*' ), ]; -const existingBlockList = finalConfig.resolver.blockList; +const existingBlockList = bobConfig.resolver.blockList; if (existingBlockList) { blockPatterns.push(existingBlockList); } -finalConfig.resolver.blockList = new RegExp( +bobConfig.resolver.blockList = new RegExp( blockPatterns.map((r) => r.source).join('|') ); diff --git a/expo-example/package.json b/expo-example/package.json index d5cee8fd..6ba4310f 100644 --- a/expo-example/package.json +++ b/expo-example/package.json @@ -36,7 +36,7 @@ "react-native": "0.81.5", "react-native-gesture-handler": "2.29.1", "react-native-nitro-modules": "0.33.2", - "react-native-reanimated": "4.1.5", + "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", diff --git a/yarn.lock b/yarn.lock index 726e23a8..87496116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8795,7 +8795,7 @@ __metadata: react-native: 0.81.5 react-native-gesture-handler: 2.29.1 react-native-nitro-modules: 0.33.2 - react-native-reanimated: 4.1.5 + react-native-reanimated: 4.2.1 react-native-safe-area-context: ~5.6.0 react-native-screens: ~4.16.0 react-native-web: ~0.21.0 @@ -14559,7 +14559,7 @@ __metadata: languageName: node linkType: hard -"react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": +"react-native-is-edge-to-edge@npm:1.2.1, react-native-is-edge-to-edge@npm:^1.1.6, react-native-is-edge-to-edge@npm:^1.2.1": version: 1.2.1 resolution: "react-native-is-edge-to-edge@npm:1.2.1" peerDependencies: @@ -14604,6 +14604,20 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@npm:4.2.1": + version: 4.2.1 + resolution: "react-native-reanimated@npm:4.2.1" + dependencies: + react-native-is-edge-to-edge: 1.2.1 + semver: 7.7.3 + peerDependencies: + react: "*" + react-native: "*" + react-native-worklets: ">=0.7.0" + checksum: 102ead7c02411227f8364dec58cffc7a9225c8b9733cce07054064ecfd90cf2b124c8d1caf2bd11fc7db8603e0c91c392957ab0ce5852dcd1b3669e7d7d59931 + languageName: node + linkType: hard + "react-native-rive-example@workspace:example": version: 0.0.0-use.local resolution: "react-native-rive-example@workspace:example" From 10f655d551b0200cf06f71112acd81dfda0929c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 22 Jan 2026 08:49:23 +0100 Subject: [PATCH 5/5] chore(example): add marketplace link for bouncing ball demo --- example/src/exercisers/RiveToReactNativeExample.tsx | 4 +++- example/src/shared/metadata.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/example/src/exercisers/RiveToReactNativeExample.tsx b/example/src/exercisers/RiveToReactNativeExample.tsx index 1b8b284f..4d90152e 100644 --- a/example/src/exercisers/RiveToReactNativeExample.tsx +++ b/example/src/exercisers/RiveToReactNativeExample.tsx @@ -226,7 +226,9 @@ function BlockJSThreadButton() { RiveToReactNativeExample.metadata = { name: 'Rive → React Native', description: - 'Demonstrates Rive animation driving React Native UI through data binding listeners', + 'Demonstrates Rive graphics driving React Native UI through data binding listeners', + riveMarketplaceLink: + 'https://rive.app/community/files/25997-48571-demo-for-tracking-rive-property-in-react-native/', } satisfies Metadata; const styles = StyleSheet.create({ diff --git a/example/src/shared/metadata.ts b/example/src/shared/metadata.ts index f0b0154b..f35468bc 100644 --- a/example/src/shared/metadata.ts +++ b/example/src/shared/metadata.ts @@ -2,4 +2,5 @@ export type Metadata = { name: string; description: string; order?: number; // Lower numbers appear first, undefined = end of list + riveMarketplaceLink?: string; };