import { Ionicons } from "@expo/vector-icons"; import { BlurView } from "expo-blur"; import React, { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Animated, Easing, Pressable, ScrollView, StyleSheet, TVFocusGuideView, View, } from "react-native"; import { Text } from "@/components/common/Text"; import { TVPinInput, type TVPinInputRef } from "@/components/inputs/TVPinInput"; import { TVOptionCard, useTVFocusAnimation } from "@/components/tv"; import type { AccountSecurityType } from "@/utils/secureCredentials"; interface TVSaveAccountModalProps { visible: boolean; onClose: () => void; onSave: (securityType: AccountSecurityType, pinCode?: string) => void; username: string; } interface SecurityOption { type: AccountSecurityType; titleKey: string; descriptionKey: string; icon: keyof typeof Ionicons.glyphMap; } const SECURITY_OPTIONS: SecurityOption[] = [ { type: "none", titleKey: "save_account.no_protection", descriptionKey: "save_account.no_protection_desc", icon: "flash-outline", }, { type: "pin", titleKey: "save_account.pin_code", descriptionKey: "save_account.pin_code_desc", icon: "keypad-outline", }, { type: "password", titleKey: "save_account.password", descriptionKey: "save_account.password_desc", icon: "lock-closed-outline", }, ]; // Custom Save Button with TV focus const TVSaveButton: React.FC<{ onPress: () => void; label: string; disabled?: boolean; hasTVPreferredFocus?: boolean; }> = ({ onPress, label, disabled = false, hasTVPreferredFocus = false }) => { const { focused, handleFocus, handleBlur, animatedStyle } = useTVFocusAnimation({ scaleAmount: 1.05, duration: 120 }); return ( {label} ); }; // Back Button for PIN step const TVBackButton: React.FC<{ onPress: () => void; label: string; hasTVPreferredFocus?: boolean; }> = ({ onPress, label, hasTVPreferredFocus = false }) => { const { focused, handleFocus, handleBlur, animatedStyle } = useTVFocusAnimation({ scaleAmount: 1.05, duration: 120 }); return ( {label} ); }; export const TVSaveAccountModal: React.FC = ({ visible, onClose, onSave, username, }) => { const { t } = useTranslation(); const [isReady, setIsReady] = useState(false); const [step, setStep] = useState<"select" | "pin">("select"); const [selectedType, setSelectedType] = useState("none"); const [pinCode, setPinCode] = useState(""); const [pinError, setPinError] = useState(null); const pinInputRef = useRef(null); // Use useState for focus tracking (per TV focus guide) const [firstCardRef, setFirstCardRef] = useState(null); const overlayOpacity = useRef(new Animated.Value(0)).current; const sheetTranslateY = useRef(new Animated.Value(200)).current; useEffect(() => { if (visible) { // Reset state when opening setStep("select"); setSelectedType("none"); setPinCode(""); setPinError(null); 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]); // Focus the first card when ready useEffect(() => { if (isReady && firstCardRef && step === "select") { const timer = setTimeout(() => { (firstCardRef as any)?.requestTVFocus?.(); }, 50); return () => clearTimeout(timer); } }, [isReady, firstCardRef, step]); useEffect(() => { if (step === "pin" && isReady) { const timer = setTimeout(() => { pinInputRef.current?.focus(); }, 150); return () => clearTimeout(timer); } }, [step, isReady]); const handleOptionSelect = (type: AccountSecurityType) => { setSelectedType(type); if (type === "pin") { setStep("pin"); setPinCode(""); setPinError(null); } else { // For "none" or "password", save immediately onSave(type); resetAndClose(); } }; const handlePinSave = () => { if (pinCode.length !== 4) { setPinError(t("pin.enter_4_digits")); return; } onSave("pin", pinCode); resetAndClose(); }; const handleBack = () => { setStep("select"); setPinCode(""); setPinError(null); }; const resetAndClose = () => { setStep("select"); setSelectedType("none"); setPinCode(""); setPinError(null); onClose(); }; if (!visible) return null; return ( {/* Header */} {t("save_account.title")} {username} {step === "select" ? ( // Security selection step <> {t("save_account.security_option")} {isReady && ( {SECURITY_OPTIONS.map((option, index) => ( handleOptionSelect(option.type)} width={220} height={100} /> ))} )} ) : ( // PIN entry step <> {t("pin.setup_pin")} { setPinCode(text); setPinError(null); }} length={4} autoFocus /> {pinError && {pinError}} {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: 20, }, title: { fontSize: 28, fontWeight: "bold", color: "#fff", marginBottom: 4, }, subtitle: { fontSize: 16, color: "rgba(255,255,255,0.6)", }, sectionTitle: { fontSize: 16, fontWeight: "500", color: "rgba(255,255,255,0.6)", marginBottom: 16, paddingHorizontal: 48, textTransform: "uppercase", letterSpacing: 1, }, scrollView: { overflow: "visible", }, scrollContent: { paddingHorizontal: 48, paddingVertical: 10, gap: 12, }, buttonRow: { marginTop: 20, paddingHorizontal: 48, flexDirection: "row", gap: 16, alignItems: "center", }, pinContainer: { paddingHorizontal: 48, alignItems: "center", marginBottom: 10, }, errorText: { color: "#ef4444", fontSize: 14, marginTop: 12, textAlign: "center", }, });