@oxyhq/services 5.13.1 → 5.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/lib/commonjs/core/HttpClient.js +238 -0
- package/lib/commonjs/core/HttpClient.js.map +1 -0
- package/lib/commonjs/core/OxyServices.js +530 -332
- package/lib/commonjs/core/OxyServices.js.map +1 -1
- package/lib/commonjs/core/RequestManager.js +199 -0
- package/lib/commonjs/core/RequestManager.js.map +1 -0
- package/lib/commonjs/core/index.js +38 -1
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/index.js +36 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/Avatar.js +94 -27
- package/lib/commonjs/ui/components/Avatar.js.map +1 -1
- package/lib/commonjs/ui/components/FollowButton.js +1 -0
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/internal/TextField.js +13 -8
- package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +183 -224
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +80 -22
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/commonjs/ui/index.js +4 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +32 -2
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +101 -59
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -2
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +75 -117
- package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js +0 -11
- package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignUpScreen.js +14 -16
- package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +50 -18
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +10 -10
- package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +16 -26
- package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +104 -212
- package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
- package/lib/commonjs/ui/stores/accountStore.js +237 -0
- package/lib/commonjs/ui/stores/accountStore.js.map +1 -0
- package/lib/commonjs/ui/stores/authStore.js +2 -1
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/commonjs/ui/styles/authStyles.js +14 -7
- package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
- package/lib/commonjs/utils/asyncUtils.js +9 -22
- package/lib/commonjs/utils/asyncUtils.js.map +1 -1
- package/lib/commonjs/utils/cache.js +259 -0
- package/lib/commonjs/utils/cache.js.map +1 -0
- package/lib/commonjs/utils/index.js +99 -0
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/commonjs/utils/languageUtils.js +159 -0
- package/lib/commonjs/utils/languageUtils.js.map +1 -0
- package/lib/commonjs/utils/requestUtils.js +217 -0
- package/lib/commonjs/utils/requestUtils.js.map +1 -0
- package/lib/commonjs/utils/sessionUtils.js +191 -0
- package/lib/commonjs/utils/sessionUtils.js.map +1 -0
- package/lib/module/core/HttpClient.js +232 -0
- package/lib/module/core/HttpClient.js.map +1 -0
- package/lib/module/core/OxyServices.js +528 -326
- package/lib/module/core/OxyServices.js.map +1 -1
- package/lib/module/core/RequestManager.js +194 -0
- package/lib/module/core/RequestManager.js.map +1 -0
- package/lib/module/core/index.js +2 -0
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/Avatar.js +94 -27
- package/lib/module/ui/components/Avatar.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +1 -0
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/internal/TextField.js +13 -8
- package/lib/module/ui/components/internal/TextField.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +182 -223
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +80 -22
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/index.js +4 -2
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +33 -2
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +102 -60
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +3 -2
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/LanguageSelectorScreen.js +73 -117
- package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInScreen.js +0 -11
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/ui/screens/SignUpScreen.js +14 -16
- package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +50 -18
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/screens/internal/SignInPasswordStep.js +10 -10
- package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignInPasswordStep.js +16 -26
- package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignInUsernameStep.js +105 -214
- package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
- package/lib/module/ui/stores/accountStore.js +229 -0
- package/lib/module/ui/stores/accountStore.js.map +1 -0
- package/lib/module/ui/stores/authStore.js +2 -1
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/module/ui/styles/authStyles.js +14 -7
- package/lib/module/ui/styles/authStyles.js.map +1 -1
- package/lib/module/utils/asyncUtils.js +10 -22
- package/lib/module/utils/asyncUtils.js.map +1 -1
- package/lib/module/utils/cache.js +250 -0
- package/lib/module/utils/cache.js.map +1 -0
- package/lib/module/utils/index.js +7 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/languageUtils.js +151 -0
- package/lib/module/utils/languageUtils.js.map +1 -0
- package/lib/module/utils/requestUtils.js +210 -0
- package/lib/module/utils/requestUtils.js.map +1 -0
- package/lib/module/utils/sessionUtils.js +180 -0
- package/lib/module/utils/sessionUtils.js.map +1 -0
- package/lib/typescript/core/HttpClient.d.ts +64 -0
- package/lib/typescript/core/HttpClient.d.ts.map +1 -0
- package/lib/typescript/core/OxyServices.d.ts +82 -71
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/core/RequestManager.d.ts +67 -0
- package/lib/typescript/core/RequestManager.d.ts.map +1 -0
- package/lib/typescript/core/index.d.ts +2 -0
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +15 -0
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/models/session.d.ts +1 -0
- package/lib/typescript/models/session.d.ts.map +1 -1
- package/lib/typescript/ui/components/Avatar.d.ts +6 -7
- package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
- package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/ui/index.d.ts +2 -2
- package/lib/typescript/ui/index.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts +3 -3
- package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
- package/lib/typescript/ui/stores/accountStore.d.ts +34 -0
- package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -0
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/ui/styles/authStyles.d.ts +18 -2
- package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
- package/lib/typescript/utils/asyncUtils.d.ts +2 -0
- package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
- package/lib/typescript/utils/cache.d.ts +128 -0
- package/lib/typescript/utils/cache.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +4 -0
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/lib/typescript/utils/languageUtils.d.ts +38 -0
- package/lib/typescript/utils/languageUtils.d.ts.map +1 -0
- package/lib/typescript/utils/requestUtils.d.ts +122 -0
- package/lib/typescript/utils/requestUtils.d.ts.map +1 -0
- package/lib/typescript/utils/sessionUtils.d.ts +55 -0
- package/lib/typescript/utils/sessionUtils.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/HttpClient.ts +277 -0
- package/src/core/OxyServices.ts +458 -351
- package/src/core/RequestManager.ts +240 -0
- package/src/core/index.ts +10 -0
- package/src/index.ts +10 -0
- package/src/models/interfaces.ts +19 -0
- package/src/models/session.ts +1 -1
- package/src/ui/components/Avatar.tsx +151 -35
- package/src/ui/components/FollowButton.tsx +1 -0
- package/src/ui/components/internal/TextField.tsx +7 -6
- package/src/ui/context/OxyContext.tsx +213 -217
- package/src/ui/hooks/useSessionSocket.ts +72 -18
- package/src/ui/index.ts +4 -1
- package/src/ui/screens/AccountSettingsScreen.tsx +34 -2
- package/src/ui/screens/AccountSwitcherScreen.tsx +102 -68
- package/src/ui/screens/FileManagementScreen.tsx +1 -1
- package/src/ui/screens/LanguageSelectorScreen.tsx +86 -143
- package/src/ui/screens/SignInScreen.tsx +0 -7
- package/src/ui/screens/SignUpScreen.tsx +14 -15
- package/src/ui/screens/WelcomeNewUserScreen.tsx +52 -15
- package/src/ui/screens/internal/SignInPasswordStep.tsx +4 -6
- package/src/ui/screens/steps/SignInPasswordStep.tsx +4 -8
- package/src/ui/screens/steps/SignInUsernameStep.tsx +110 -256
- package/src/ui/stores/accountStore.ts +285 -0
- package/src/ui/stores/authStore.ts +2 -1
- package/src/ui/styles/authStyles.ts +14 -7
- package/src/utils/asyncUtils.ts +10 -24
- package/src/utils/cache.ts +264 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/languageUtils.ts +174 -0
- package/src/utils/requestUtils.ts +234 -0
- package/src/utils/sessionUtils.ts +206 -0
|
@@ -4,12 +4,15 @@ import type { UseFollowHook } from '../hooks/useFollow.types';
|
|
|
4
4
|
import { OxyServices } from '../../core';
|
|
5
5
|
import type { User, ApiError } from '../../models/interfaces';
|
|
6
6
|
import type { SessionLoginResponse, ClientSession, MinimalUserData } from '../../models/session';
|
|
7
|
+
import { normalizeAndSortSessions, mergeSessions, sessionsArraysEqual } from '../../utils/sessionUtils';
|
|
7
8
|
import { DeviceManager } from '../../utils/deviceManager';
|
|
8
9
|
import { useSessionSocket } from '../hooks/useSessionSocket';
|
|
9
10
|
import { toast } from '../../lib/sonner';
|
|
10
11
|
import { useAuthStore } from '../stores/authStore';
|
|
11
12
|
import type { BottomSheetController } from '../navigation/types';
|
|
12
13
|
import type { RouteName } from '../navigation/routes';
|
|
14
|
+
import { getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode } from '../../utils/languageUtils';
|
|
15
|
+
import type { LanguageMetadata } from '../../utils/languageUtils';
|
|
13
16
|
|
|
14
17
|
// Define the context shape
|
|
15
18
|
// NOTE: We intentionally avoid importing useFollow here to prevent a require cycle.
|
|
@@ -28,6 +31,9 @@ export interface OxyContextState {
|
|
|
28
31
|
|
|
29
32
|
// Language state
|
|
30
33
|
currentLanguage: string;
|
|
34
|
+
currentLanguageMetadata: LanguageMetadata | null; // Full language metadata (name, nativeName, etc.)
|
|
35
|
+
currentLanguageName: string; // Language name (e.g., 'English')
|
|
36
|
+
currentNativeLanguageName: string; // Native language name (e.g., 'Español')
|
|
31
37
|
|
|
32
38
|
// Auth methods
|
|
33
39
|
login: (username: string, password: string, deviceName?: string) => Promise<User>;
|
|
@@ -154,6 +160,7 @@ const getStorage = async (): Promise<StorageInterface> => {
|
|
|
154
160
|
// Storage keys for sessions
|
|
155
161
|
const getStorageKeys = (prefix = 'oxy_session') => ({
|
|
156
162
|
activeSessionId: `${prefix}_active_session_id`, // Only store the active session ID
|
|
163
|
+
sessionIds: `${prefix}_session_ids`, // Store all session IDs for quick account loading
|
|
157
164
|
language: `${prefix}_language`, // Store the selected language
|
|
158
165
|
});
|
|
159
166
|
|
|
@@ -194,30 +201,23 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
194
201
|
const [minimalUser, setMinimalUser] = useState<MinimalUserData | null>(null);
|
|
195
202
|
const [sessions, setSessions] = useState<ClientSession[]>([]);
|
|
196
203
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
204
|
+
|
|
205
|
+
// Track in-flight refresh to prevent duplicate calls
|
|
206
|
+
const refreshInFlightRef = useRef<Promise<void> | null>(null);
|
|
207
|
+
|
|
197
208
|
const [storage, setStorage] = useState<StorageInterface | null>(null);
|
|
198
209
|
const [currentLanguage, setCurrentLanguage] = useState<string>('en-US');
|
|
199
210
|
|
|
200
211
|
// Storage keys (memoized to prevent infinite loops) - declared early for use in helpers
|
|
201
212
|
const keys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
|
|
202
213
|
|
|
203
|
-
// Normalize language codes to BCP-47 (e.g., en-US)
|
|
204
|
-
const normalizeLanguageCode = useCallback((lang?: string | null): string | null => {
|
|
205
|
-
if (!lang) return null;
|
|
206
|
-
if (lang.includes('-')) return lang;
|
|
207
|
-
const map: Record<string, string> = {
|
|
208
|
-
en: 'en-US', es: 'es-ES', ca: 'ca-ES', fr: 'fr-FR', de: 'de-DE', it: 'it-IT', pt: 'pt-PT',
|
|
209
|
-
ja: 'ja-JP', ko: 'ko-KR', zh: 'zh-CN', ar: 'ar-SA'
|
|
210
|
-
};
|
|
211
|
-
return map[lang] || lang;
|
|
212
|
-
}, []);
|
|
213
|
-
|
|
214
214
|
// Helper to apply language preference from user/server
|
|
215
215
|
const applyLanguagePreference = useCallback(async (user: User): Promise<void> => {
|
|
216
216
|
const userLanguage = (user as Record<string, unknown>)?.language as string | undefined;
|
|
217
217
|
if (!userLanguage || !storage) return;
|
|
218
218
|
|
|
219
219
|
try {
|
|
220
|
-
const serverLang = normalizeLanguageCode(userLanguage)
|
|
220
|
+
const serverLang = normalizeLanguageCode(userLanguage);
|
|
221
221
|
await storage.setItem(keys.language, serverLang);
|
|
222
222
|
setCurrentLanguage(serverLang);
|
|
223
223
|
} catch (e) {
|
|
@@ -225,25 +225,58 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
225
225
|
console.warn('Failed to apply server language preference', e);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
}, [storage, keys.language
|
|
228
|
+
}, [storage, keys.language]);
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
const mapServerSessionsToClient = useCallback((serverSessions: Array<{
|
|
230
|
+
const mapSessionsToClient = useCallback((sessions: Array<{
|
|
232
231
|
sessionId: string;
|
|
233
|
-
deviceId
|
|
232
|
+
deviceId?: string;
|
|
234
233
|
expiresAt?: string;
|
|
235
234
|
lastActive?: string;
|
|
235
|
+
user?: { id?: string; _id?: { toString(): string } };
|
|
236
236
|
userId?: string;
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
isCurrent?: boolean;
|
|
238
|
+
}>, fallbackDeviceId?: string, fallbackUserId?: string): ClientSession[] => {
|
|
239
|
+
return sessions.map((s: any) => ({
|
|
239
240
|
sessionId: s.sessionId,
|
|
240
|
-
deviceId: s.deviceId,
|
|
241
|
-
expiresAt: s.expiresAt || new Date().toISOString(),
|
|
241
|
+
deviceId: s.deviceId || fallbackDeviceId || '',
|
|
242
|
+
expiresAt: s.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
242
243
|
lastActive: s.lastActive || new Date().toISOString(),
|
|
243
|
-
userId: s.userId || fallbackUserId
|
|
244
|
+
userId: s.user?.id || s.userId || (s.user?._id?.toString()) || fallbackUserId || '',
|
|
245
|
+
isCurrent: Boolean(s.isCurrent),
|
|
244
246
|
}));
|
|
245
247
|
}, []);
|
|
246
248
|
|
|
249
|
+
// Save all session IDs to storage for quick loading on initialization
|
|
250
|
+
const saveSessionIds = useCallback(async (sessionIds: string[]): Promise<void> => {
|
|
251
|
+
if (!storage) return;
|
|
252
|
+
try {
|
|
253
|
+
const uniqueIds = Array.from(new Set(sessionIds));
|
|
254
|
+
await storage.setItem(keys.sessionIds, JSON.stringify(uniqueIds));
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (__DEV__) {
|
|
257
|
+
console.warn('Failed to save session IDs:', err);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}, [storage, keys.sessionIds]);
|
|
261
|
+
|
|
262
|
+
const updateSessions = useCallback((newSessions: ClientSession[], mergeWithExisting = false) => {
|
|
263
|
+
setSessions((prevSessions) => {
|
|
264
|
+
const sessionsToProcess = mergeWithExisting
|
|
265
|
+
? mergeSessions(prevSessions, newSessions, activeSessionId, false)
|
|
266
|
+
: normalizeAndSortSessions(newSessions, activeSessionId, false);
|
|
267
|
+
|
|
268
|
+
// Save all session IDs to storage
|
|
269
|
+
if (storage) {
|
|
270
|
+
const allSessionIds = sessionsToProcess.map(s => s.sessionId);
|
|
271
|
+
saveSessionIds(allSessionIds).catch(() => {
|
|
272
|
+
// Ignore errors - non-critical
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return sessionsArraysEqual(prevSessions, sessionsToProcess) ? prevSessions : sessionsToProcess;
|
|
277
|
+
});
|
|
278
|
+
}, [activeSessionId, storage, saveSessionIds]);
|
|
279
|
+
|
|
247
280
|
// Token ready state - start optimistically so children render immediately
|
|
248
281
|
const [tokenReady, setTokenReady] = useState(true);
|
|
249
282
|
|
|
@@ -252,6 +285,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
252
285
|
if (!storage) return;
|
|
253
286
|
try {
|
|
254
287
|
await storage.removeItem(keys.activeSessionId);
|
|
288
|
+
await storage.removeItem(keys.sessionIds);
|
|
255
289
|
} catch (err) {
|
|
256
290
|
if (__DEV__) {
|
|
257
291
|
console.error('Clear storage error:', err);
|
|
@@ -290,8 +324,49 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
290
324
|
setCurrentLanguage(savedLanguage);
|
|
291
325
|
}
|
|
292
326
|
|
|
327
|
+
// Load all stored session IDs and validate them
|
|
328
|
+
const storedSessionIdsJson = await storage.getItem(keys.sessionIds);
|
|
329
|
+
const storedSessionIds: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
|
|
330
|
+
|
|
293
331
|
// Try to restore active session from storage
|
|
294
332
|
const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
|
|
333
|
+
const validSessions: ClientSession[] = [];
|
|
334
|
+
|
|
335
|
+
// If we have stored session IDs, validate them (even without active session)
|
|
336
|
+
if (storedSessionIds.length > 0) {
|
|
337
|
+
if (__DEV__) {
|
|
338
|
+
console.log('Loading stored sessions on init:', storedSessionIds.length);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Validate each stored session ID and build session list
|
|
342
|
+
for (const sessionId of storedSessionIds) {
|
|
343
|
+
try {
|
|
344
|
+
const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
|
|
345
|
+
if (validation.valid && validation.user) {
|
|
346
|
+
validSessions.push({
|
|
347
|
+
sessionId,
|
|
348
|
+
userId: validation.user.id?.toString() || '',
|
|
349
|
+
deviceId: '',
|
|
350
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
351
|
+
lastActive: new Date().toISOString(),
|
|
352
|
+
isCurrent: sessionId === storedActiveSessionId,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {
|
|
356
|
+
// Session invalid, skip it
|
|
357
|
+
if (__DEV__) {
|
|
358
|
+
console.warn('Session validation failed for:', sessionId, e);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Update sessions list with validated sessions (even if no active session)
|
|
364
|
+
if (validSessions.length > 0) {
|
|
365
|
+
updateSessions(validSessions, false);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// If we have an active session, authenticate with it
|
|
295
370
|
if (storedActiveSessionId) {
|
|
296
371
|
try {
|
|
297
372
|
const validation = await oxyServices.validateSession(storedActiveSessionId, { useHeaderValidation: true });
|
|
@@ -304,34 +379,31 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
304
379
|
|
|
305
380
|
await applyLanguagePreference(fullUser);
|
|
306
381
|
|
|
307
|
-
// Get all device sessions to support multiple accounts
|
|
308
382
|
try {
|
|
309
383
|
const deviceSessions = await oxyServices.getDeviceSessions(storedActiveSessionId);
|
|
310
|
-
const allDeviceSessions = deviceSessions.
|
|
311
|
-
|
|
312
|
-
deviceId: ds.deviceId,
|
|
313
|
-
expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
314
|
-
lastActive: ds.lastActive || new Date().toISOString(),
|
|
315
|
-
userId: ds.user?.id || ds.userId || fullUser.id,
|
|
316
|
-
}));
|
|
317
|
-
setSessions(allDeviceSessions);
|
|
384
|
+
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
|
|
385
|
+
updateSessions(allDeviceSessions, true);
|
|
318
386
|
} catch (e) {
|
|
319
|
-
// Fallback to user sessions
|
|
320
387
|
if (__DEV__) {
|
|
321
388
|
console.warn('Failed to get device sessions on init, falling back to user sessions:', e);
|
|
322
389
|
}
|
|
323
|
-
|
|
324
|
-
|
|
390
|
+
const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
|
|
391
|
+
updateSessions(mapSessionsToClient(serverSessions, undefined, fullUser.id), false);
|
|
325
392
|
}
|
|
326
393
|
onAuthStateChange?.(fullUser);
|
|
327
394
|
} else {
|
|
328
|
-
|
|
395
|
+
// Active session invalid, remove it but keep other sessions
|
|
396
|
+
await storage.removeItem(keys.activeSessionId);
|
|
397
|
+
// Update session list to remove invalid active session
|
|
398
|
+
updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
|
|
329
399
|
}
|
|
330
400
|
} catch (e) {
|
|
331
401
|
if (__DEV__) {
|
|
332
|
-
console.error('
|
|
402
|
+
console.error('Active session validation error', e);
|
|
333
403
|
}
|
|
334
|
-
|
|
404
|
+
// Remove invalid active session but keep other sessions
|
|
405
|
+
await storage.removeItem(keys.activeSessionId);
|
|
406
|
+
updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
|
|
335
407
|
}
|
|
336
408
|
}
|
|
337
409
|
setTokenReady(true);
|
|
@@ -344,7 +416,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
344
416
|
}
|
|
345
417
|
};
|
|
346
418
|
initAuth();
|
|
347
|
-
}, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference,
|
|
419
|
+
}, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference, mapSessionsToClient, updateSessions]);
|
|
348
420
|
|
|
349
421
|
// Save active session ID to storage (only session ID, no user data)
|
|
350
422
|
const saveActiveSessionId = useCallback(async (sessionId: string): Promise<void> => {
|
|
@@ -352,25 +424,22 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
352
424
|
await storage.setItem(keys.activeSessionId, sessionId);
|
|
353
425
|
}, [storage, keys.activeSessionId]);
|
|
354
426
|
|
|
355
|
-
// Switch to a different session
|
|
356
427
|
const switchToSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
357
428
|
try {
|
|
358
|
-
// Don't set isLoading - session switches should happen silently in background
|
|
359
|
-
// Validate session first before attempting to switch
|
|
360
429
|
const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
|
|
361
430
|
if (!validation.valid) {
|
|
362
|
-
|
|
363
|
-
setSessions((prevSessions) => prevSessions.filter(s => s.sessionId !== sessionId));
|
|
431
|
+
updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
|
|
364
432
|
throw new Error('Session is invalid or expired');
|
|
365
433
|
}
|
|
366
434
|
|
|
367
|
-
|
|
435
|
+
if (!validation.user) {
|
|
436
|
+
throw new Error('User data not available from session validation');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const fullUser = validation.user;
|
|
368
440
|
await oxyServices.getTokenBySession(sessionId);
|
|
369
441
|
setTokenReady(true);
|
|
370
442
|
|
|
371
|
-
// Load full user data - use user from validation if available, otherwise fetch
|
|
372
|
-
const fullUser = validation.user || await oxyServices.getUserBySession(sessionId);
|
|
373
|
-
|
|
374
443
|
setActiveSessionId(sessionId);
|
|
375
444
|
loginSuccess(fullUser);
|
|
376
445
|
setMinimalUser({
|
|
@@ -381,57 +450,25 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
381
450
|
|
|
382
451
|
await saveActiveSessionId(sessionId);
|
|
383
452
|
await applyLanguagePreference(fullUser);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
393
|
-
lastActive: ds.lastActive || new Date().toISOString(),
|
|
394
|
-
userId: ds.user?.id || ds.userId || fullUser.id,
|
|
395
|
-
}));
|
|
396
|
-
// Merge with existing sessions to preserve other accounts
|
|
397
|
-
setSessions((prevSessions) => {
|
|
398
|
-
const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
|
|
399
|
-
const newSessions = allDeviceSessions.filter(s => !existingSessionIds.has(s.sessionId));
|
|
400
|
-
// Combine existing sessions with new ones, prioritizing new data for existing sessions
|
|
401
|
-
const sessionMap = new Map(prevSessions.map(s => [s.sessionId, s]));
|
|
402
|
-
allDeviceSessions.forEach(s => sessionMap.set(s.sessionId, s));
|
|
403
|
-
return Array.from(sessionMap.values());
|
|
404
|
-
});
|
|
405
|
-
} catch (error) {
|
|
406
|
-
// Fallback to user sessions - merge with existing to preserve other accounts
|
|
407
|
-
if (__DEV__) {
|
|
408
|
-
console.warn('Failed to get device sessions after switch, falling back to user sessions:', error);
|
|
409
|
-
}
|
|
410
|
-
const serverSessions = await oxyServices.getSessionsBySessionId(sessionId);
|
|
411
|
-
const userSessions = mapServerSessionsToClient(serverSessions, fullUser.id);
|
|
412
|
-
// Merge with existing sessions to preserve other accounts
|
|
413
|
-
setSessions((prevSessions) => {
|
|
414
|
-
const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
|
|
415
|
-
const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
|
|
416
|
-
// Combine existing sessions with new ones, prioritizing new data for existing sessions
|
|
417
|
-
const sessionMap = new Map(prevSessions.map(s => [s.sessionId, s]));
|
|
418
|
-
userSessions.forEach(s => sessionMap.set(s.sessionId, s));
|
|
419
|
-
return Array.from(sessionMap.values());
|
|
453
|
+
|
|
454
|
+
oxyServices.getDeviceSessions(sessionId)
|
|
455
|
+
.then((deviceSessions) => {
|
|
456
|
+
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
|
|
457
|
+
updateSessions(allDeviceSessions, true);
|
|
458
|
+
})
|
|
459
|
+
.catch((error) => {
|
|
460
|
+
if (__DEV__) console.warn('Failed to get device sessions after switch:', error);
|
|
420
461
|
});
|
|
421
|
-
|
|
422
|
-
|
|
462
|
+
|
|
423
463
|
onAuthStateChange?.(fullUser);
|
|
424
464
|
} catch (error: any) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
465
|
+
const isInvalidSession = error?.response?.status === 401 ||
|
|
466
|
+
error?.message?.includes('Invalid or expired session') ||
|
|
467
|
+
error?.message?.includes('Session is invalid');
|
|
468
|
+
|
|
430
469
|
if (isInvalidSession) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// If this was the active session, try to switch to another valid session
|
|
470
|
+
updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
|
|
471
|
+
|
|
435
472
|
if (sessionId === activeSessionId && sessions.length > 1) {
|
|
436
473
|
const otherSessions = sessions.filter(s => s.sessionId !== sessionId);
|
|
437
474
|
for (const otherSession of otherSessions) {
|
|
@@ -448,7 +485,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
448
485
|
}
|
|
449
486
|
}
|
|
450
487
|
}
|
|
451
|
-
|
|
488
|
+
|
|
452
489
|
const errorMessage = error instanceof Error ? error.message : 'Failed to switch session';
|
|
453
490
|
if (__DEV__) {
|
|
454
491
|
console.error('Switch session error:', error);
|
|
@@ -458,9 +495,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
458
495
|
setTokenReady(false);
|
|
459
496
|
throw error; // Re-throw so calling code can handle it
|
|
460
497
|
}
|
|
461
|
-
}, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference,
|
|
498
|
+
}, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference, mapSessionsToClient, onError, activeSessionId, sessions]);
|
|
462
499
|
|
|
463
|
-
// Login method - only store session ID, retrieve data from backend
|
|
464
500
|
const login = useCallback(async (username: string, password: string, deviceName?: string): Promise<User> => {
|
|
465
501
|
if (!storage) throw new Error('Storage not initialized');
|
|
466
502
|
useAuthStore.setState({ isLoading: true, error: null });
|
|
@@ -494,45 +530,24 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
494
530
|
await oxyServices.getTokenBySession(sessionResponse.sessionId);
|
|
495
531
|
const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
496
532
|
|
|
497
|
-
// Get all device sessions to check for duplicates BEFORE setting the new session as active
|
|
498
|
-
// This returns all sessions on the device, not just for the current user
|
|
499
533
|
let allDeviceSessions: ClientSession[] = [];
|
|
500
534
|
try {
|
|
501
535
|
const deviceSessions = await oxyServices.getDeviceSessions(sessionResponse.sessionId);
|
|
502
|
-
|
|
503
|
-
// Map device sessions to client format
|
|
504
|
-
// Device sessions include user info, so we can map them directly
|
|
505
|
-
allDeviceSessions = deviceSessions.map((ds: any) => ({
|
|
506
|
-
sessionId: ds.sessionId,
|
|
507
|
-
deviceId: ds.deviceId || sessionResponse.deviceId,
|
|
508
|
-
expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
509
|
-
lastActive: ds.lastActive || new Date().toISOString(),
|
|
510
|
-
userId: ds.user?.id || ds.userId || (ds.user?._id?.toString()) || fullUser.id,
|
|
511
|
-
}));
|
|
536
|
+
allDeviceSessions = mapSessionsToClient(deviceSessions, sessionResponse.deviceId, fullUser.id);
|
|
512
537
|
} catch (error) {
|
|
513
|
-
// Fallback to user sessions if device sessions fail
|
|
514
538
|
if (__DEV__) {
|
|
515
539
|
console.warn('Failed to get device sessions, falling back to user sessions:', error);
|
|
516
540
|
}
|
|
517
541
|
const serverSessions = await oxyServices.getSessionsBySessionId(sessionResponse.sessionId);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// Merge with existing sessions to preserve other accounts
|
|
521
|
-
const existingSessionIds = new Set((sessions || []).map(s => s.sessionId));
|
|
522
|
-
const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
|
|
523
|
-
allDeviceSessions = [...(sessions || []), ...newSessions];
|
|
542
|
+
allDeviceSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
|
|
524
543
|
}
|
|
525
544
|
|
|
526
|
-
// Check if this user is already signed in with another session on this device
|
|
527
|
-
// Compare userId as string to handle both string and ObjectId formats
|
|
528
545
|
const userUserId = fullUser.id?.toString();
|
|
529
546
|
const existingSession = allDeviceSessions.find(
|
|
530
547
|
s => s.userId?.toString() === userUserId && s.sessionId !== sessionResponse.sessionId
|
|
531
548
|
);
|
|
532
|
-
|
|
549
|
+
|
|
533
550
|
if (existingSession) {
|
|
534
|
-
// User is already signed in on this device, switch to existing session instead
|
|
535
|
-
// Logout the newly created session to clean it up
|
|
536
551
|
try {
|
|
537
552
|
await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
|
|
538
553
|
} catch (logoutError) {
|
|
@@ -540,27 +555,19 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
540
555
|
console.warn('Failed to logout duplicate session:', logoutError);
|
|
541
556
|
}
|
|
542
557
|
}
|
|
543
|
-
|
|
544
|
-
// Switch to the existing session
|
|
545
558
|
await switchToSession(existingSession.sessionId);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// Update sessions list (excluding the duplicate we just created)
|
|
550
|
-
setSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId));
|
|
551
|
-
|
|
559
|
+
loginSuccess(fullUser);
|
|
560
|
+
setMinimalUser(sessionResponse.user);
|
|
561
|
+
updateSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId), false);
|
|
552
562
|
onAuthStateChange?.(fullUser);
|
|
553
563
|
return fullUser;
|
|
554
564
|
}
|
|
555
565
|
|
|
556
|
-
// No duplicate found, proceed with the new session
|
|
557
566
|
setActiveSessionId(sessionResponse.sessionId);
|
|
558
567
|
await saveActiveSessionId(sessionResponse.sessionId);
|
|
559
|
-
|
|
560
568
|
loginSuccess(fullUser);
|
|
561
569
|
setMinimalUser(sessionResponse.user);
|
|
562
|
-
|
|
563
|
-
setSessions(allDeviceSessions);
|
|
570
|
+
updateSessions(allDeviceSessions, true);
|
|
564
571
|
|
|
565
572
|
onAuthStateChange?.(fullUser);
|
|
566
573
|
return fullUser;
|
|
@@ -572,7 +579,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
572
579
|
} finally {
|
|
573
580
|
useAuthStore.setState({ isLoading: false });
|
|
574
581
|
}
|
|
575
|
-
}, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure,
|
|
582
|
+
}, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure, mapSessionsToClient, onError, sessions, switchToSession]);
|
|
576
583
|
|
|
577
584
|
// Logout method
|
|
578
585
|
const logout = useCallback(async (targetSessionId?: string): Promise<void> => {
|
|
@@ -582,17 +589,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
582
589
|
const sessionToLogout = targetSessionId || activeSessionId;
|
|
583
590
|
await oxyServices.logoutSession(activeSessionId, sessionToLogout);
|
|
584
591
|
|
|
585
|
-
// Remove session from local state
|
|
586
592
|
const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
|
|
587
|
-
|
|
593
|
+
updateSessions(filteredSessions, false);
|
|
588
594
|
|
|
589
|
-
// If logging out active session
|
|
590
595
|
if (sessionToLogout === activeSessionId) {
|
|
591
596
|
if (filteredSessions.length > 0) {
|
|
592
|
-
// Switch to another session
|
|
593
597
|
await switchToSession(filteredSessions[0].sessionId);
|
|
594
598
|
} else {
|
|
595
|
-
// No sessions left
|
|
596
599
|
setActiveSessionId(null);
|
|
597
600
|
logoutStore();
|
|
598
601
|
setMinimalUser(null);
|
|
@@ -613,7 +616,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
613
616
|
}
|
|
614
617
|
}, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, storage, keys.activeSessionId, onAuthStateChange, onError]);
|
|
615
618
|
|
|
616
|
-
// Logout all sessions
|
|
617
619
|
const logoutAll = useCallback(async (): Promise<void> => {
|
|
618
620
|
if (!activeSessionId) {
|
|
619
621
|
const error = new Error('No active session found');
|
|
@@ -625,7 +627,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
625
627
|
try {
|
|
626
628
|
await oxyServices.logoutAllSessions(activeSessionId);
|
|
627
629
|
|
|
628
|
-
|
|
630
|
+
updateSessions([], false);
|
|
629
631
|
setActiveSessionId(null);
|
|
630
632
|
logoutStore();
|
|
631
633
|
setMinimalUser(null);
|
|
@@ -684,28 +686,17 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
684
686
|
// Get all device sessions to support multiple accounts
|
|
685
687
|
try {
|
|
686
688
|
const deviceSessions = await oxyServices.getDeviceSessions(response.sessionId);
|
|
687
|
-
const allDeviceSessions = deviceSessions.
|
|
688
|
-
|
|
689
|
-
deviceId: ds.deviceId,
|
|
690
|
-
expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
691
|
-
lastActive: ds.lastActive || new Date().toISOString(),
|
|
692
|
-
userId: ds.user?.id || ds.userId || fullUser.id,
|
|
693
|
-
}));
|
|
694
|
-
setSessions(allDeviceSessions);
|
|
689
|
+
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
|
|
690
|
+
updateSessions(allDeviceSessions, true);
|
|
695
691
|
} catch (error) {
|
|
696
692
|
// Fallback to user sessions if device sessions fail
|
|
697
693
|
if (__DEV__) {
|
|
698
694
|
console.warn('Failed to get device sessions for MFA, falling back to user sessions:', error);
|
|
699
695
|
}
|
|
700
|
-
|
|
701
|
-
const userSessions =
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
setSessions((prevSessions) => {
|
|
705
|
-
const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
|
|
706
|
-
const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
|
|
707
|
-
return [...prevSessions, ...newSessions];
|
|
708
|
-
});
|
|
696
|
+
const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
|
|
697
|
+
const userSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
|
|
698
|
+
|
|
699
|
+
updateSessions(userSessions, true);
|
|
709
700
|
}
|
|
710
701
|
|
|
711
702
|
onAuthStateChange?.(fullUser);
|
|
@@ -720,83 +711,77 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
720
711
|
}
|
|
721
712
|
}, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange, applyLanguagePreference, onError]);
|
|
722
713
|
|
|
723
|
-
// Switch session method (wrapper for consistency)
|
|
724
714
|
const switchSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
725
715
|
await switchToSession(sessionId);
|
|
726
716
|
}, [switchToSession]);
|
|
727
717
|
|
|
728
|
-
// Remove session method (wrapper for consistency)
|
|
729
718
|
const removeSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
730
719
|
await logout(sessionId);
|
|
731
720
|
}, [logout]);
|
|
732
721
|
|
|
733
|
-
// Refresh sessions method
|
|
734
722
|
const refreshSessions = useCallback(async (): Promise<void> => {
|
|
735
723
|
if (!activeSessionId) return;
|
|
736
724
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
sessionId: ds.sessionId,
|
|
742
|
-
deviceId: ds.deviceId,
|
|
743
|
-
expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
744
|
-
lastActive: ds.lastActive || new Date().toISOString(),
|
|
745
|
-
userId: ds.user?.id || ds.userId || user?.id,
|
|
746
|
-
}));
|
|
747
|
-
setSessions(allDeviceSessions);
|
|
748
|
-
} catch (error) {
|
|
749
|
-
// Fallback to user sessions if device sessions fail
|
|
750
|
-
// Merge with existing sessions to preserve other accounts
|
|
751
|
-
if (__DEV__) {
|
|
752
|
-
console.warn('Failed to refresh device sessions, falling back to user sessions:', error);
|
|
753
|
-
}
|
|
754
|
-
try {
|
|
755
|
-
const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
|
|
756
|
-
const userSessions = mapServerSessionsToClient(serverSessions, user?.id);
|
|
757
|
-
// Merge with existing sessions to preserve other accounts
|
|
758
|
-
setSessions((prevSessions) => {
|
|
759
|
-
const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
|
|
760
|
-
const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
|
|
761
|
-
// Combine existing sessions with new ones, prioritizing new data for existing sessions
|
|
762
|
-
const sessionMap = new Map(prevSessions.map(s => [s.sessionId, s]));
|
|
763
|
-
userSessions.forEach(s => sessionMap.set(s.sessionId, s));
|
|
764
|
-
return Array.from(sessionMap.values());
|
|
765
|
-
});
|
|
766
|
-
} catch (fallbackError) {
|
|
767
|
-
if (__DEV__) {
|
|
768
|
-
console.error('Refresh sessions error:', fallbackError);
|
|
769
|
-
}
|
|
725
|
+
// If a refresh is already in progress, return the existing promise
|
|
726
|
+
if (refreshInFlightRef.current) {
|
|
727
|
+
return refreshInFlightRef.current;
|
|
728
|
+
}
|
|
770
729
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
730
|
+
// Create the refresh promise
|
|
731
|
+
const refreshPromise = (async () => {
|
|
732
|
+
try {
|
|
733
|
+
const deviceSessions = await oxyServices.getDeviceSessions(activeSessionId);
|
|
734
|
+
const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, user?.id);
|
|
735
|
+
updateSessions(allDeviceSessions, true);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (__DEV__) {
|
|
738
|
+
console.warn('Failed to refresh device sessions, falling back to user sessions:', error);
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
|
|
742
|
+
const userSessions = mapSessionsToClient(serverSessions, undefined, user?.id);
|
|
743
|
+
updateSessions(userSessions, true);
|
|
744
|
+
} catch (fallbackError) {
|
|
745
|
+
if (__DEV__) {
|
|
746
|
+
console.error('Refresh sessions error:', fallbackError);
|
|
747
|
+
}
|
|
774
748
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
749
|
+
// If the current session is invalid, try to find another valid session
|
|
750
|
+
if (sessions.length > 1) {
|
|
751
|
+
const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
|
|
752
|
+
|
|
753
|
+
for (const session of otherSessions) {
|
|
754
|
+
try {
|
|
755
|
+
const validation = await oxyServices.validateSession(session.sessionId, {
|
|
756
|
+
useHeaderValidation: true
|
|
757
|
+
});
|
|
758
|
+
if (validation.valid) {
|
|
759
|
+
await switchToSession(session.sessionId);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
} catch {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
783
765
|
}
|
|
784
|
-
} catch {
|
|
785
|
-
continue;
|
|
786
766
|
}
|
|
767
|
+
|
|
768
|
+
// No valid sessions found, clear all
|
|
769
|
+
updateSessions([], false);
|
|
770
|
+
setActiveSessionId(null);
|
|
771
|
+
logoutStore();
|
|
772
|
+
setMinimalUser(null);
|
|
773
|
+
await clearAllStorage();
|
|
774
|
+
onAuthStateChange?.(null);
|
|
787
775
|
}
|
|
776
|
+
} finally {
|
|
777
|
+
// Clear the in-flight ref when done
|
|
778
|
+
refreshInFlightRef.current = null;
|
|
788
779
|
}
|
|
780
|
+
})();
|
|
789
781
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
logoutStore();
|
|
794
|
-
setMinimalUser(null);
|
|
795
|
-
await clearAllStorage();
|
|
796
|
-
onAuthStateChange?.(null);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}, [activeSessionId, oxyServices, user?.id, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapServerSessionsToClient]);
|
|
782
|
+
refreshInFlightRef.current = refreshPromise;
|
|
783
|
+
return refreshPromise;
|
|
784
|
+
}, [activeSessionId, oxyServices, user?.id, updateSessions, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapSessionsToClient]);
|
|
800
785
|
|
|
801
786
|
// Device management methods
|
|
802
787
|
const getDeviceSessions = useCallback(async (): Promise<Array<{
|
|
@@ -821,7 +806,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
821
806
|
|
|
822
807
|
try {
|
|
823
808
|
await oxyServices.logoutAllDeviceSessions(activeSessionId);
|
|
824
|
-
|
|
809
|
+
updateSessions([], false);
|
|
825
810
|
setActiveSessionId(null);
|
|
826
811
|
logoutStore();
|
|
827
812
|
setMinimalUser(null);
|
|
@@ -941,6 +926,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
941
926
|
}
|
|
942
927
|
};
|
|
943
928
|
|
|
929
|
+
// Compute language metadata from currentLanguage
|
|
930
|
+
const languageMetadata = useMemo(() => getLanguageMetadata(currentLanguage), [currentLanguage]);
|
|
931
|
+
const languageName = useMemo(() => getLanguageName(currentLanguage), [currentLanguage]);
|
|
932
|
+
const nativeLanguageName = useMemo(() => getNativeLanguageName(currentLanguage), [currentLanguage]);
|
|
933
|
+
|
|
944
934
|
const contextValue: OxyContextState = useMemo(() => ({
|
|
945
935
|
user,
|
|
946
936
|
minimalUser,
|
|
@@ -951,6 +941,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
951
941
|
isTokenReady: tokenReady,
|
|
952
942
|
error,
|
|
953
943
|
currentLanguage,
|
|
944
|
+
currentLanguageMetadata: languageMetadata,
|
|
945
|
+
currentLanguageName: languageName,
|
|
946
|
+
currentNativeLanguageName: nativeLanguageName,
|
|
954
947
|
login,
|
|
955
948
|
logout,
|
|
956
949
|
logoutAll,
|
|
@@ -978,6 +971,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
978
971
|
tokenReady,
|
|
979
972
|
error,
|
|
980
973
|
currentLanguage,
|
|
974
|
+
languageMetadata,
|
|
975
|
+
languageName,
|
|
976
|
+
nativeLanguageName,
|
|
981
977
|
login,
|
|
982
978
|
logout,
|
|
983
979
|
logoutAll,
|