@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
974 lines (920 loc) • 39.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useOxy = exports.default = exports.OxyProvider = exports.OxyContextProvider = void 0;
var _react = require("react");
var _core = require("../../core");
var _sessionUtils = require("../../utils/sessionUtils");
var _deviceManager = require("../../utils/deviceManager");
var _useSessionSocket = require("../hooks/useSessionSocket");
var _sonner = require("../../lib/sonner");
var _authStore = require("../stores/authStore");
var _languageUtils = require("../../utils/languageUtils");
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // Define the context shape
// Create the context with default values
const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
// Props for the OxyContextProvider
// Platform storage implementation
// Web localStorage implementation
class WebStorage {
async getItem(key) {
return localStorage.getItem(key);
}
async setItem(key, value) {
localStorage.setItem(key, value);
}
async removeItem(key) {
localStorage.removeItem(key);
}
async clear() {
localStorage.clear();
}
}
// React Native AsyncStorage implementation
let AsyncStorage;
// Determine the platform and set up storage
const isReactNative = () => {
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
};
// Get appropriate storage for the platform
const getStorage = async () => {
if (isReactNative()) {
if (!AsyncStorage) {
try {
const asyncStorageModule = await Promise.resolve().then(() => _interopRequireWildcard(require('@react-native-async-storage/async-storage')));
AsyncStorage = asyncStorageModule.default;
} catch (error) {
console.error('Failed to import AsyncStorage:', error);
throw new Error('AsyncStorage is required in React Native environment');
}
}
return AsyncStorage;
}
return new WebStorage();
};
// Storage keys for sessions
const getStorageKeys = (prefix = 'oxy_session') => ({
activeSessionId: `${prefix}_active_session_id`,
// Only store the active session ID
sessionIds: `${prefix}_session_ids`,
// Store all session IDs for quick account loading
language: `${prefix}_language` // Store the selected language
});
let cachedUseFollowHook = null;
const loadUseFollowHook = () => {
if (cachedUseFollowHook) {
return cachedUseFollowHook;
}
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {
useFollow
} = require('../hooks/useFollow');
cachedUseFollowHook = useFollow;
return cachedUseFollowHook;
} catch (error) {
if (__DEV__) {
console.warn('useFollow hook is not available. Please import useFollow from @oxyhq/services directly.', error);
}
const fallback = () => {
throw new Error('useFollow hook is only available in the UI bundle. Import it from @oxyhq/services.');
};
cachedUseFollowHook = fallback;
return cachedUseFollowHook;
}
};
const OxyProvider = ({
children,
oxyServices: providedOxyServices,
baseURL,
storageKeyPrefix = 'oxy_session',
onAuthStateChange,
onError,
bottomSheetRef
}) => {
// Create oxyServices automatically if not provided
const oxyServicesRef = (0, _react.useRef)(null);
if (!oxyServicesRef.current) {
if (providedOxyServices) {
oxyServicesRef.current = providedOxyServices;
} else if (baseURL) {
oxyServicesRef.current = new _core.OxyServices({
baseURL
});
} else {
throw new Error('Either oxyServices or baseURL must be provided to OxyContextProvider');
}
}
const oxyServices = oxyServicesRef.current;
// Zustand state
const user = (0, _authStore.useAuthStore)(state => state.user);
const isAuthenticated = (0, _authStore.useAuthStore)(state => state.isAuthenticated);
const isLoading = (0, _authStore.useAuthStore)(state => state.isLoading);
const error = (0, _authStore.useAuthStore)(state => state.error);
const loginSuccess = (0, _authStore.useAuthStore)(state => state.loginSuccess);
const loginFailure = (0, _authStore.useAuthStore)(state => state.loginFailure);
const logoutStore = (0, _authStore.useAuthStore)(state => state.logout);
// Local state for non-auth fields
const [minimalUser, setMinimalUser] = (0, _react.useState)(null);
const [sessions, setSessions] = (0, _react.useState)([]);
const [activeSessionId, setActiveSessionId] = (0, _react.useState)(null);
// Track in-flight refresh to prevent duplicate calls
const refreshInFlightRef = (0, _react.useRef)(null);
// Track recently removed session IDs to avoid validating them (cleared after 5 seconds)
const removedSessionsRef = (0, _react.useRef)(new Set());
const [storage, setStorage] = (0, _react.useState)(null);
const [currentLanguage, setCurrentLanguage] = (0, _react.useState)('en-US');
const useFollowHook = (0, _react.useMemo)(() => loadUseFollowHook(), []);
// Storage keys (memoized to prevent infinite loops) - declared early for use in helpers
const keys = (0, _react.useMemo)(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
// Helper to apply language preference from user/server
const applyLanguagePreference = (0, _react.useCallback)(async user => {
const userLanguage = user?.language;
if (!userLanguage || !storage) return;
try {
const serverLang = (0, _languageUtils.normalizeLanguageCode)(userLanguage);
await storage.setItem(keys.language, serverLang);
setCurrentLanguage(serverLang);
} catch (e) {
if (__DEV__) {
console.warn('Failed to apply server language preference', e);
}
}
}, [storage, keys.language]);
const mapSessionsToClient = (0, _react.useCallback)((sessions, fallbackDeviceId, fallbackUserId) => {
return sessions.map(s => ({
sessionId: s.sessionId,
deviceId: s.deviceId || fallbackDeviceId || '',
expiresAt: s.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
lastActive: s.lastActive || new Date().toISOString(),
userId: s.user?.id || s.userId || s.user?._id?.toString() || fallbackUserId || '',
isCurrent: Boolean(s.isCurrent)
}));
}, []);
// Save all session IDs to storage for quick loading on initialization
const saveSessionIds = (0, _react.useCallback)(async sessionIds => {
if (!storage) return;
try {
const uniqueIds = Array.from(new Set(sessionIds));
await storage.setItem(keys.sessionIds, JSON.stringify(uniqueIds));
} catch (err) {
if (__DEV__) {
console.warn('Failed to save session IDs:', err);
}
}
}, [storage, keys.sessionIds]);
const updateSessions = (0, _react.useCallback)((newSessions, mergeWithExisting = false) => {
setSessions(prevSessions => {
const sessionsToProcess = mergeWithExisting ? (0, _sessionUtils.mergeSessions)(prevSessions, newSessions, activeSessionId, false) : (0, _sessionUtils.normalizeAndSortSessions)(newSessions, activeSessionId, false);
// Save all session IDs to storage
if (storage) {
const allSessionIds = sessionsToProcess.map(s => s.sessionId);
saveSessionIds(allSessionIds).catch(() => {
// Ignore errors - non-critical
});
}
return (0, _sessionUtils.sessionsArraysEqual)(prevSessions, sessionsToProcess) ? prevSessions : sessionsToProcess;
});
}, [activeSessionId, storage, saveSessionIds]);
// Token ready state - start optimistically so children render immediately
const [tokenReady, setTokenReady] = (0, _react.useState)(true);
// Clear all storage
const clearAllStorage = (0, _react.useCallback)(async () => {
if (!storage) return;
try {
await storage.removeItem(keys.activeSessionId);
await storage.removeItem(keys.sessionIds);
} catch (err) {
if (__DEV__) {
console.error('Clear storage error:', err);
}
onError?.({
message: 'Failed to clear storage',
code: 'STORAGE_ERROR',
status: 500
});
}
}, [storage, keys, onError]);
// Initialize storage
(0, _react.useEffect)(() => {
const initStorage = async () => {
try {
const platformStorage = await getStorage();
setStorage(platformStorage);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to initialize storage';
_authStore.useAuthStore.setState({
error: errorMessage
});
onError?.({
message: errorMessage,
code: 'STORAGE_INIT_ERROR',
status: 500
});
}
};
initStorage();
}, [onError]);
// Initialize authentication state
// Note: We don't set isLoading during initialization to avoid showing spinners
// Children render immediately and can check isTokenReady/isAuthenticated themselves
(0, _react.useEffect)(() => {
const initAuth = async () => {
if (!storage) return;
// Don't set isLoading during initialization - let it happen in background
try {
// Load saved language preference
const savedLanguageRaw = await storage.getItem(keys.language);
const savedLanguage = (0, _languageUtils.normalizeLanguageCode)(savedLanguageRaw) || savedLanguageRaw;
if (savedLanguage) {
setCurrentLanguage(savedLanguage);
}
// Load all stored session IDs and validate them
const storedSessionIdsJson = await storage.getItem(keys.sessionIds);
const storedSessionIds = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
// Try to restore active session from storage
const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
const validSessions = [];
// If we have stored session IDs, validate them (even without active session)
if (storedSessionIds.length > 0) {
if (__DEV__) {
console.log('Loading stored sessions on init:', storedSessionIds.length);
}
// Validate each stored session ID and build session list
for (const sessionId of storedSessionIds) {
try {
const validation = await oxyServices.validateSession(sessionId, {
useHeaderValidation: true
});
if (validation.valid && validation.user) {
validSessions.push({
sessionId,
userId: validation.user.id?.toString() || '',
deviceId: '',
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
lastActive: new Date().toISOString(),
isCurrent: sessionId === storedActiveSessionId
});
}
} catch (e) {
// Session invalid, skip it
if (__DEV__) {
console.warn('Session validation failed for:', sessionId, e);
}
}
}
// Update sessions list with validated sessions (even if no active session)
if (validSessions.length > 0) {
updateSessions(validSessions, false);
}
}
// If we have an active session, authenticate with it
if (storedActiveSessionId) {
try {
const validation = await oxyServices.validateSession(storedActiveSessionId, {
useHeaderValidation: true
});
if (validation.valid) {
setActiveSessionId(storedActiveSessionId);
await oxyServices.getTokenBySession(storedActiveSessionId);
const fullUser = await oxyServices.getUserBySession(storedActiveSessionId);
loginSuccess(fullUser);
setMinimalUser({
id: fullUser.id,
username: fullUser.username,
avatar: fullUser.avatar
});
await applyLanguagePreference(fullUser);
try {
const deviceSessions = await oxyServices.getDeviceSessions(storedActiveSessionId);
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
updateSessions(allDeviceSessions, true);
} catch (e) {
if (__DEV__) {
console.warn('Failed to get device sessions on init, falling back to user sessions:', e);
}
const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
updateSessions(mapSessionsToClient(serverSessions, undefined, fullUser.id), false);
}
onAuthStateChange?.(fullUser);
} else {
// Active session invalid, remove it but keep other sessions
await storage.removeItem(keys.activeSessionId);
// Update session list to remove invalid active session
updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
}
} catch (e) {
if (__DEV__) {
console.error('Active session validation error', e);
}
// Remove invalid active session but keep other sessions
await storage.removeItem(keys.activeSessionId);
updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
}
}
setTokenReady(true);
} catch (e) {
if (__DEV__) {
console.error('Auth init error', e);
}
await clearAllStorage();
setTokenReady(true);
}
};
initAuth();
}, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference, mapSessionsToClient, updateSessions]);
// Save active session ID to storage (only session ID, no user data)
const saveActiveSessionId = (0, _react.useCallback)(async sessionId => {
if (!storage) return;
await storage.setItem(keys.activeSessionId, sessionId);
}, [storage, keys.activeSessionId]);
const switchToSession = (0, _react.useCallback)(async sessionId => {
try {
const validation = await oxyServices.validateSession(sessionId, {
useHeaderValidation: true
});
if (!validation.valid) {
updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
throw new Error('Session is invalid or expired');
}
if (!validation.user) {
throw new Error('User data not available from session validation');
}
const fullUser = validation.user;
await oxyServices.getTokenBySession(sessionId);
setTokenReady(true);
setActiveSessionId(sessionId);
loginSuccess(fullUser);
setMinimalUser({
id: fullUser.id,
username: fullUser.username,
avatar: fullUser.avatar
});
await saveActiveSessionId(sessionId);
await applyLanguagePreference(fullUser);
oxyServices.getDeviceSessions(sessionId).then(deviceSessions => {
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
updateSessions(allDeviceSessions, true);
}).catch(error => {
if (__DEV__) console.warn('Failed to get device sessions after switch:', error);
});
onAuthStateChange?.(fullUser);
} catch (error) {
const isInvalidSession = error?.response?.status === 401 || error?.message?.includes('Invalid or expired session') || error?.message?.includes('Session is invalid');
if (isInvalidSession) {
updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
if (sessionId === activeSessionId && sessions.length > 1) {
const otherSessions = sessions.filter(s => s.sessionId !== sessionId);
for (const otherSession of otherSessions) {
try {
const otherValidation = await oxyServices.validateSession(otherSession.sessionId, {
useHeaderValidation: true
});
if (otherValidation.valid) {
await switchToSession(otherSession.sessionId);
return;
}
} catch {
// Continue to next session
continue;
}
}
}
}
const errorMessage = error instanceof Error ? error.message : 'Failed to switch session';
if (__DEV__) {
console.error('Switch session error:', error);
}
_authStore.useAuthStore.setState({
error: errorMessage
});
onError?.({
message: errorMessage,
code: isInvalidSession ? 'INVALID_SESSION' : 'SESSION_SWITCH_ERROR',
status: isInvalidSession ? 401 : 500
});
setTokenReady(false);
throw error; // Re-throw so calling code can handle it
}
}, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference, mapSessionsToClient, onError, activeSessionId, sessions]);
const login = (0, _react.useCallback)(async (username, password, deviceName) => {
if (!storage) throw new Error('Storage not initialized');
_authStore.useAuthStore.setState({
isLoading: true,
error: null
});
try {
const deviceFingerprint = _deviceManager.DeviceManager.getDeviceFingerprint();
const deviceInfo = await _deviceManager.DeviceManager.getDeviceInfo();
const response = await oxyServices.signIn(username, password, deviceName || deviceInfo.deviceName || _deviceManager.DeviceManager.getDefaultDeviceName(), deviceFingerprint);
// Handle MFA requirement
if (response && 'mfaRequired' in response && response.mfaRequired) {
const mfaError = new Error('Multi-factor authentication required');
mfaError.code = 'MFA_REQUIRED';
mfaError.mfaToken = response.mfaToken;
mfaError.expiresAt = response.expiresAt;
throw mfaError;
}
const sessionResponse = response;
await oxyServices.getTokenBySession(sessionResponse.sessionId);
const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
let allDeviceSessions = [];
try {
const deviceSessions = await oxyServices.getDeviceSessions(sessionResponse.sessionId);
allDeviceSessions = mapSessionsToClient(deviceSessions, sessionResponse.deviceId, fullUser.id);
} catch (error) {
if (__DEV__) {
console.warn('Failed to get device sessions, falling back to user sessions:', error);
}
const serverSessions = await oxyServices.getSessionsBySessionId(sessionResponse.sessionId);
allDeviceSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
}
const userUserId = fullUser.id?.toString();
const existingSession = allDeviceSessions.find(s => s.userId?.toString() === userUserId && s.sessionId !== sessionResponse.sessionId);
if (existingSession) {
try {
await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
} catch (logoutError) {
if (__DEV__) {
console.warn('Failed to logout duplicate session:', logoutError);
}
}
await switchToSession(existingSession.sessionId);
loginSuccess(fullUser);
setMinimalUser(sessionResponse.user);
updateSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId), false);
onAuthStateChange?.(fullUser);
return fullUser;
}
setActiveSessionId(sessionResponse.sessionId);
await saveActiveSessionId(sessionResponse.sessionId);
loginSuccess(fullUser);
setMinimalUser(sessionResponse.user);
updateSessions(allDeviceSessions, true);
onAuthStateChange?.(fullUser);
return fullUser;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Login failed';
loginFailure(errorMessage);
onError?.({
message: errorMessage,
code: 'LOGIN_ERROR',
status: 401
});
throw error;
} finally {
_authStore.useAuthStore.setState({
isLoading: false
});
}
}, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure, mapSessionsToClient, onError, sessions, switchToSession]);
// Clear session state without calling API (for remote removals)
const clearSessionState = (0, _react.useCallback)(async () => {
updateSessions([], false);
setActiveSessionId(null);
logoutStore();
setMinimalUser(null);
await clearAllStorage();
onAuthStateChange?.(null);
}, [updateSessions, logoutStore, clearAllStorage, onAuthStateChange]);
// Logout method
const logout = (0, _react.useCallback)(async targetSessionId => {
if (!activeSessionId) return;
try {
const sessionToLogout = targetSessionId || activeSessionId;
await oxyServices.logoutSession(activeSessionId, sessionToLogout);
const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
updateSessions(filteredSessions, false);
if (sessionToLogout === activeSessionId) {
if (filteredSessions.length > 0) {
await switchToSession(filteredSessions[0].sessionId);
} else {
setActiveSessionId(null);
logoutStore();
setMinimalUser(null);
await storage?.removeItem(keys.activeSessionId);
if (onAuthStateChange) {
onAuthStateChange(null);
}
}
}
} catch (error) {
// Check if error is 401 (session already removed remotely)
const is401Error = error?.response?.status === 401 || error?.message?.includes('Invalid or expired session') || error?.message?.includes('Session is invalid');
if (is401Error && targetSessionId === activeSessionId) {
// Session was already removed remotely, clear state without API call
await clearSessionState();
return;
}
const errorMessage = error instanceof Error ? error.message : 'Logout failed';
if (__DEV__) {
console.error('Logout error:', error);
}
_authStore.useAuthStore.setState({
error: errorMessage
});
onError?.({
message: errorMessage,
code: 'LOGOUT_ERROR',
status: 500
});
}
}, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, storage, keys.activeSessionId, onAuthStateChange, onError, clearSessionState]);
const logoutAll = (0, _react.useCallback)(async () => {
if (!activeSessionId) {
const error = new Error('No active session found');
_authStore.useAuthStore.setState({
error: error.message
});
onError?.({
message: error.message,
code: 'NO_SESSION_ERROR',
status: 404
});
throw error;
}
try {
await oxyServices.logoutAllSessions(activeSessionId);
updateSessions([], false);
setActiveSessionId(null);
logoutStore();
setMinimalUser(null);
await clearAllStorage();
onAuthStateChange?.(null);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Logout all failed';
_authStore.useAuthStore.setState({
error: errorMessage
});
onError?.({
message: errorMessage,
code: 'LOGOUT_ALL_ERROR',
status: 500
});
throw error;
}
}, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
// Token restoration is handled in initAuth and switchToSession
// No separate effect needed - children render immediately with isTokenReady available
// Sign up method
const signUp = (0, _react.useCallback)(async (username, email, password) => {
if (!storage) throw new Error('Storage not initialized');
_authStore.useAuthStore.setState({
isLoading: true,
error: null
});
try {
await oxyServices.signUp(username, email, password);
const user = await login(username, password);
return user;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Sign up failed';
loginFailure(errorMessage);
onError?.({
message: errorMessage,
code: 'SIGNUP_ERROR',
status: 400
});
throw error;
} finally {
_authStore.useAuthStore.setState({
isLoading: false
});
}
}, [storage, oxyServices, login, loginFailure, onError]);
// Complete MFA login by verifying TOTP
const completeMfaLogin = (0, _react.useCallback)(async (mfaToken, code) => {
if (!storage) throw new Error('Storage not initialized');
_authStore.useAuthStore.setState({
isLoading: true,
error: null
});
try {
const response = await oxyServices.verifyTotpLogin(mfaToken, code);
// Set as active session
setActiveSessionId(response.sessionId);
await saveActiveSessionId(response.sessionId);
// Fetch access token and user data
await oxyServices.getTokenBySession(response.sessionId);
const fullUser = await oxyServices.getUserBySession(response.sessionId);
loginSuccess(fullUser);
setMinimalUser({
id: fullUser.id,
username: fullUser.username,
avatar: fullUser.avatar
});
await applyLanguagePreference(fullUser);
// Get all device sessions to support multiple accounts
try {
const deviceSessions = await oxyServices.getDeviceSessions(response.sessionId);
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
updateSessions(allDeviceSessions, true);
} catch (error) {
// Fallback to user sessions if device sessions fail
if (__DEV__) {
console.warn('Failed to get device sessions for MFA, falling back to user sessions:', error);
}
const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
const userSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
updateSessions(userSessions, true);
}
onAuthStateChange?.(fullUser);
return fullUser;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'MFA verification failed';
loginFailure(errorMessage);
onError?.({
message: errorMessage,
code: 'MFA_ERROR',
status: 401
});
throw error;
} finally {
_authStore.useAuthStore.setState({
isLoading: false
});
}
}, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange, applyLanguagePreference, onError]);
const switchSession = (0, _react.useCallback)(async sessionId => {
await switchToSession(sessionId);
}, [switchToSession]);
const removeSession = (0, _react.useCallback)(async sessionId => {
await logout(sessionId);
}, [logout]);
const refreshSessions = (0, _react.useCallback)(async () => {
if (!activeSessionId) return;
// If a refresh is already in progress, return the existing promise
if (refreshInFlightRef.current) {
return refreshInFlightRef.current;
}
// Create the refresh promise
const refreshPromise = (async () => {
try {
const deviceSessions = await oxyServices.getDeviceSessions(activeSessionId);
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, user?.id);
updateSessions(allDeviceSessions, true);
} catch (error) {
if (__DEV__) {
console.warn('Failed to refresh device sessions, falling back to user sessions:', error);
}
try {
const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
const userSessions = mapSessionsToClient(serverSessions, undefined, user?.id);
updateSessions(userSessions, true);
} catch (fallbackError) {
if (__DEV__) {
console.error('Refresh sessions error:', fallbackError);
}
// Check if the error is a 401 (session removed/invalid)
const is401Error = fallbackError?.response?.status === 401 || fallbackError?.message?.includes('Invalid or expired session') || fallbackError?.message?.includes('Session is invalid');
// If 401 error, don't try to validate other sessions - they may have been removed too
// Instead, check if current session is in removed sessions list
if (is401Error && removedSessionsRef.current.has(activeSessionId || '')) {
// Current session was removed, clear all and logout
updateSessions([], false);
setActiveSessionId(null);
logoutStore();
setMinimalUser(null);
await clearAllStorage();
onAuthStateChange?.(null);
return;
}
// If the current session is invalid, try to find another valid session
// But skip sessions that were recently removed
if (sessions.length > 1) {
const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId && !removedSessionsRef.current.has(s.sessionId));
for (const session of otherSessions) {
try {
const validation = await oxyServices.validateSession(session.sessionId, {
useHeaderValidation: true
});
if (validation.valid) {
await switchToSession(session.sessionId);
return;
}
} catch (validationError) {
// If validation returns 401, mark this session as removed
const isValidation401 = validationError?.response?.status === 401 || validationError?.message?.includes('Invalid or expired session');
if (isValidation401) {
removedSessionsRef.current.add(session.sessionId);
// Clear from tracking after 5 seconds
setTimeout(() => {
removedSessionsRef.current.delete(session.sessionId);
}, 5000);
}
continue;
}
}
}
// No valid sessions found, clear all
updateSessions([], false);
setActiveSessionId(null);
logoutStore();
setMinimalUser(null);
await clearAllStorage();
onAuthStateChange?.(null);
}
} finally {
// Clear the in-flight ref when done
refreshInFlightRef.current = null;
}
})();
refreshInFlightRef.current = refreshPromise;
return refreshPromise;
}, [activeSessionId, oxyServices, user?.id, updateSessions, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapSessionsToClient]);
// Device management methods
const getDeviceSessions = (0, _react.useCallback)(async () => {
if (!activeSessionId) throw new Error('No active session');
try {
return await oxyServices.getDeviceSessions(activeSessionId);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to get device sessions';
onError?.({
message: errorMessage,
code: 'GET_DEVICE_SESSIONS_ERROR',
status: 500
});
throw error;
}
}, [activeSessionId, oxyServices, onError]);
const logoutAllDeviceSessions = (0, _react.useCallback)(async () => {
if (!activeSessionId) throw new Error('No active session');
try {
await oxyServices.logoutAllDeviceSessions(activeSessionId);
updateSessions([], false);
setActiveSessionId(null);
logoutStore();
setMinimalUser(null);
await clearAllStorage();
onAuthStateChange?.(null);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to logout all device sessions';
onError?.({
message: errorMessage,
code: 'LOGOUT_ALL_DEVICES_ERROR',
status: 500
});
throw error;
}
}, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
const updateDeviceName = (0, _react.useCallback)(async deviceName => {
if (!activeSessionId) throw new Error('No active session');
try {
await oxyServices.updateDeviceName(activeSessionId, deviceName);
await _deviceManager.DeviceManager.updateDeviceName(deviceName);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to update device name';
onError?.({
message: errorMessage,
code: 'UPDATE_DEVICE_NAME_ERROR',
status: 500
});
throw error;
}
}, [activeSessionId, oxyServices, onError]);
// Language management method
const setLanguage = (0, _react.useCallback)(async languageId => {
if (!storage) throw new Error('Storage not initialized');
try {
await storage.setItem(keys.language, languageId);
setCurrentLanguage(languageId);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to save language preference';
onError?.({
message: errorMessage,
code: 'LANGUAGE_SAVE_ERROR',
status: 500
});
throw error;
}
}, [storage, keys.language, onError]);
// Bottom sheet control methods
const showBottomSheet = (0, _react.useCallback)(screenOrConfig => {
if (__DEV__) console.log('showBottomSheet called with:', screenOrConfig);
if (bottomSheetRef?.current) {
if (__DEV__) console.log('bottomSheetRef is available');
// First, show the bottom sheet
if (bottomSheetRef.current.expand) {
if (__DEV__) console.log('Expanding bottom sheet');
bottomSheetRef.current.expand();
} else if (bottomSheetRef.current.present) {
if (__DEV__) console.log('Presenting bottom sheet');
bottomSheetRef.current.present();
} else if (__DEV__) {
console.warn('No expand or present method available on bottomSheetRef');
}
// Then navigate to the specified screen if provided
if (screenOrConfig) {
// Add a small delay to ensure the bottom sheet is opened first
setTimeout(() => {
if (typeof screenOrConfig === 'string') {
// Simple screen name
if (__DEV__) console.log('Navigating to screen:', screenOrConfig);
bottomSheetRef.current?.navigate?.(screenOrConfig);
} else {
// Screen with props
if (__DEV__) console.log('Navigating to screen with props:', screenOrConfig.screen, screenOrConfig.props);
bottomSheetRef.current?.navigate?.(screenOrConfig.screen, screenOrConfig.props);
}
}, 100);
}
} else if (__DEV__) {
console.warn('bottomSheetRef is not available. Pass a bottomSheetRef to OxyProvider.');
}
}, [bottomSheetRef]);
const hideBottomSheet = (0, _react.useCallback)(() => {
if (bottomSheetRef?.current) {
bottomSheetRef.current.dismiss?.();
}
}, [bottomSheetRef]);
// Get current deviceId from active session
const currentDeviceId = (0, _react.useMemo)(() => {
if (!activeSessionId || !sessions.length) return null;
const activeSession = sessions.find(s => s.sessionId === activeSessionId);
return activeSession?.deviceId || null;
}, [activeSessionId, sessions]);
// Callback to track removed sessions
const handleSessionRemoved = (0, _react.useCallback)(sessionId => {
// Add to removed sessions tracking
removedSessionsRef.current.add(sessionId);
// Clear from tracking after 5 seconds
setTimeout(() => {
removedSessionsRef.current.delete(sessionId);
}, 5000);
}, []);
// Integrate socket for real-time session updates
(0, _useSessionSocket.useSessionSocket)({
userId: user?.id,
activeSessionId,
currentDeviceId,
refreshSessions,
logout,
clearSessionState,
baseURL: oxyServices.getBaseURL(),
onRemoteSignOut: (0, _react.useCallback)(() => {
_sonner.toast.info('You have been signed out remotely.');
logout();
}, [logout]),
onSessionRemoved: handleSessionRemoved
});
// Compute language metadata from currentLanguage
const languageMetadata = (0, _react.useMemo)(() => (0, _languageUtils.getLanguageMetadata)(currentLanguage), [currentLanguage]);
const languageName = (0, _react.useMemo)(() => (0, _languageUtils.getLanguageName)(currentLanguage), [currentLanguage]);
const nativeLanguageName = (0, _react.useMemo)(() => (0, _languageUtils.getNativeLanguageName)(currentLanguage), [currentLanguage]);
const contextValue = (0, _react.useMemo)(() => ({
user,
minimalUser,
sessions,
activeSessionId,
isAuthenticated,
isLoading,
isTokenReady: tokenReady,
error,
currentLanguage,
currentLanguageMetadata: languageMetadata,
currentLanguageName: languageName,
currentNativeLanguageName: nativeLanguageName,
login,
logout,
logoutAll,
signUp,
completeMfaLogin,
switchSession,
removeSession,
refreshSessions,
setLanguage,
getDeviceSessions,
logoutAllDeviceSessions,
updateDeviceName,
oxyServices,
bottomSheetRef,
showBottomSheet,
hideBottomSheet,
useFollow: useFollowHook
}), [user?.id,
// Only depend on user ID, not the entire user object
minimalUser?.id, sessions.length,
// Only depend on sessions count, not the entire array
activeSessionId, isAuthenticated, isLoading, tokenReady, error, currentLanguage, languageMetadata, languageName, nativeLanguageName, login, logout, logoutAll, signUp, completeMfaLogin, switchSession, removeSession, refreshSessions, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, oxyServices, bottomSheetRef, showBottomSheet, hideBottomSheet, useFollowHook]);
// Always render children - let the consuming app decide how to handle token loading state
return /*#__PURE__*/(0, _jsxRuntime.jsx)(OxyContext.Provider, {
value: contextValue,
children: children
});
};
// Alias for backward compatibility
exports.OxyProvider = OxyProvider;
const OxyContextProvider = exports.OxyContextProvider = OxyProvider;
// Hook to use the context
const useOxy = () => {
const context = (0, _react.useContext)(OxyContext);
if (!context) {
throw new Error('useOxy must be used within an OxyContextProvider');
}
return context;
};
exports.useOxy = useOxy;
var _default = exports.default = OxyContext;
//# sourceMappingURL=OxyContext.js.map