diff --git a/components/SlideToConfirm.tsx b/components/SlideToConfirm.tsx index 961f910..d51b934 100644 --- a/components/SlideToConfirm.tsx +++ b/components/SlideToConfirm.tsx @@ -1,16 +1,16 @@ /** - * SlideToConfirm — No RNGH dependency. - * Uses Reanimated useSharedValue (JSI-direct, sets on UI thread immediately) - * + onTouchStart/Move/End on the handle for gesture capture. + * SlideToConfirm — pure RN, no RNGH, no Reanimated. + * PanResponder on handle + Animated(useNativeDriver:false). */ import { useRef } from 'react'; -import { StyleSheet, Text, View, ViewStyle } from 'react-native'; -import Animated, { - runOnJS, - useAnimatedStyle, - useSharedValue, - withSpring, -} from 'react-native-reanimated'; +import { + Animated, + PanResponder, + StyleSheet, + Text, + View, + ViewStyle, +} from 'react-native'; interface Props { onConfirm: () => void; @@ -20,66 +20,82 @@ interface Props { } const HANDLE_SIZE = 56; -const THRESHOLD = 0.80; // 80% of track +const THRESHOLD = 0.80; export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', disabled = false, style }: Props) { - const trackWidth = useRef(0); - const startX = useRef(0); - const confirmed = useRef(false); - const translateX = useSharedValue(0); + const trackWidth = useRef(0); + const confirmed = useRef(false); + const translateX = useRef(new Animated.Value(0)).current; - const handleStyle = useAnimatedStyle(() => ({ - transform: [{ translateX: translateX.value }], - })); + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => !disabled, + onMoveShouldSetPanResponder: () => !disabled, + onPanResponderGrant: () => { + confirmed.current = false; + (translateX as any).setOffset((translateX as any)._value); + translateX.setValue(0); + }, + onPanResponderMove: (_, gs) => { + const maxX = Math.max(trackWidth.current - HANDLE_SIZE - 6, 1); + const clamped = Math.min(Math.max(gs.dx, 0), maxX); + translateX.setValue(clamped - (translateX as any)._offset); - const labelStyle = useAnimatedStyle(() => { - const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1); - const ratio = translateX.value / maxX; - return { opacity: 1 - ratio * 1.5 }; + if (!confirmed.current && clamped / maxX >= THRESHOLD) { + confirmed.current = true; + (translateX as any).flattenOffset(); + Animated.spring(translateX, { + toValue: maxX, + useNativeDriver: false, + damping: 18, + stiffness: 220, + }).start(); + onConfirm(); + } + }, + onPanResponderRelease: () => { + (translateX as any).flattenOffset(); + if (!confirmed.current) { + Animated.spring(translateX, { + toValue: 0, + useNativeDriver: false, + damping: 18, + stiffness: 220, + }).start(); + } + }, + onPanResponderTerminate: () => { + (translateX as any).flattenOffset(); + if (!confirmed.current) { + Animated.spring(translateX, { + toValue: 0, + useNativeDriver: false, + }).start(); + } + }, + }) + ).current; + + const maxXForOpacity = Math.max(trackWidth.current - HANDLE_SIZE - 6, 1); + const labelOpacity = translateX.interpolate({ + inputRange: [0, maxXForOpacity > 0 ? maxXForOpacity * 0.6 : 1], + outputRange: [1, 0], + extrapolate: 'clamp', }); - const onTouchStart = (e: any) => { - if (disabled) return; - confirmed.current = false; - startX.current = e.nativeEvent.pageX - translateX.value; - }; - - const onTouchMove = (e: any) => { - if (disabled) return; - const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1); - const raw = e.nativeEvent.pageX - startX.current; - const clamped = Math.min(Math.max(raw, 0), maxX); - translateX.value = clamped; - - if (!confirmed.current && clamped / maxX >= THRESHOLD) { - confirmed.current = true; - translateX.value = withSpring(maxX, { damping: 18, stiffness: 220 }); - runOnJS(onConfirm)(); - } - }; - - const onTouchEnd = () => { - if (disabled || confirmed.current) return; - translateX.value = withSpring(0, { damping: 18, stiffness: 220 }); - }; - return ( { trackWidth.current = e.nativeEvent.layout.width; }} > - {/* Label fades as handle moves right */} - + {label} - {/* Sliding handle */} @@ -95,13 +111,8 @@ const styles = StyleSheet.create({ justifyContent: 'center', overflow: 'hidden', }, - trackDisabled: { - backgroundColor: '#94A3B8', - }, - labelWrap: { - alignItems: 'center', - justifyContent: 'center', - }, + trackDisabled: { backgroundColor: '#94A3B8' }, + labelWrap: { alignItems: 'center', justifyContent: 'center' }, label: { color: 'rgba(255,255,255,0.85)', fontSize: 15, @@ -122,9 +133,7 @@ const styles = StyleSheet.create({ shadowRadius: 4, elevation: 4, }, - handleDisabled: { - backgroundColor: '#E2E8F0', - }, + handleDisabled: { backgroundColor: '#E2E8F0' }, arrow: { fontSize: 28, color: '#0891B2',