fix(app): fix sidebar crash when switching iOS theme

Unistyles' StyleSheet.create((theme) => ...) wraps styled components in
<UnistylesComponent> and patches native view properties via C++. When
these dynamic styles are applied to Reanimated's Animated.View, a theme
change causes both systems to fight over the same native node, crashing
with "Unable to find node on an unmounted component."

Fix: use plain React Native StyleSheet for static positioning on
Animated.View, and pass theme-dependent values (backgroundColor) as
inline styles from useUnistyles() instead of Unistyles dynamic sheets.

Also:
- Add gestureAnimatingRef to prevent double withTiming during gesture opens
- Remove all debug instrumentation added during investigation
- Add Maestro flow and bash script for automated theme-toggle verification
- Add docs/MOBILE_TESTING.md covering Maestro patterns, self-verification
  loops, the Unistyles+Reanimated pitfall, and simulator commands
This commit is contained in:
Mohamed Boudra
2026-04-04 13:21:31 +07:00
parent dcffe46f90
commit 4896cfe970
10 changed files with 453 additions and 68 deletions
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import { Gesture } from "react-native-gesture-handler";
import { Extrapolation, interpolate, runOnJS, useSharedValue } from "react-native-reanimated";
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
@@ -9,14 +9,28 @@ interface UseExplorerOpenGestureParams {
}
export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestureParams) {
const { translateX, backdropOpacity, windowWidth, animateToOpen, animateToClose, isGesturing } =
useExplorerSidebarAnimation();
const {
translateX,
backdropOpacity,
windowWidth,
animateToOpen,
animateToClose,
isGesturing,
gestureAnimatingRef,
openGestureRef,
} = useExplorerSidebarAnimation();
const touchStartX = useSharedValue(0);
const touchStartY = useSharedValue(0);
const handleGestureOpen = useCallback(() => {
gestureAnimatingRef.current = true;
onOpen();
}, [onOpen, gestureAnimatingRef]);
return useMemo(
() =>
Gesture.Pan()
.withRef(openGestureRef)
.enabled(enabled)
.manualActivation(true)
.onTouchesDown((event) => {
@@ -78,7 +92,7 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
const shouldOpen = shouldOpenByPosition || shouldOpenByVelocity;
if (shouldOpen) {
animateToOpen();
runOnJS(onOpen)();
runOnJS(handleGestureOpen)();
} else {
animateToClose();
}
@@ -94,7 +108,8 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
animateToOpen,
animateToClose,
isGesturing,
onOpen,
openGestureRef,
handleGestureOpen,
touchStartX,
touchStartY,
],