diff --git a/components/SlideToConfirm.tsx b/components/SlideToConfirm.tsx index a48d75e..dd668c7 100644 --- a/components/SlideToConfirm.tsx +++ b/components/SlideToConfirm.tsx @@ -1,9 +1,12 @@ /** * SlideToConfirm — pure RN, no RNGH, no Reanimated. * - * Key fix: Animated.event pipes gesture data directly to native thread - * (no JS bridge delay) — this is what makes the handle actually track the finger. - * useNativeDriver: true on all animations for 60fps smoothness. + * Key findings (Conan investigation): + * - Animated.event + useNativeDriver:true is BROKEN with PanResponder — gestureState + * (dx/dy) is JS-computed so native driver skips setValue() entirely. Never use. + * - useNativeDriver: false is fine on New Arch (Fabric+JSI) — no bridge penalty. + * - onStartShouldSetPanResponderCapture claims gesture in capture phase BEFORE + * the Modal container can contest it on iOS — fixes the delayed-grant lag. */ import { useEffect, useRef, useState } from 'react'; import { @@ -37,18 +40,16 @@ export function SlideToConfirm({ style, color, }: Props) { - // trackWidthRef — read inside PanResponder callbacks (no stale closure) - // trackWidthState — triggers re-render so opacity interpolation updates const trackWidthRef = useRef(0); const [trackWidthState, setTrackWidthState] = useState(0); - // pan drives all animation — useNativeDriver: true for all animations + // useNativeDriver: false — mandatory for PanResponder gestureState (dx/dy are JS-computed) const pan = useRef(new Animated.Value(0)).current; const [isConfirmed, setIsConfirmed] = useState(false); const confirmedRef = useRef(false); - // Live refs — break stale closures inside PanResponder + // Live refs — break stale closures inside PanResponder (created once in useRef) const disabledRef = useRef(disabled); disabledRef.current = disabled; const onConfirmRef = useRef(onConfirm); @@ -60,7 +61,7 @@ export function SlideToConfirm({ const snapToEnd = () => { Animated.spring(pan, { toValue: getMaxX(), - useNativeDriver: true, + useNativeDriver: false, damping: 18, stiffness: 220, }).start(({ finished }) => { @@ -75,7 +76,7 @@ export function SlideToConfirm({ Animated.timing(pan, { toValue: 0, duration: SNAP_DURATION, - useNativeDriver: true, + useNativeDriver: false, }).start(({ finished }) => { if (finished && !disabledRef.current) runTip(); }); @@ -84,8 +85,8 @@ export function SlideToConfirm({ const runTip = () => { if (disabledRef.current) return; Animated.sequence([ - Animated.timing(pan, { toValue: TIP_DISTANCE, duration: TIP_DURATION, useNativeDriver: true }), - Animated.timing(pan, { toValue: 0, duration: TIP_DURATION, useNativeDriver: true }), + Animated.timing(pan, { toValue: TIP_DISTANCE, duration: TIP_DURATION, useNativeDriver: false }), + Animated.timing(pan, { toValue: 0, duration: TIP_DURATION, useNativeDriver: false }), ]).start(); }; @@ -96,10 +97,12 @@ export function SlideToConfirm({ const panResponder = useRef( PanResponder.create({ - // Claim gesture on touch start — ensures dx=0 at grant, no jump + // Capture phase — claims gesture BEFORE Modal container can contest it on iOS + // This prevents the delayed-grant that made v1/v2 appear laggy + onStartShouldSetPanResponderCapture: () => + !disabledRef.current && trackWidthRef.current > 0, onStartShouldSetPanResponder: () => !disabledRef.current && trackWidthRef.current > 0, - // Also claim on move (horizontal dominant) as fallback onMoveShouldSetPanResponder: (_, gs) => !disabledRef.current && trackWidthRef.current > 0 && @@ -108,14 +111,14 @@ export function SlideToConfirm({ onPanResponderGrant: () => { confirmedRef.current = false; pan.stopAnimation(); - pan.setValue(0); // reset — Animated.event maps dx from 0 + pan.setValue(0); }, - // Animated.event pipes dx directly to native thread — true 1:1 finger tracking - onPanResponderMove: Animated.event( - [null, { dx: pan }], - { useNativeDriver: true } - ), + onPanResponderMove: (_, gs) => { + const maxX = getMaxX(); + const clamped = Math.min(Math.max(gs.dx, 0), maxX); + pan.setValue(clamped); + }, onPanResponderRelease: (_, gs) => { const maxX = getMaxX(); @@ -145,7 +148,6 @@ export function SlideToConfirm({ outputRange: [0, 1], extrapolate: 'clamp', }); - // Clamp translateX visually so handle doesn't overshoot track const clampedX = pan.interpolate({ inputRange: [0, maxX], outputRange: [0, maxX], @@ -168,7 +170,6 @@ export function SlideToConfirm({ setTrackWidthState(w); }} > - {/* "Slide to confirm" label — fades out as handle moves right */} {label} - {/* "Confirmed ✓" label — fades in near end */} Confirmed ✓ - {/* Sliding handle — driven by Animated.event (native thread) */}