diff --git a/app/_layout.tsx b/app/_layout.tsx
index 943ccc6..5e5fd36 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -2,7 +2,6 @@ import '../global.css';
import { useEffect } from 'react';
import { Stack, router } from 'expo-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useAuthStore } from '../stores/authStore';
const queryClient = new QueryClient();
@@ -25,10 +24,8 @@ export default function RootLayout() {
}, [isLoading, token]);
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/components/SlideToConfirm.tsx b/components/SlideToConfirm.tsx
index 4b6d3ab..961f910 100644
--- a/components/SlideToConfirm.tsx
+++ b/components/SlideToConfirm.tsx
@@ -1,123 +1,134 @@
-import { useCallback } from 'react';
+/**
+ * SlideToConfirm — No RNGH dependency.
+ * Uses Reanimated useSharedValue (JSI-direct, sets on UI thread immediately)
+ * + onTouchStart/Move/End on the handle for gesture capture.
+ */
+import { useRef } from 'react';
import { StyleSheet, Text, View, ViewStyle } from 'react-native';
-import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
-import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
+import Animated, {
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
interface Props {
onConfirm: () => void;
label?: string;
- color?: string;
- threshold?: number;
disabled?: boolean;
style?: ViewStyle;
}
-const TRACK_HEIGHT = 60;
-const HANDLE_SIZE = 48;
-const TRACK_PADDING = 5;
+const HANDLE_SIZE = 56;
+const THRESHOLD = 0.80; // 80% of track
-export function SlideToConfirm({
- onConfirm,
- label = 'Slide to confirm',
- color = '#059669',
- threshold = 0.82,
- disabled = false,
- style,
-}: Props) {
- const translateX = useSharedValue(0);
- const maxX = useSharedValue(200); // updated on layout
+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 triggerConfirm = useCallback(() => { onConfirm(); }, [onConfirm]);
-
- const pan = Gesture.Pan()
- .enabled(!disabled)
- .minDistance(0)
- .activeOffsetX([-5, 5])
- .onUpdate((e) => {
- 'worklet';
- translateX.value = Math.max(0, Math.min(e.translationX, maxX.value));
- })
- .onEnd(() => {
- 'worklet';
- if (translateX.value >= maxX.value * threshold) {
- translateX.value = withSpring(maxX.value, { damping: 20, stiffness: 200, mass: 0.8 });
- runOnJS(triggerConfirm)();
- } else {
- translateX.value = withSpring(0, { damping: 20, stiffness: 200, mass: 0.8 });
- }
- });
-
- const animatedHandle = useAnimatedStyle(() => ({
+ const handleStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
- const animatedFill = useAnimatedStyle(() => ({
- width: translateX.value + HANDLE_SIZE + TRACK_PADDING,
- }));
+ const labelStyle = useAnimatedStyle(() => {
+ const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1);
+ const ratio = translateX.value / maxX;
+ return { opacity: 1 - ratio * 1.5 };
+ });
- const animatedLabel = useAnimatedStyle(() => ({
- opacity: Math.max(0, 1 - (translateX.value / Math.max(maxX.value * 0.5, 1))),
- }));
+ 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 (
- // GestureHandlerRootView inside Modal is REQUIRED on iOS New Arch
- // Modal creates a new UIWindow — RNGH's gesture manager must be re-rooted here
-
- {
- const w = e.nativeEvent.layout.width;
- maxX.value = w - HANDLE_SIZE - TRACK_PADDING * 2;
- }}
+ { trackWidth.current = e.nativeEvent.layout.width; }}
+ >
+ {/* Label fades as handle moves right */}
+
+ {label}
+
+
+ {/* Sliding handle */}
+
- {/* Colored fill that grows behind the handle */}
-
-
- {/* Label fades as handle moves right */}
-
- {label}
-
-
- {/* Draggable handle */}
-
-
- ›
-
-
-
-
+ ›
+
+
);
}
const styles = StyleSheet.create({
- root: { width: '100%' },
track: {
- height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2,
- backgroundColor: '#F1F5F9', borderWidth: 1.5,
- justifyContent: 'center', alignItems: 'center',
- overflow: 'hidden', position: 'relative',
+ height: 60,
+ backgroundColor: '#0E7490',
+ borderRadius: 30,
+ justifyContent: 'center',
+ overflow: 'hidden',
},
- fill: {
- position: 'absolute', left: 0, top: 0,
- height: TRACK_HEIGHT, zIndex: 1,
+ trackDisabled: {
+ backgroundColor: '#94A3B8',
+ },
+ labelWrap: {
+ alignItems: 'center',
+ justifyContent: 'center',
},
label: {
- position: 'absolute', zIndex: 2,
- fontSize: 14, fontWeight: '700', color: '#64748B',
- textAlign: 'center', width: '100%', paddingRight: 16,
+ color: 'rgba(255,255,255,0.85)',
+ fontSize: 15,
+ fontWeight: '600',
+ letterSpacing: 0.3,
},
handle: {
- position: 'absolute', zIndex: 3,
- justifyContent: 'center', alignItems: 'center',
- shadowColor: '#000', shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.18, shadowRadius: 4, elevation: 4,
+ width: HANDLE_SIZE,
+ height: HANDLE_SIZE,
+ borderRadius: HANDLE_SIZE / 2,
+ backgroundColor: '#fff',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginLeft: 3,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.18,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+ handleDisabled: {
+ backgroundColor: '#E2E8F0',
+ },
+ arrow: {
+ fontSize: 28,
+ color: '#0891B2',
+ fontWeight: '800',
+ lineHeight: 32,
},
- arrow: { fontSize: 26, color: '#fff', fontWeight: '800', marginLeft: 2 },
});