import { Ionicons } from "@expo/vector-icons"; import { BlurView } from "expo-blur"; import React, { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, Animated, Easing, Pressable, StyleSheet, TVFocusGuideView, View, } from "react-native"; import { Text } from "@/components/common/Text"; import { scaleSize } from "@/utils/scaleSize"; import { verifyAccountPIN } from "@/utils/secureCredentials"; interface TVPINEntryModalProps { visible: boolean; onClose: () => void; onSuccess: () => void; onForgotPIN?: () => void; serverUrl: string; userId: string; username: string; } // Number pad button const NumberPadButton: React.FC<{ value: string; onPress: () => void; hasTVPreferredFocus?: boolean; isBackspace?: boolean; disabled?: boolean; }> = ({ value, onPress, hasTVPreferredFocus, isBackspace, disabled }) => { const [focused, setFocused] = useState(false); const scale = useRef(new Animated.Value(1)).current; const animateTo = (v: number) => Animated.timing(scale, { toValue: v, duration: 100, easing: Easing.out(Easing.quad), useNativeDriver: true, }).start(); return ( { setFocused(true); animateTo(1.1); }} onBlur={() => { setFocused(false); animateTo(1); }} hasTVPreferredFocus={hasTVPreferredFocus} disabled={disabled} focusable={!disabled} > {isBackspace ? ( ) : ( {value} )} ); }; // PIN dot indicator const PinDot: React.FC<{ filled: boolean; error: boolean }> = ({ filled, error, }) => ( ); // Forgot PIN link const ForgotPINLink: React.FC<{ onPress: () => void; label: string; }> = ({ onPress, label }) => { const [focused, setFocused] = useState(false); const scale = useRef(new Animated.Value(1)).current; const animateTo = (v: number) => Animated.timing(scale, { toValue: v, duration: 100, easing: Easing.out(Easing.quad), useNativeDriver: true, }).start(); return ( { setFocused(true); animateTo(1.05); }} onBlur={() => { setFocused(false); animateTo(1); }} > {label} ); }; export const TVPINEntryModal: React.FC = ({ visible, onClose, onSuccess, onForgotPIN, serverUrl, userId, username, }) => { const { t } = useTranslation(); const [isReady, setIsReady] = useState(false); const [pinCode, setPinCode] = useState(""); const [error, setError] = useState(false); const [isVerifying, setIsVerifying] = useState(false); const overlayOpacity = useRef(new Animated.Value(0)).current; const contentScale = useRef(new Animated.Value(0.9)).current; const shakeAnimation = useRef(new Animated.Value(0)).current; useEffect(() => { if (visible) { setPinCode(""); setError(false); setIsVerifying(false); overlayOpacity.setValue(0); contentScale.setValue(0.9); Animated.parallel([ Animated.timing(overlayOpacity, { toValue: 1, duration: 250, easing: Easing.out(Easing.quad), useNativeDriver: true, }), Animated.timing(contentScale, { toValue: 1, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), ]).start(); const timer = setTimeout(() => setIsReady(true), 100); return () => clearTimeout(timer); } setIsReady(false); }, [visible, overlayOpacity, contentScale]); const shake = () => { Animated.sequence([ Animated.timing(shakeAnimation, { toValue: 15, duration: 50, useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: -15, duration: 50, useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: 15, duration: 50, useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: 0, duration: 50, useNativeDriver: true, }), ]).start(); }; const handleNumberPress = async (num: string) => { if (isVerifying || pinCode.length >= 4) return; setError(false); const newPin = pinCode + num; setPinCode(newPin); // Auto-verify when 4 digits entered if (newPin.length === 4) { setIsVerifying(true); try { const isValid = await verifyAccountPIN(serverUrl, userId, newPin); if (isValid) { onSuccess(); setPinCode(""); } else { setError(true); shake(); setTimeout(() => setPinCode(""), 300); } } catch { setError(true); shake(); setTimeout(() => setPinCode(""), 300); } finally { setIsVerifying(false); } } }; const handleBackspace = () => { if (isVerifying) return; setError(false); setPinCode((prev) => prev.slice(0, -1)); }; 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?.(); }, }, ]); }; if (!visible) return null; return ( {/* Header */} {t("pin.enter_pin")} {username} {/* PIN Dots */} {[0, 1, 2, 3].map((i) => ( i} error={error} /> ))} {/* Number Pad */} {isReady && ( {/* Row 1: 1-3 */} handleNumberPress("1")} hasTVPreferredFocus disabled={isVerifying} /> handleNumberPress("2")} disabled={isVerifying} /> handleNumberPress("3")} disabled={isVerifying} /> {/* Row 2: 4-6 */} handleNumberPress("4")} disabled={isVerifying} /> handleNumberPress("5")} disabled={isVerifying} /> handleNumberPress("6")} disabled={isVerifying} /> {/* Row 3: 7-9 */} handleNumberPress("7")} disabled={isVerifying} /> handleNumberPress("8")} disabled={isVerifying} /> handleNumberPress("9")} disabled={isVerifying} /> {/* Row 4: empty, 0, backspace */} handleNumberPress("0")} disabled={isVerifying} /> )} {/* Forgot PIN */} {isReady && onForgotPIN && ( )} ); }; const styles = StyleSheet.create({ overlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0, 0, 0, 0.8)", justifyContent: "center", alignItems: "center", zIndex: 1000, }, contentContainer: { width: "100%", maxWidth: 400, }, blurContainer: { borderRadius: scaleSize(24), overflow: "hidden", }, content: { padding: scaleSize(40), alignItems: "center", }, title: { fontSize: scaleSize(28), fontWeight: "bold", color: "#fff", marginBottom: scaleSize(8), textAlign: "center", }, subtitle: { fontSize: scaleSize(18), color: "rgba(255,255,255,0.6)", marginBottom: scaleSize(32), textAlign: "center", }, pinDotsContainer: { flexDirection: "row", gap: scaleSize(16), marginBottom: scaleSize(32), }, pinDot: { width: scaleSize(20), height: scaleSize(20), borderRadius: scaleSize(10), borderWidth: scaleSize(2), borderColor: "rgba(255,255,255,0.4)", backgroundColor: "transparent", }, pinDotFilled: { backgroundColor: "#fff", borderColor: "#fff", }, pinDotError: { borderColor: "#ef4444", backgroundColor: "#ef4444", }, numberPad: { gap: scaleSize(12), marginBottom: scaleSize(24), }, numberRow: { flexDirection: "row", gap: scaleSize(12), }, numberButton: { width: scaleSize(72), height: scaleSize(72), borderRadius: scaleSize(36), justifyContent: "center", alignItems: "center", }, numberButtonPlaceholder: { width: scaleSize(72), height: scaleSize(72), }, numberText: { fontSize: scaleSize(28), fontWeight: "600", }, forgotContainer: { marginTop: 8, }, });