import { FC, useCallback, useEffect, useMemo, useState } from "react" import { ActivityIndicator, Dimensions, RefreshControl, ScrollView, TextStyle, TouchableOpacity, View, ViewStyle, } from "react-native" import { Button } from "@/components/Button" import { Dialog } from "@/components/Dialog" import { Icon } from "@/components/Icon" import { Screen } from "@/components/Screen" import { Skeleton, SkeletonContainer } from "@/components/Skeleton" import { Text } from "@/components/Text" import { useAuth } from "@/context/AuthContext" import { translate } from "@/i18n/translate" import { AppStackScreenProps } from "@/navigators/navigationTypes" import { authApi } from "@/services/api/authApi" import type { Session } from "@/services/api/authTypes" import { useAppTheme } from "@/theme/context" import { $styles } from "@/theme/styles" import type { ThemedStyle } from "@/theme/types" import { s } from "@/utils/responsive" import { useHeader } from "@/utils/useHeader" export const SessionManagementScreen: FC> = function SessionManagementScreen({ navigation }) { const { themed, theme } = useAppTheme() const { accessToken } = useAuth() // Session state const [sessions, setSessions] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) const [revokingSessionId, setRevokingSessionId] = useState(null) const [isRevokingAll, setIsRevokingAll] = useState(false) // Dialog state const [confirmDialogVisible, setConfirmDialogVisible] = useState(false) const [confirmAllDialogVisible, setConfirmAllDialogVisible] = useState(false) const [successDialogVisible, setSuccessDialogVisible] = useState(false) const [successMessage, setSuccessMessage] = useState("") const [pendingSessionId, setPendingSessionId] = useState(null) useHeader( { title: translate("securityScreen:activeSessions"), leftIcon: "back", onLeftPress: () => navigation.goBack(), }, [], ) // Fetch sessions const fetchSessions = useCallback( async (showLoading = true) => { if (!accessToken) return if (showLoading) setIsLoading(true) try { const result = await authApi.getSessions(accessToken, { active: true }) if (result.kind === "ok" && result.data.data?.sessions) { setSessions(result.data.data.sessions) } } catch (e) { if (__DEV__) console.error("Failed to fetch sessions:", e) } finally { setIsLoading(false) setIsRefreshing(false) } }, [accessToken], ) // Initial fetch useEffect(() => { fetchSessions() }, [fetchSessions]) // Pull to refresh const onRefresh = useCallback(() => { setIsRefreshing(true) fetchSessions(false) }, [fetchSessions]) // Show confirm dialog for single session const handleRevokeSession = useCallback( (sessionId: string) => { if (!accessToken) return setPendingSessionId(sessionId) setConfirmDialogVisible(true) }, [accessToken], ) // Actually revoke a single session const confirmRevokeSession = useCallback(async () => { if (!accessToken || !pendingSessionId) return setConfirmDialogVisible(false) setRevokingSessionId(pendingSessionId) try { const result = await authApi.revokeSession(accessToken, pendingSessionId) if (result.kind === "ok") { setSessions((prev) => prev.filter((s) => s.id !== pendingSessionId)) setSuccessMessage(translate("securityScreen:sessionRevoked")) setSuccessDialogVisible(true) } } catch (e) { if (__DEV__) console.error("Failed to revoke session:", e) } finally { setRevokingSessionId(null) setPendingSessionId(null) } }, [accessToken, pendingSessionId]) // Show confirm dialog for all other sessions const handleRevokeAllOther = useCallback(() => { if (!accessToken) return setConfirmAllDialogVisible(true) }, [accessToken]) // Actually revoke all other sessions const confirmRevokeAllOther = useCallback(async () => { if (!accessToken) return setConfirmAllDialogVisible(false) setIsRevokingAll(true) try { const result = await authApi.revokeOtherSessions(accessToken) if (result.kind === "ok") { const revokedCount = result.data.data?.revokedCount || 0 // Keep only current session setSessions((prev) => prev.filter((s) => s.isCurrent)) if (revokedCount > 0) { setSuccessMessage(translate("securityScreen:sessionsRevoked", { count: revokedCount })) setSuccessDialogVisible(true) } } } catch (e) { if (__DEV__) console.error("Failed to revoke other sessions:", e) } finally { setIsRevokingAll(false) } }, [accessToken]) // Get device icon based on device type const getDeviceIcon = (deviceType: string): "monitor" | "smartphone" | "tablet" => { switch (deviceType.toLowerCase()) { case "desktop": return "monitor" case "mobile": return "smartphone" case "tablet": return "tablet" default: return "monitor" } } // Format last active time const formatLastActive = (dateString: string): string => { const date = new Date(dateString) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMins < 1) return "Just now" if (diffMins < 60) return `${diffMins}m ago` if (diffHours < 24) return `${diffHours}h ago` if (diffDays < 7) return `${diffDays}d ago` return date.toLocaleDateString() } // Get other sessions (not current) const otherSessions = sessions.filter((s) => !s.isCurrent) const currentSession = sessions.find((s) => s.isCurrent) return ( } > {isLoading ? ( ) : ( <> {/* Current Session */} {currentSession && ( {currentSession.deviceInfo.deviceName || `${currentSession.deviceInfo.os} - ${currentSession.deviceInfo.browser}`} {translate("securityScreen:currentDevice")} {currentSession.location?.city ? `${currentSession.location.city} · ${currentSession.ipAddress}` : currentSession.ipAddress} {translate("securityScreen:lastActive")}:{" "} {formatLastActive(currentSession.lastActiveAt)} )} {/* Other Sessions */} {otherSessions.length > 0 ? otherSessions.map((session) => ( {session.deviceInfo.deviceName || `${session.deviceInfo.os} - ${session.deviceInfo.browser}`} {session.location?.city ? `${session.location.city} · ${session.ipAddress}` : session.ipAddress} {translate("securityScreen:lastActive")}:{" "} {formatLastActive(session.lastActiveAt)} handleRevokeSession(session.id)} disabled={revokingSessionId === session.id} style={themed($logoutButton)} > {revokingSessionId === session.id ? ( ) : ( {translate("securityScreen:logoutDevice")} )} )) : !currentSession && ( )} )} {/* Fixed Bottom Button - Only show when there are other sessions */} {otherSessions.length > 0 && (