From 295c32e6e7f33b2c2486509344fbaab846b8e81d Mon Sep 17 00:00:00 2001 From: Nemo Date: Wed, 25 Mar 2026 05:43:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20SlideToConfirm=20stale=20closure=20?= =?UTF-8?q?=E2=80=94=20live=20refs=20for=20disabled=20+=20onConfirm;=20hor?= =?UTF-8?q?izontal-only=20drag=20on=20iOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(app)/tasks/[id].tsx | 1 - components/SlideToConfirm.tsx | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/(app)/tasks/[id].tsx b/app/(app)/tasks/[id].tsx index 27c8b93..d2ceeeb 100644 --- a/app/(app)/tasks/[id].tsx +++ b/app/(app)/tasks/[id].tsx @@ -9,7 +9,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import * as Location from 'expo-location'; import { api } from '../../../services/api'; import { useAuthStore } from '../../../stores/authStore'; -import { SlideToConfirm } from '../../../components/SlideToConfirm'; // ─── Constants ──────────────────────────────────────────────────────────────── const STATUS_FLOW = ['OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED'] as const; diff --git a/components/SlideToConfirm.tsx b/components/SlideToConfirm.tsx index 4b6b327..c40f32d 100644 --- a/components/SlideToConfirm.tsx +++ b/components/SlideToConfirm.tsx @@ -1,6 +1,9 @@ /** * SlideToConfirm — pure RN, no RNGH, no Reanimated. * PanResponder on handle + Animated(useNativeDriver:false). + * + * Fix: live refs break stale closures — disabledRef + onConfirmRef + * updated on every render so PanResponder always reads current values. */ import { useRef } from 'react'; import { @@ -28,17 +31,25 @@ export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', disabled const confirmed = useRef(false); const translateX = useRef(new Animated.Value(0)).current; + // ── Live refs: break stale closures inside PanResponder ────────────────── + const disabledRef = useRef(disabled); + disabledRef.current = disabled; // updated on every render + const onConfirmRef = useRef(onConfirm); + onConfirmRef.current = onConfirm; // updated on every render + const panResponder = useRef( PanResponder.create({ - onStartShouldSetPanResponder: () => !disabled, - onMoveShouldSetPanResponder: () => !disabled, + onStartShouldSetPanResponder: () => !disabledRef.current, + // Horizontal-dominance check prevents ScrollView stealing vertical-ish drags on iOS + onMoveShouldSetPanResponder: (_, gs) => + !disabledRef.current && Math.abs(gs.dx) > Math.abs(gs.dy) * 2, 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 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); @@ -51,7 +62,7 @@ export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', disabled damping: 18, stiffness: 220, }).start(); - onConfirm(); + onConfirmRef.current(); // always calls latest onConfirm } }, onPanResponderRelease: () => {