UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

455 lines (444 loc) 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var _reactNative = require("react-native"); var _spacing = require("../constants/spacing.js"); var _sonner = require("../../lib/sonner"); var _confirmAction = require("../utils/confirmAction.js"); var _index = require("../components/index.js"); var _useThemeStyles = require("../hooks/useThemeStyles.js"); var _themeUtils = require("../utils/themeUtils.js"); var _OxyContext = require("../context/OxyContext.js"); var _jsxRuntime = require("react/jsx-runtime"); // Button background colors for session actions const SWITCH_BUTTON_BG = { dark: '#1E2A38', light: '#E6F2FF' }; const LOGOUT_BUTTON_BG = { dark: '#3A1E1E', light: '#FFEBEE' }; const SessionManagementScreen = ({ onClose, theme, goBack }) => { // Use useOxy() hook for OxyContext values const { sessions: userSessions, activeSessionId, refreshSessions, logout, logoutAll, switchSession } = (0, _OxyContext.useOxy)(); const [loading, setLoading] = (0, _react.useState)(true); const [refreshing, setRefreshing] = (0, _react.useState)(false); const [actionLoading, setActionLoading] = (0, _react.useState)(null); const [switchLoading, setSwitchLoading] = (0, _react.useState)(null); const [lastRefreshed, setLastRefreshed] = (0, _react.useState)(null); // Use centralized theme styles hook for consistency const normalizedTheme = (0, _themeUtils.normalizeTheme)(theme); const themeStyles = (0, _useThemeStyles.useThemeStyles)(normalizedTheme); // Extract commonly used colors for readability const { textColor, backgroundColor, borderColor, primaryColor, dangerColor, successColor, isDarkTheme } = themeStyles; // Memoized load sessions function - prevents unnecessary re-renders const loadSessions = (0, _react.useCallback)(async (isRefresh = false) => { try { if (isRefresh) { setRefreshing(true); } else { setLoading(true); } await refreshSessions(); setLastRefreshed(new Date()); } catch (error) { if (__DEV__) { console.error('Failed to load sessions:', error); } if (_reactNative.Platform.OS === 'web') { _sonner.toast.error('Failed to load sessions. Please try again.'); } else { _reactNative.Alert.alert('Error', 'Failed to load sessions. Please try again.', [{ text: 'OK' }]); } } finally { setLoading(false); setRefreshing(false); } }, [refreshSessions]); // Memoized logout session handler - prevents unnecessary re-renders const handleLogoutSession = (0, _react.useCallback)(async sessionId => { (0, _confirmAction.confirmAction)('Are you sure you want to logout this session?', async () => { try { setActionLoading(sessionId); await logout(sessionId); await refreshSessions(); _sonner.toast.success('Session logged out successfully'); } catch (error) { if (__DEV__) { console.error('Logout session failed:', error); } _sonner.toast.error('Failed to logout session. Please try again.'); } finally { setActionLoading(null); } }); }, [logout, refreshSessions]); // Memoized bulk action items - prevents unnecessary re-renders when dependencies haven't changed const otherSessionsCount = (0, _react.useMemo)(() => userSessions.filter(s => s.sessionId !== activeSessionId).length, [userSessions, activeSessionId]); // Memoized logout other sessions handler - prevents unnecessary re-renders const handleLogoutOtherSessions = (0, _react.useCallback)(async () => { if (otherSessionsCount === 0) { _sonner.toast.info('No other sessions to logout.'); return; } (0, _confirmAction.confirmAction)(`This will logout ${otherSessionsCount} other session${otherSessionsCount > 1 ? 's' : ''}. Continue?`, async () => { try { setActionLoading('others'); for (const session of userSessions) { if (session.sessionId !== activeSessionId) { await logout(session.sessionId); } } await refreshSessions(); _sonner.toast.success('Other sessions logged out successfully'); } catch (error) { if (__DEV__) { console.error('Logout other sessions failed:', error); } _sonner.toast.error('Failed to logout other sessions. Please try again.'); } finally { setActionLoading(null); } }); }, [otherSessionsCount, userSessions, activeSessionId, logout, refreshSessions]); // Memoized logout all sessions handler - prevents unnecessary re-renders const handleLogoutAllSessions = (0, _react.useCallback)(async () => { (0, _confirmAction.confirmAction)('This will logout all sessions including this one and you will need to sign in again. Continue?', async () => { try { setActionLoading('all'); await logoutAll(); } catch (error) { if (__DEV__) { console.error('Logout all sessions failed:', error); } _sonner.toast.error('Failed to logout all sessions. Please try again.'); } finally { setActionLoading(null); } }); }, [logoutAll]); // Memoized relative time formatter - prevents function recreation on every render const formatRelative = (0, _react.useCallback)(dateString => { if (!dateString) return 'Unknown'; const date = new Date(dateString); const now = new Date(); const diffMs = date.getTime() - now.getTime(); const absMin = Math.abs(diffMs) / 60000; const isFuture = diffMs > 0; const fmt = n => n < 1 ? 'moments' : Math.floor(n); if (absMin < 1) return isFuture ? 'in moments' : 'just now'; if (absMin < 60) return isFuture ? `in ${fmt(absMin)}m` : `${fmt(absMin)}m ago`; const hrs = absMin / 60; if (hrs < 24) return isFuture ? `in ${fmt(hrs)}h` : `${fmt(hrs)}h ago`; const days = hrs / 24; if (days < 7) return isFuture ? `in ${fmt(days)}d` : `${fmt(days)}d ago`; return date.toLocaleDateString(); }, []); // Memoized switch session handler - prevents unnecessary re-renders const handleSwitchSession = (0, _react.useCallback)(async sessionId => { if (sessionId === activeSessionId) return; setSwitchLoading(sessionId); try { await switchSession(sessionId); _sonner.toast.success('Switched session'); } catch (e) { if (__DEV__) { console.error('Switch session failed', e); } _sonner.toast.error('Failed to switch session'); } finally { setSwitchLoading(null); } }, [activeSessionId, switchSession]); // Memoized refresh handler for pull-to-refresh const handleRefresh = (0, _react.useCallback)(() => { loadSessions(true); }, [loadSessions]); (0, _react.useEffect)(() => { loadSessions(); }, [loadSessions]); // Memoized session items - prevents unnecessary re-renders when dependencies haven't changed const sessionItems = (0, _react.useMemo)(() => { return userSessions.map(session => { const isCurrent = session.sessionId === activeSessionId; const subtitleParts = []; if (session.deviceId) subtitleParts.push(`Device ${session.deviceId.substring(0, 10)}...`); subtitleParts.push(`Last ${formatRelative(session.lastActive)}`); subtitleParts.push(`Expires ${formatRelative(session.expiresAt)}`); return { id: session.sessionId, icon: isCurrent ? 'shield-checkmark' : 'laptop-outline', iconColor: isCurrent ? successColor : primaryColor, title: isCurrent ? 'Current Session' : `Session ${session.sessionId.substring(0, 8)}...`, subtitle: subtitleParts.join(' \u2022 '), showChevron: false, multiRow: true, customContentBelow: !isCurrent ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.sessionActionsRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { onPress: () => handleSwitchSession(session.sessionId), style: [styles.sessionPillButton, { backgroundColor: isDarkTheme ? SWITCH_BUTTON_BG.dark : SWITCH_BUTTON_BG.light, borderColor: primaryColor }], disabled: switchLoading === session.sessionId || actionLoading === session.sessionId, children: switchLoading === session.sessionId ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: primaryColor }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sessionPillText, { color: primaryColor }], children: "Switch" }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { onPress: () => handleLogoutSession(session.sessionId), style: [styles.sessionPillButton, { backgroundColor: isDarkTheme ? LOGOUT_BUTTON_BG.dark : LOGOUT_BUTTON_BG.light, borderColor: dangerColor }], disabled: actionLoading === session.sessionId || switchLoading === session.sessionId, children: actionLoading === session.sessionId ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: dangerColor }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sessionPillText, { color: dangerColor }], children: "Logout" }) })] }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.sessionActionsRow, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.currentBadgeText, { color: successColor }], children: "Active" }) }), selected: isCurrent, dense: true }; }); }, [userSessions, activeSessionId, formatRelative, successColor, primaryColor, isDarkTheme, switchLoading, actionLoading, handleSwitchSession, handleLogoutSession, dangerColor]); const bulkItems = (0, _react.useMemo)(() => [{ id: 'logout-others', icon: 'exit-outline', iconColor: primaryColor, title: 'Logout Other Sessions', subtitle: otherSessionsCount === 0 ? 'No other sessions' : 'End all sessions except this one', onPress: handleLogoutOtherSessions, showChevron: false, customContent: actionLoading === 'others' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: primaryColor }) : undefined, disabled: actionLoading === 'others' || otherSessionsCount === 0, dense: true }, { id: 'logout-all', icon: 'warning-outline', iconColor: dangerColor, title: 'Logout All Sessions', subtitle: 'End all sessions including this one', onPress: handleLogoutAllSessions, showChevron: false, customContent: actionLoading === 'all' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: dangerColor }) : undefined, disabled: actionLoading === 'all', dense: true }], [otherSessionsCount, primaryColor, dangerColor, handleLogoutOtherSessions, handleLogoutAllSessions, actionLoading]); if (loading) { return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.container, styles.centerContent, { backgroundColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "large", color: primaryColor }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.loadingText, { color: textColor }], children: "Loading sessions..." })] }); } return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.container, { backgroundColor }], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Header, { title: "Active Sessions", subtitle: "Manage your active sessions across all devices", onBack: goBack || onClose, elevation: "subtle" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, { style: styles.scrollView, contentContainerStyle: styles.scrollContainer, refreshControl: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.RefreshControl, { refreshing: refreshing, onRefresh: handleRefresh, tintColor: primaryColor }), children: userSessions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [lastRefreshed && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, { style: [styles.metaText, { color: '#777', marginBottom: 6 }], children: ["Last refreshed ", formatRelative(lastRefreshed.toISOString())] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.fullBleed, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GroupedSection, { items: sessionItems }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.sectionSpacer }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.fullBleed, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GroupedSection, { items: bulkItems }) })] }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.emptyState, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.emptyStateText, { color: isDarkTheme ? '#BBBBBB' : '#666666' }], children: "No active sessions found" }) }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.footer, { borderTopColor: borderColor }], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.closeButton, onPress: onClose, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.closeButtonText, { color: primaryColor }], children: "Close" }) }) })] }); }; const styles = _reactNative.StyleSheet.create({ container: { flex: 1 }, centerContent: { justifyContent: 'center', alignItems: 'center' }, scrollView: { flex: 1 }, scrollContainer: { ..._spacing.screenContentStyle, paddingTop: 0 // Header handles top spacing }, // Removed legacy session card & bulk action styles (now using GroupedSection) sessionActionsRow: { flexDirection: 'row', gap: 8, marginTop: 6 }, sessionPillButton: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 20, borderWidth: 1, flexDirection: 'row', alignItems: 'center' }, sessionPillText: { fontSize: 12, fontWeight: '600', letterSpacing: 0.3, textTransform: 'uppercase' }, currentBadgeText: { fontSize: 12, fontWeight: '600', paddingHorizontal: 10, paddingVertical: 4, backgroundColor: '#2E7D3215', borderRadius: 16, overflow: 'hidden', textTransform: 'uppercase', letterSpacing: 0.5 }, metaText: { fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: '600' }, fullBleed: { width: '100%', alignSelf: 'stretch' }, sectionSpacer: { height: 12 }, emptyState: { alignItems: 'center', paddingVertical: 40 }, emptyStateText: { fontSize: 16, fontStyle: 'italic' }, loadingText: { fontSize: 16, marginTop: 16 }, footer: { padding: 16, borderTopWidth: 1, alignItems: 'center' }, closeButton: { paddingVertical: 8, paddingHorizontal: 16 }, closeButtonText: { fontSize: 16, fontWeight: '600' } }); var _default = exports.default = SessionManagementScreen; //# sourceMappingURL=SessionManagementScreen.js.map