diff --git a/.prettierignore b/.prettierignore index d4a22eea49..8214e01592 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ docs/build/ lib/ node_modules/ vendor/ +.gradle/ *.lottie.json diff --git a/docs/docs/guides/compatibility.md b/docs/docs/guides/compatibility.md index 31e877c50f..b0e0e1090a 100644 --- a/docs/docs/guides/compatibility.md +++ b/docs/docs/guides/compatibility.md @@ -45,7 +45,7 @@ This library supports as minimal `react-native` version as possible. However it This library is heavily relies on `react-native-reanimated` primitives to bring advanced concepts for keyboard handling. -The minimal supported version of `react-native-reanimated` is `2.11.0`. +The minimum supported version of `react-native-reanimated` is `3.0.0` (as officially supported by `react-native-reanimated` team). ## Third-party libraries compatibility diff --git a/package.json b/package.json index 3409550e9e..eeec46ac2a 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@commitlint/config-conventional": "^11.0.0", "@react-native/eslint-config": "^0.74.85", "@release-it/conventional-changelog": "^2.0.0", + "@testing-library/react-hooks": "^8.0.1", "@types/jest": "^29.2.1", "@types/react": "^18.2.6", "@typescript-eslint/eslint-plugin": "^6.7.4", @@ -96,6 +97,7 @@ "react-native": "0.74.3", "react-native-builder-bob": "^0.18.0", "react-native-reanimated": "3.12.1", + "react-test-renderer": "18.2.0", "release-it": "^14.2.2", "typescript": "5.0.4" }, diff --git a/src/animated.tsx b/src/animated.tsx index 64a37cffbb..d742c1d35a 100644 --- a/src/animated.tsx +++ b/src/animated.tsx @@ -1,17 +1,16 @@ /* eslint react/jsx-sort-props: off */ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Animated, Platform, StyleSheet } from "react-native"; import Reanimated, { useSharedValue } from "react-native-reanimated"; import { KeyboardControllerView } from "./bindings"; import { KeyboardContext } from "./context"; -import { useAnimatedValue, useSharedHandlers } from "./internal"; +import { focusedInputEventsMap, keyboardEventsMap } from "./event-mappings"; +import { useAnimatedValue, useEventHandlerRegistration } from "./internal"; import { applyMonkeyPatch, revertMonkeyPatch } from "./monkey-patch"; import { useAnimatedKeyboardHandler, useFocusedInputLayoutHandler, - useFocusedInputSelectionHandler, - useFocusedInputTextHandler, } from "./reanimated"; import type { KeyboardAnimationContext } from "./context"; @@ -25,9 +24,7 @@ import type { import type { ViewStyle } from "react-native"; const KeyboardControllerViewAnimated = Reanimated.createAnimatedComponent( - Animated.createAnimatedComponent( - KeyboardControllerView, - ) as React.FC, + Animated.createAnimatedComponent(KeyboardControllerView), ); type Styles = { @@ -84,6 +81,8 @@ export const KeyboardProvider = ({ navigationBarTranslucent, enabled: initiallyEnabled = true, }: KeyboardProviderProps) => { + // ref + const viewTagRef = useRef>(null); // state const [enabled, setEnabled] = useState(initiallyEnabled); // animated values @@ -93,10 +92,14 @@ export const KeyboardProvider = ({ const progressSV = useSharedValue(0); const heightSV = useSharedValue(0); const layout = useSharedValue(null); - const [setKeyboardHandlers, broadcastKeyboardEvents] = - useSharedHandlers(); - const [setInputHandlers, broadcastInputEvents] = - useSharedHandlers(); + const setKeyboardHandlers = useEventHandlerRegistration( + keyboardEventsMap, + viewTagRef, + ); + const setInputHandlers = useEventHandlerRegistration( + focusedInputEventsMap, + viewTagRef, + ); // memo const context = useMemo( () => ({ @@ -147,25 +150,17 @@ export const KeyboardProvider = ({ onKeyboardMoveStart: (event: NativeEvent) => { "worklet"; - broadcastKeyboardEvents("onStart", event); updateSharedValues(event, ["ios"]); }, onKeyboardMove: (event: NativeEvent) => { "worklet"; - broadcastKeyboardEvents("onMove", event); updateSharedValues(event, ["android"]); }, - onKeyboardMoveEnd: (event: NativeEvent) => { - "worklet"; - - broadcastKeyboardEvents("onEnd", event); - }, onKeyboardMoveInteractive: (event: NativeEvent) => { "worklet"; updateSharedValues(event, ["android", "ios"]); - broadcastKeyboardEvents("onInteractive", event); }, }, [], @@ -184,26 +179,6 @@ export const KeyboardProvider = ({ }, [], ); - const inputTextHandler = useFocusedInputTextHandler( - { - onFocusedInputTextChanged: (e) => { - "worklet"; - - broadcastInputEvents("onChangeText", e); - }, - }, - [], - ); - const inputSelectionHandler = useFocusedInputSelectionHandler( - { - onFocusedInputSelectionChanged: (e) => { - "worklet"; - - broadcastInputEvents("onSelectionChange", e); - }, - }, - [], - ); // effects useEffect(() => { @@ -217,6 +192,7 @@ export const KeyboardProvider = ({ return ( {children} diff --git a/src/context.ts b/src/context.ts index 94fd2e0525..3b80d9547e 100644 --- a/src/context.ts +++ b/src/context.ts @@ -2,9 +2,9 @@ import { createContext, useContext } from "react"; import { Animated } from "react-native"; import type { - FocusedInputHandlers, + FocusedInputHandler, FocusedInputLayoutChangedEvent, - KeyboardHandlers, + KeyboardHandler, } from "./types"; import type React from "react"; import type { SharedValue } from "react-native-reanimated"; @@ -22,11 +22,12 @@ export type KeyboardAnimationContext = { animated: AnimatedContext; reanimated: ReanimatedContext; layout: SharedValue; - setKeyboardHandlers: (handlers: KeyboardHandlers) => void; - setInputHandlers: (handlers: FocusedInputHandlers) => void; + setKeyboardHandlers: (handlers: KeyboardHandler) => () => void; + setInputHandlers: (handlers: FocusedInputHandler) => () => void; setEnabled: React.Dispatch>; }; const NOOP = () => {}; +const NESTED_NOOP = () => NOOP; const withSharedValue = (value: T): SharedValue => ({ value, addListener: NOOP, @@ -48,8 +49,8 @@ const defaultContext: KeyboardAnimationContext = { height: DEFAULT_SHARED_VALUE, }, layout: DEFAULT_LAYOUT, - setKeyboardHandlers: NOOP, - setInputHandlers: NOOP, + setKeyboardHandlers: NESTED_NOOP, + setInputHandlers: NESTED_NOOP, setEnabled: NOOP, }; diff --git a/src/event-handler.js b/src/event-handler.js new file mode 100644 index 0000000000..f95d0d4a79 --- /dev/null +++ b/src/event-handler.js @@ -0,0 +1,15 @@ +let REACore = null; + +try { + REACore = require("react-native-reanimated/src/core"); +} catch (e1) { + try { + REACore = require("react-native-reanimated/src/reanimated2/core"); + } catch (e2) { + console.warn("Failed to load REACore from both paths"); + } +} +const registerEventHandler = REACore.registerEventHandler; +const unregisterEventHandler = REACore.unregisterEventHandler; + +export { registerEventHandler, unregisterEventHandler }; diff --git a/src/event-handler.ts b/src/event-handler.ts new file mode 100644 index 0000000000..d78db6e54c --- /dev/null +++ b/src/event-handler.ts @@ -0,0 +1,8 @@ +declare function registerEventHandler( + handler: (event: never) => void, + eventName: string, + viewTag: number, +): number; +declare function unregisterEventHandler(id: number): void; + +export { registerEventHandler, unregisterEventHandler }; diff --git a/src/event-mappings.ts b/src/event-mappings.ts new file mode 100644 index 0000000000..a4602770ee --- /dev/null +++ b/src/event-mappings.ts @@ -0,0 +1,14 @@ +import type { FocusedInputHandler, KeyboardHandler } from "./types"; + +export const keyboardEventsMap = new Map([ + ["onStart", "onKeyboardMoveStart"], + ["onMove", "onKeyboardMove"], + ["onEnd", "onKeyboardMoveEnd"], + ["onInteractive", "onKeyboardMoveInteractive"], +]); +export const focusedInputEventsMap = new Map( + [ + ["onChangeText", "onFocusedInputTextChanged"], + ["onSelectionChange", "onFocusedInputSelectionChanged"], + ], +); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index aa505d21b5..6b2f18eada 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,7 +3,8 @@ import { useEffect } from "react"; import { KeyboardController } from "../bindings"; import { AndroidSoftInputModes } from "../constants"; import { useKeyboardContext } from "../context"; -import { uuid } from "../utils"; + +import useSyncEffect from "./useSyncEffect"; import type { AnimatedContext, ReanimatedContext } from "../context"; import type { FocusedInputHandler, KeyboardHandler } from "../types"; @@ -39,14 +40,10 @@ export function useGenericKeyboardHandler( ) { const context = useKeyboardContext(); - useEffect(() => { - const key = uuid(); - - context.setKeyboardHandlers({ [key]: handler }); + useSyncEffect(() => { + const cleanup = context.setKeyboardHandlers(handler); - return () => { - context.setKeyboardHandlers({ [key]: undefined }); - }; + return () => cleanup(); }, deps); } @@ -71,19 +68,15 @@ export function useReanimatedFocusedInput() { } export function useFocusedInputHandler( - handler?: FocusedInputHandler, + handler: FocusedInputHandler, deps?: DependencyList, ) { const context = useKeyboardContext(); - useEffect(() => { - const key = uuid(); - - context.setInputHandlers({ [key]: handler }); + useSyncEffect(() => { + const cleanup = context.setInputHandlers(handler); - return () => { - context.setInputHandlers({ [key]: undefined }); - }; + return () => cleanup(); }, deps); } diff --git a/src/hooks/useSyncEffect/__tests__/index.spec.ts b/src/hooks/useSyncEffect/__tests__/index.spec.ts new file mode 100644 index 0000000000..728e3056c3 --- /dev/null +++ b/src/hooks/useSyncEffect/__tests__/index.spec.ts @@ -0,0 +1,142 @@ +import { renderHook } from "@testing-library/react-hooks"; + +import useSyncEffect from "../index"; + +describe("scenarios when `useSyncEffect` should call `effect`", () => { + it("should call `effect` every re-render if no deps are provided", () => { + const cleanup = jest.fn(); + const effect = jest.fn().mockReturnValue(cleanup); + const { rerender, unmount } = renderHook(() => useSyncEffect(effect)); + + expect(effect).toHaveBeenCalledTimes(1); // will be called on mount + + rerender(); // simulate parent update + expect(effect).toHaveBeenCalledTimes(2); // it should call update, since no deps were provided + expect(cleanup).toHaveBeenCalledTimes(1); // it should call cleanup, since new effect was run + + rerender(); // simulate new parent update + expect(effect).toHaveBeenCalledTimes(3); // it should call update, since no deps were provided + expect(cleanup).toHaveBeenCalledTimes(2); // it should call cleanup, since new effect was run + + unmount(); + expect(effect).toHaveBeenCalledTimes(3); // shouldn't call effect on unmount + expect(cleanup).toHaveBeenCalledTimes(3); // should call cleanup on unmount again + }); + + it("should call `effect` only on mount, if `deps=[]`", () => { + const cleanup = jest.fn(); + const effect = jest.fn().mockReturnValue(cleanup); + const { rerender, unmount } = renderHook(() => useSyncEffect(effect, [])); + + expect(effect).toHaveBeenCalledTimes(1); // will be called on mount + expect(cleanup).toHaveBeenCalledTimes(0); + + rerender(); // simulate parent update + expect(effect).toHaveBeenCalledTimes(1); // re-render from outside should be skipped + expect(cleanup).toHaveBeenCalledTimes(0); + + rerender(); // simulate parent update + expect(effect).toHaveBeenCalledTimes(1); // re-render from outside should be skipped + expect(cleanup).toHaveBeenCalledTimes(0); + + unmount(); + expect(effect).toHaveBeenCalledTimes(1); // shouldn't call effect on unmount + expect(cleanup).toHaveBeenCalledTimes(1); // should call cleanup on unmount again + }); + + it("should call `effect` when deps were changed", () => { + const cleanup = jest.fn(); + const effect = jest.fn().mockReturnValue(cleanup); + let dep1 = 1; + let dep2 = {}; + const { rerender, unmount } = renderHook(() => + useSyncEffect(effect, [dep1, dep2]), + ); + + expect(cleanup).toHaveBeenCalledTimes(0); + expect(effect).toHaveBeenCalledTimes(1); // will be called on mount + + rerender(); + expect(cleanup).toHaveBeenCalledTimes(0); + expect(effect).toHaveBeenCalledTimes(1); // deps weren't changed, so `effect` shouldn't be called + + dep1 = 2; + rerender(); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(effect).toHaveBeenCalledTimes(2); // one dep was changed, so it should call `effect` + + rerender(); + expect(cleanup).toHaveBeenCalledTimes(1); + expect(effect).toHaveBeenCalledTimes(2); // deps weren't changed, so `effect` shouldn't be called + + dep2 = { newProperty: true }; + rerender(); + expect(cleanup).toHaveBeenCalledTimes(2); + expect(effect).toHaveBeenCalledTimes(3); // second dep was changed, so it should call `effect` + + rerender(); + expect(cleanup).toHaveBeenCalledTimes(2); + expect(effect).toHaveBeenCalledTimes(3); // deps weren't changed, so `effect` shouldn't be called + + dep1 = 3; + dep2 = { newProperty: true, anotherProperty: false }; + rerender(); + // deps were changed simultaneously, so it should call `effect` (only once) + expect(cleanup).toHaveBeenCalledTimes(3); + expect(effect).toHaveBeenCalledTimes(4); + + rerender(); + expect(cleanup).toHaveBeenCalledTimes(3); + expect(effect).toHaveBeenCalledTimes(4); // deps weren't changed, so `effect` shouldn't be called + + unmount(); + expect(cleanup).toHaveBeenCalledTimes(4); // should call cleanup on unmount again + expect(effect).toHaveBeenCalledTimes(4); + }); + + it("shouldn't not memoize `effect` and `cleanup` when deps changed", () => { + const cleanup1 = jest.fn(); + const effect1 = jest.fn().mockReturnValue(cleanup1); + const cleanup2 = jest.fn(); + const effect2 = jest.fn().mockReturnValue(cleanup2); + let effect = effect1; + let dep1 = 1; + let dep2 = 2; + const { rerender, unmount } = renderHook(() => + useSyncEffect(effect, [dep1, dep2]), + ); + + expect(effect1).toHaveBeenCalledTimes(1); + expect(cleanup1).toHaveBeenCalledTimes(0); + expect(effect2).toHaveBeenCalledTimes(0); + expect(cleanup2).toHaveBeenCalledTimes(0); + + rerender(); + expect(effect1).toHaveBeenCalledTimes(1); + expect(cleanup1).toHaveBeenCalledTimes(0); + expect(effect2).toHaveBeenCalledTimes(0); + expect(cleanup2).toHaveBeenCalledTimes(0); + + effect = effect2; + dep2 = 4; + rerender(); + expect(effect1).toHaveBeenCalledTimes(1); + expect(cleanup1).toHaveBeenCalledTimes(1); + expect(effect2).toHaveBeenCalledTimes(1); + expect(cleanup2).toHaveBeenCalledTimes(0); + + dep1 = 3; + effect = effect1; + rerender(); + expect(effect1).toHaveBeenCalledTimes(2); + expect(cleanup1).toHaveBeenCalledTimes(1); + expect(effect2).toHaveBeenCalledTimes(1); + expect(cleanup2).toHaveBeenCalledTimes(1); + + unmount(); + expect(effect1).toHaveBeenCalledTimes(2); + expect(cleanup1).toHaveBeenCalledTimes(2); + expect(effect2).toHaveBeenCalledTimes(1); + expect(cleanup2).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/hooks/useSyncEffect/index.ts b/src/hooks/useSyncEffect/index.ts new file mode 100644 index 0000000000..1196f16e8f --- /dev/null +++ b/src/hooks/useSyncEffect/index.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef } from "react"; + +import type { DependencyList } from "react"; + +/** + * @description + * Equivalent to `useEffect` but will run the effect synchronously, i. e. before render. + * + * @param {effect} - imperative function + * @param {deps} - if present, effect will only activate if the values in the list change + * + * @author Kiryl Ziusko + * @since 1.13.0 + * @version 1.0.0 + */ +const useSyncEffect: typeof useEffect = (effect, deps) => { + const cachedDeps = useRef(null); + const areDepsEqual = deps?.every( + (el, index) => cachedDeps.current && el === cachedDeps.current[index], + ); + const cleanupRef = useRef<(() => void) | void>(); + + if (!areDepsEqual || !cachedDeps.current) { + cleanupRef.current?.(); + cleanupRef.current = effect(); + cachedDeps.current = deps; + } + + useEffect(() => { + return () => { + cleanupRef.current?.(); + }; + }, []); +}; + +export default useSyncEffect; diff --git a/src/internal.ts b/src/internal.ts index 9ab764749e..796a852b00 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -1,64 +1,44 @@ -import { useCallback, useRef } from "react"; -import { Animated } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; +import { useRef } from "react"; +import { Animated, findNodeHandle } from "react-native"; -import type { Handlers } from "./types"; +import { registerEventHandler, unregisterEventHandler } from "./event-handler"; -type UntypedHandler = Record void>; -type SharedHandlersReturnType = [ - (handler: Handlers) => void, - (type: K, event: Parameters[0]) => void, -]; +type EventHandler = (event: never) => void; +type ComponentOrHandle = Parameters[0]; -/** - * Hook for storing worklet handlers (objects with keys, where values are worklets). - * Returns methods for setting handlers and broadcasting events in them. - * - * T is a generic that looks like: - * @example - * { - * onEvent: () => {}, - * onEvent2: () => {}, - * } - */ -export function useSharedHandlers< - T extends UntypedHandler, ->(): SharedHandlersReturnType { - const handlers = useSharedValue>({}); - const jsHandlers = useRef>({}); +export function useEventHandlerRegistration< + H extends Partial>, +>( + map: Map, + viewTagRef: React.MutableRefObject, +) { + const onRegisterHandler = (handler: H) => { + const viewTag = findNodeHandle(viewTagRef.current); + const ids = Object.keys(handler).map((handlerName) => { + const eventName = map.get(handlerName as keyof H); + const functionToCall = handler[handlerName as keyof H]; - // since js -> worklet -> js call is asynchronous, we can not write handlers - // straight into shared variable (using current shared value as a previous result), - // since there may be a race condition in a call, and closure may have out-of-dated - // values. As a result, some of handlers may be not written to "all handlers" object. - // Below we are writing all handlers to `ref` and afterwards synchronize them with - // shared value (since `refs` are not referring to actual value in worklets). - // This approach allow us to update synchronously handlers in js thread (and it assures, - // that it will have all of them) and then update them in worklet thread (calls are - // happening in FIFO order, so we will always have actual value). - const updateSharedHandlers = () => { - // eslint-disable-next-line react-compiler/react-compiler - handlers.value = jsHandlers.current; - }; - const setHandlers = useCallback((handler: Handlers) => { - jsHandlers.current = { - ...jsHandlers.current, - ...handler, - }; - updateSharedHandlers(); - }, []); - const broadcast = ( - type: K, - event: Parameters[0], - ) => { - "worklet"; + if (eventName && viewTag) { + return registerEventHandler( + (event: Parameters>[0]) => { + "worklet"; + + functionToCall?.(event); + }, + eventName, + viewTag, + ); + } - Object.keys(handlers.value).forEach((key) => { - handlers.value[key]?.[type]?.(event); + return null; }); + + return () => { + ids.forEach((id) => (id ? unregisterEventHandler(id) : null)); + }; }; - return [setHandlers, broadcast]; + return onRegisterHandler; } /** diff --git a/src/reanimated.native.ts b/src/reanimated.native.ts index 8485a6a862..b5600e3d41 100644 --- a/src/reanimated.native.ts +++ b/src/reanimated.native.ts @@ -4,10 +4,6 @@ import type { EventWithName, FocusedInputLayoutChangedEvent, FocusedInputLayoutHandlerHook, - FocusedInputSelectionChangedEvent, - FocusedInputSelectionHandlerHook, - FocusedInputTextChangedEvent, - FocusedInputTextHandlerHook, KeyboardHandlerHook, NativeEvent, } from "./types"; @@ -84,49 +80,3 @@ export const useFocusedInputLayoutHandler: FocusedInputLayoutHandlerHook< doDependenciesDiffer, ); }; - -export const useFocusedInputTextHandler: FocusedInputTextHandlerHook< - EventContext, - EventWithName -> = (handlers, dependencies) => { - const { context, doDependenciesDiffer } = useHandler(handlers, dependencies); - - return useEvent( - (event) => { - "worklet"; - const { onFocusedInputTextChanged } = handlers; - - if ( - onFocusedInputTextChanged && - event.eventName.endsWith("onFocusedInputTextChanged") - ) { - onFocusedInputTextChanged(event, context); - } - }, - ["onFocusedInputTextChanged"], - doDependenciesDiffer, - ); -}; - -export const useFocusedInputSelectionHandler: FocusedInputSelectionHandlerHook< - EventContext, - EventWithName -> = (handlers, dependencies) => { - const { context, doDependenciesDiffer } = useHandler(handlers, dependencies); - - return useEvent( - (event) => { - "worklet"; - const { onFocusedInputSelectionChanged } = handlers; - - if ( - onFocusedInputSelectionChanged && - event.eventName.endsWith("onFocusedInputSelectionChanged") - ) { - onFocusedInputSelectionChanged(event, context); - } - }, - ["onFocusedInputSelectionChanged"], - doDependenciesDiffer, - ); -}; diff --git a/src/reanimated.ts b/src/reanimated.ts index 12bcbf2330..ab0868c610 100644 --- a/src/reanimated.ts +++ b/src/reanimated.ts @@ -2,10 +2,6 @@ import type { EventWithName, FocusedInputLayoutChangedEvent, FocusedInputLayoutHandlerHook, - FocusedInputSelectionChangedEvent, - FocusedInputSelectionHandlerHook, - FocusedInputTextChangedEvent, - FocusedInputTextHandlerHook, KeyboardHandlerHook, NativeEvent, } from "./types"; @@ -20,11 +16,3 @@ export const useFocusedInputLayoutHandler: FocusedInputLayoutHandlerHook< Record, EventWithName > = NOOP; -export const useFocusedInputTextHandler: FocusedInputTextHandlerHook< - Record, - EventWithName -> = NOOP; -export const useFocusedInputSelectionHandler: FocusedInputSelectionHandlerHook< - Record, - EventWithName -> = NOOP; diff --git a/src/types.ts b/src/types.ts index 4f5dd31701..eb98cf06d8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,6 +47,8 @@ export type EventWithName = { // native View/Module declarations export type KeyboardControllerProps = { + //ref prop + ref?: React.Ref>; // callback props onKeyboardMoveStart?: ( e: NativeSyntheticEvent>, diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 90300d25d0..0000000000 --- a/src/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export const uuid = () => Math.random().toString(36).slice(-6); diff --git a/yarn.lock b/yarn.lock index 396e45a561..fd1290e18b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,6 +1323,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.12.5": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" + integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.0.0", "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -2408,6 +2415,14 @@ dependencies: defer-to-connect "^1.0.1" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -7877,6 +7892,13 @@ react-devtools-core@^5.0.0: shell-quote "^1.6.1" ws "^7" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -7892,6 +7914,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-native-builder-bob@^0.18.0: version "0.18.3" resolved "https://registry.yarnpkg.com/react-native-builder-bob/-/react-native-builder-bob-0.18.3.tgz#fb4d3e50a3b2290db3c88de6d40403ac7eb9f85f" @@ -7989,6 +8016,15 @@ react-shallow-renderer@^16.15.0: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" +react-test-renderer@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e" + integrity sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA== + dependencies: + react-is "^18.2.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.23.0" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -8382,6 +8418,13 @@ scheduler@0.24.0-canary-efb381bbf-20230505: dependencies: loose-envify "^1.1.0" +scheduler@^0.23.0: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + selfsigned@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0"