import { Ionicons } from "@expo/vector-icons"; import { BlurView } from "expo-blur"; import React, { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, Animated, Easing, Pressable, StyleSheet, TextInput, TVFocusGuideView, View, } from "react-native"; import { Text } from "@/components/common/Text"; import { useTVFocusAnimation } from "@/components/tv"; interface TVPasswordEntryModalProps { visible: boolean; onClose: () => void; onSubmit: (password: string) => Promise; username: string; } // TV Submit Button const TVSubmitButton: React.FC<{ onPress: () => void; label: string; loading?: boolean; disabled?: boolean; }> = ({ onPress, label, loading = false, disabled = false }) => { const { focused, handleFocus, handleBlur, animatedStyle } = useTVFocusAnimation({ scaleAmount: 1.05, duration: 120 }); const isDisabled = disabled || loading; return ( {loading ? ( ) : ( <> {label} )} ); }; // TV Focusable Password Input const TVPasswordInput: React.FC<{ value: string; onChangeText: (text: string) => void; placeholder: string; onSubmitEditing: () => void; hasTVPreferredFocus?: boolean; }> = ({ value, onChangeText, placeholder, onSubmitEditing, hasTVPreferredFocus, }) => { const { focused, handleFocus, handleBlur, animatedStyle } = useTVFocusAnimation({ scaleAmount: 1.02, duration: 120 }); const inputRef = useRef(null); return ( inputRef.current?.focus()} onFocus={() => { handleFocus(); inputRef.current?.focus(); }} onBlur={handleBlur} hasTVPreferredFocus={hasTVPreferredFocus} > ); }; export const TVPasswordEntryModal: React.FC = ({ visible, onClose, onSubmit, username, }) => { const { t } = useTranslation(); const [isReady, setIsReady] = useState(false); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const overlayOpacity = useRef(new Animated.Value(0)).current; const sheetTranslateY = useRef(new Animated.Value(200)).current; useEffect(() => { if (visible) { // Reset state when opening setPassword(""); setError(null); setIsLoading(false); overlayOpacity.setValue(0); sheetTranslateY.setValue(200); Animated.parallel([ Animated.timing(overlayOpacity, { toValue: 1, duration: 250, easing: Easing.out(Easing.quad), useNativeDriver: true, }), Animated.timing(sheetTranslateY, { toValue: 0, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), ]).start(); } }, [visible, overlayOpacity, sheetTranslateY]); useEffect(() => { if (visible) { const timer = setTimeout(() => setIsReady(true), 100); return () => clearTimeout(timer); } setIsReady(false); }, [visible]); const handleSubmit = async () => { if (!password) { setError(t("password.enter_password")); return; } setIsLoading(true); setError(null); try { await onSubmit(password); setPassword(""); } catch { setError(t("password.invalid_password")); } finally { setIsLoading(false); } }; if (!visible) return null; return ( {/* Header */} {t("password.enter_password")} {t("password.enter_password_for", { username })} {/* Password Input */} {isReady && ( {t("login.password")} { setPassword(text); setError(null); }} placeholder={t("login.password")} onSubmitEditing={handleSubmit} hasTVPreferredFocus /> {error && {error}} )} {/* Submit Button */} {isReady && ( )} ); }; const styles = StyleSheet.create({ overlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", zIndex: 1000, }, sheetContainer: { width: "100%", }, blurContainer: { borderTopLeftRadius: 24, borderTopRightRadius: 24, overflow: "hidden", }, content: { paddingTop: 24, paddingBottom: 50, overflow: "visible", }, header: { paddingHorizontal: 48, marginBottom: 24, }, title: { fontSize: 28, fontWeight: "bold", color: "#fff", marginBottom: 4, }, subtitle: { fontSize: 16, color: "rgba(255,255,255,0.6)", }, inputContainer: { paddingHorizontal: 48, marginBottom: 20, }, inputLabel: { fontSize: 14, color: "rgba(255,255,255,0.6)", marginBottom: 8, }, errorText: { color: "#ef4444", fontSize: 14, marginTop: 8, }, buttonContainer: { paddingHorizontal: 48, alignItems: "flex-start", }, });