Files
cruiseLovers/assets/components/EmergencyButton.tsx

152 lines
4.5 KiB
TypeScript

import { useAuth } from '@/assets/contexts/useAuth';
import { getCachedContacts } from '@/assets/services/offlineStorage';
import { colors } from '@/assets/styles/colors';
import { fonts } from '@/assets/styles/fonts';
import { FontAwesome } from '@expo/vector-icons';
import { useEffect, useState } from 'react';
import { Alert, Linking, Platform, StyleSheet, Text, useWindowDimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const BTN_SIZE = 64;
const MARGIN = 16;
export function EmergencyButton() {
const { isAuthenticated } = useAuth();
const insets = useSafeAreaInsets();
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
const [emergencyPhone, setEmergencyPhone] = useState<string | null>(null);
const tabBarHeight = (Platform.OS === 'ios' ? 54 : 64) + insets.bottom;
const maxX = screenWidth - BTN_SIZE - MARGIN;
const minY = insets.top + MARGIN;
const maxY = screenHeight - tabBarHeight - BTN_SIZE - MARGIN;
const x = useSharedValue(screenWidth - BTN_SIZE - MARGIN);
const y = useSharedValue(screenHeight - tabBarHeight - BTN_SIZE - MARGIN);
const startX = useSharedValue(0);
const startY = useSharedValue(0);
const scale = useSharedValue(1);
const isDragActive = useSharedValue(false);
useEffect(() => {
if (!isAuthenticated) return;
getCachedContacts().then(({ contactData }) => {
if (contactData?.emergencyPhone) {
setEmergencyPhone(contactData.emergencyPhone);
}
});
}, [isAuthenticated]);
const handlePress = () => {
if (!emergencyPhone) return;
const url = `tel:${emergencyPhone.replace(/\s/g, '')}`;
Alert.alert(
'Linha de Emergência 24h',
`Ligar para ${emergencyPhone}?`,
[
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Ligar',
style: 'default',
onPress: () =>
Linking.openURL(url).catch(() =>
Alert.alert('Erro', 'Não foi possível abrir a aplicação de chamadas.'),
),
},
],
);
};
// Toque simples → abre o Alert
const tap = Gesture.Tap()
.maxDuration(500)
.onEnd(() => {
runOnJS(handlePress)();
});
// Segurar 800ms → ativa modo arrasto com feedback de escala
const longPress = Gesture.LongPress()
.minDuration(800)
.onStart(() => {
isDragActive.value = true;
scale.value = withSpring(1.2, { damping: 12, stiffness: 200 });
startX.value = x.value;
startY.value = y.value;
});
// Pan → só move quando o modo arrasto está ativo
const pan = Gesture.Pan()
.onUpdate((e) => {
if (!isDragActive.value) return;
x.value = Math.max(MARGIN, Math.min(maxX, startX.value + e.translationX));
y.value = Math.max(minY, Math.min(maxY, startY.value + e.translationY));
})
.onEnd(() => {
if (!isDragActive.value) return;
isDragActive.value = false;
scale.value = withSpring(1, { damping: 15, stiffness: 250 });
// snap para a borda mais próxima sem bounce
const snapRight = x.value + BTN_SIZE / 2 > screenWidth / 2;
x.value = withTiming(snapRight ? maxX : MARGIN, { duration: 250 });
});
const composed = Gesture.Race(
tap,
Gesture.Simultaneous(longPress, pan),
);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: x.value },
{ translateY: y.value },
{ scale: scale.value },
],
}));
if (!isAuthenticated || !emergencyPhone) return null;
return (
<GestureDetector gesture={composed}>
<Animated.View style={[styles.btn, animatedStyle]}>
<FontAwesome name="phone" size={18} color={colors.branco} />
<Text style={styles.label}>SOS</Text>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
btn: {
position: 'absolute',
top: 0,
left: 0,
width: BTN_SIZE,
height: BTN_SIZE,
borderRadius: BTN_SIZE / 2,
backgroundColor: colors.vermelho,
alignItems: 'center',
justifyContent: 'center',
gap: 4,
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
elevation: 10,
zIndex: 999,
},
label: {
color: colors.branco,
fontFamily: fonts.bold,
fontSize: 11,
letterSpacing: 0.5,
},
});