import { BottomSheetBackdrop, type BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, Animated, Keyboard, Platform, TouchableOpacity, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useHaptic } from "@/hooks/useHaptic"; import { verifyAccountPIN } from "@/utils/secureCredentials"; import { Button } from "./Button"; import { Text } from "./common/Text"; import { PinInput } from "./inputs/PinInput"; interface PINEntryModalProps { visible: boolean; onClose: () => void; onSuccess: () => void; onForgotPIN?: () => void; serverUrl: string; userId: string; username: string; } export const PINEntryModal: React.FC = ({ visible, onClose, onSuccess, onForgotPIN, serverUrl, userId, username, }) => { const { t } = useTranslation(); const insets = useSafeAreaInsets(); const bottomSheetModalRef = useRef(null); const [pinCode, setPinCode] = useState(""); const [error, setError] = useState(null); const [isVerifying, setIsVerifying] = useState(false); const shakeAnimation = useRef(new Animated.Value(0)).current; const errorHaptic = useHaptic("error"); const successHaptic = useHaptic("success"); const isAndroid = Platform.OS === "android"; const snapPoints = useMemo( () => (isAndroid ? ["100%"] : ["50%"]), [isAndroid], ); useEffect(() => { if (visible) { bottomSheetModalRef.current?.present(); setPinCode(""); setError(null); } else { bottomSheetModalRef.current?.dismiss(); } }, [visible]); const handleSheetChanges = useCallback( (index: number) => { if (index === -1) { setPinCode(""); setError(null); onClose(); } }, [onClose], ); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( ), [], ); const shake = () => { Animated.sequence([ Animated.timing(shakeAnimation, { toValue: 10, duration: 50, useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: -10, duration: 50, useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: 10, duration: 50, useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: 0, duration: 50, useNativeDriver: true, }), ]).start(); }; const handlePinChange = async (value: string) => { setPinCode(value); setError(null); // Auto-verify when 4 digits entered if (value.length === 4) { setIsVerifying(true); try { const isValid = await verifyAccountPIN(serverUrl, userId, value); if (isValid) { Keyboard.dismiss(); successHaptic(); onSuccess(); setPinCode(""); } else { errorHaptic(); setError(t("pin.invalid_pin")); shake(); setPinCode(""); } } catch { errorHaptic(); setError(t("pin.invalid_pin")); shake(); setPinCode(""); } finally { setIsVerifying(false); } } }; const handleForgotPIN = () => { Alert.alert(t("pin.forgot_pin"), t("pin.forgot_pin_desc"), [ { text: t("common.cancel"), style: "cancel" }, { text: t("common.continue"), style: "destructive", onPress: () => { onClose(); onForgotPIN?.(); }, }, ]); }; return ( {/* Header */} {t("pin.enter_pin")} {t("pin.enter_pin_for", { username })} {/* PIN Input */} {error && ( {error} )} {isVerifying && ( {t("common.verifying") || "Verifying..."} )} {/* Forgot PIN */} {t("pin.forgot_pin")} {/* Cancel Button */} ); };