@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
423 lines (414 loc) • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("react");
var _reactNative = require("react-native");
var _OxyContext = require("../context/OxyContext");
var _sonner = require("../../lib/sonner");
var _confirmAction = require("../utils/confirmAction");
var _components = require("../components");
var _jsxRuntime = require("react/jsx-runtime");
const SessionManagementScreen = ({
onClose,
theme,
goBack
}) => {
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);
const isDarkTheme = theme === 'dark';
const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
const backgroundColor = isDarkTheme ? '#121212' : '#FFFFFF';
const secondaryBackgroundColor = isDarkTheme ? '#222222' : '#F5F5F5';
const borderColor = isDarkTheme ? '#444444' : '#E0E0E0';
const primaryColor = '#0066CC';
const dangerColor = '#D32F2F';
const successColor = '#2E7D32';
// 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) {
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) {
console.error('Logout session failed:', error);
_sonner.toast.error('Failed to logout session. Please try again.');
} finally {
setActionLoading(null);
}
});
}, [logout, refreshSessions]);
// Memoized logout other sessions handler - prevents unnecessary re-renders
const handleLogoutOtherSessions = (0, _react.useCallback)(async () => {
const otherSessionsCount = userSessions.filter(s => s.sessionId !== activeSessionId).length;
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) {
console.error('Logout other sessions failed:', error);
_sonner.toast.error('Failed to logout other sessions. Please try again.');
} finally {
setActionLoading(null);
}
});
}, [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) {
console.error('Logout all sessions failed:', error);
_sonner.toast.error('Failed to logout all sessions. Please try again.');
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) {
console.error('Switch session failed', e);
_sonner.toast.error('Failed to switch session');
} finally {
setSwitchLoading(null);
}
}, [activeSessionId, switchSession]);
(0, _react.useEffect)(() => {
loadSessions();
}, [loadSessions]);
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..."
})]
});
}
// 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 ? '#1E2A38' : '#E6F2FF',
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 ? '#3A1E1E' : '#FFEBEE',
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]);
// 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]);
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]);
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: [styles.container, {
backgroundColor
}],
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Header, {
title: "Active Sessions",
subtitle: "Manage your active sessions across all devices",
theme: theme,
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: () => loadSessions(true),
tintColor: primaryColor
}),
children: userSessions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
children: [lastRefreshed && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
style: [styles.metaText, {
color: isDarkTheme ? '#777' : '#777',
marginBottom: 6
}],
children: ["Last refreshed ", formatRelative(lastRefreshed.toISOString())]
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: styles.fullBleed,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.GroupedSection, {
items: sessionItems,
theme: theme
})
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: {
height: 12
}
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
style: styles.fullBleed,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.GroupedSection, {
items: bulkItems,
theme: theme
})
})]
}) : /*#__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: {
padding: 20,
paddingTop: 0
},
// 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'
},
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