@oxyhq/services 5.13.1 → 5.13.3

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.
Files changed (204) hide show
  1. package/README.md +71 -0
  2. package/lib/commonjs/core/HttpClient.js +238 -0
  3. package/lib/commonjs/core/HttpClient.js.map +1 -0
  4. package/lib/commonjs/core/OxyServices.js +538 -332
  5. package/lib/commonjs/core/OxyServices.js.map +1 -1
  6. package/lib/commonjs/core/RequestManager.js +199 -0
  7. package/lib/commonjs/core/RequestManager.js.map +1 -0
  8. package/lib/commonjs/core/index.js +38 -1
  9. package/lib/commonjs/core/index.js.map +1 -1
  10. package/lib/commonjs/index.js +36 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/ui/components/Avatar.js +94 -27
  13. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  14. package/lib/commonjs/ui/components/FollowButton.js +1 -0
  15. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  16. package/lib/commonjs/ui/components/internal/TextField.js +13 -8
  17. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  18. package/lib/commonjs/ui/context/OxyContext.js +183 -224
  19. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/useSessionSocket.js +80 -22
  21. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  22. package/lib/commonjs/ui/index.js +4 -1
  23. package/lib/commonjs/ui/index.js.map +1 -1
  24. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +32 -2
  25. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  26. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +101 -59
  27. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -2
  29. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +75 -117
  31. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/SignInScreen.js +0 -11
  33. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  34. package/lib/commonjs/ui/screens/SignUpScreen.js +14 -16
  35. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +50 -18
  37. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  38. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +10 -10
  39. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  40. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +16 -26
  41. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  42. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +104 -212
  43. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  44. package/lib/commonjs/ui/stores/accountStore.js +237 -0
  45. package/lib/commonjs/ui/stores/accountStore.js.map +1 -0
  46. package/lib/commonjs/ui/stores/authStore.js +2 -1
  47. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  48. package/lib/commonjs/ui/styles/authStyles.js +14 -7
  49. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  50. package/lib/commonjs/utils/asyncUtils.js +9 -22
  51. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  52. package/lib/commonjs/utils/cache.js +259 -0
  53. package/lib/commonjs/utils/cache.js.map +1 -0
  54. package/lib/commonjs/utils/index.js +99 -0
  55. package/lib/commonjs/utils/index.js.map +1 -1
  56. package/lib/commonjs/utils/languageUtils.js +159 -0
  57. package/lib/commonjs/utils/languageUtils.js.map +1 -0
  58. package/lib/commonjs/utils/requestUtils.js +217 -0
  59. package/lib/commonjs/utils/requestUtils.js.map +1 -0
  60. package/lib/commonjs/utils/sessionUtils.js +191 -0
  61. package/lib/commonjs/utils/sessionUtils.js.map +1 -0
  62. package/lib/module/core/HttpClient.js +232 -0
  63. package/lib/module/core/HttpClient.js.map +1 -0
  64. package/lib/module/core/OxyServices.js +536 -326
  65. package/lib/module/core/OxyServices.js.map +1 -1
  66. package/lib/module/core/RequestManager.js +194 -0
  67. package/lib/module/core/RequestManager.js.map +1 -0
  68. package/lib/module/core/index.js +2 -0
  69. package/lib/module/core/index.js.map +1 -1
  70. package/lib/module/index.js +2 -0
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/ui/components/Avatar.js +94 -27
  73. package/lib/module/ui/components/Avatar.js.map +1 -1
  74. package/lib/module/ui/components/FollowButton.js +1 -0
  75. package/lib/module/ui/components/FollowButton.js.map +1 -1
  76. package/lib/module/ui/components/internal/TextField.js +13 -8
  77. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  78. package/lib/module/ui/context/OxyContext.js +182 -223
  79. package/lib/module/ui/context/OxyContext.js.map +1 -1
  80. package/lib/module/ui/hooks/useSessionSocket.js +80 -22
  81. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  82. package/lib/module/ui/index.js +4 -2
  83. package/lib/module/ui/index.js.map +1 -1
  84. package/lib/module/ui/screens/AccountSettingsScreen.js +33 -2
  85. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  86. package/lib/module/ui/screens/AccountSwitcherScreen.js +102 -60
  87. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  88. package/lib/module/ui/screens/FileManagementScreen.js +3 -2
  89. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  90. package/lib/module/ui/screens/LanguageSelectorScreen.js +73 -117
  91. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
  92. package/lib/module/ui/screens/SignInScreen.js +0 -11
  93. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  94. package/lib/module/ui/screens/SignUpScreen.js +14 -16
  95. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  96. package/lib/module/ui/screens/WelcomeNewUserScreen.js +50 -18
  97. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  98. package/lib/module/ui/screens/internal/SignInPasswordStep.js +10 -10
  99. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  100. package/lib/module/ui/screens/steps/SignInPasswordStep.js +16 -26
  101. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  102. package/lib/module/ui/screens/steps/SignInUsernameStep.js +105 -214
  103. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  104. package/lib/module/ui/stores/accountStore.js +229 -0
  105. package/lib/module/ui/stores/accountStore.js.map +1 -0
  106. package/lib/module/ui/stores/authStore.js +2 -1
  107. package/lib/module/ui/stores/authStore.js.map +1 -1
  108. package/lib/module/ui/styles/authStyles.js +14 -7
  109. package/lib/module/ui/styles/authStyles.js.map +1 -1
  110. package/lib/module/utils/asyncUtils.js +10 -22
  111. package/lib/module/utils/asyncUtils.js.map +1 -1
  112. package/lib/module/utils/cache.js +250 -0
  113. package/lib/module/utils/cache.js.map +1 -0
  114. package/lib/module/utils/index.js +7 -0
  115. package/lib/module/utils/index.js.map +1 -1
  116. package/lib/module/utils/languageUtils.js +151 -0
  117. package/lib/module/utils/languageUtils.js.map +1 -0
  118. package/lib/module/utils/requestUtils.js +210 -0
  119. package/lib/module/utils/requestUtils.js.map +1 -0
  120. package/lib/module/utils/sessionUtils.js +180 -0
  121. package/lib/module/utils/sessionUtils.js.map +1 -0
  122. package/lib/typescript/core/HttpClient.d.ts +64 -0
  123. package/lib/typescript/core/HttpClient.d.ts.map +1 -0
  124. package/lib/typescript/core/OxyServices.d.ts +88 -71
  125. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  126. package/lib/typescript/core/RequestManager.d.ts +67 -0
  127. package/lib/typescript/core/RequestManager.d.ts.map +1 -0
  128. package/lib/typescript/core/index.d.ts +2 -0
  129. package/lib/typescript/core/index.d.ts.map +1 -1
  130. package/lib/typescript/index.d.ts +2 -0
  131. package/lib/typescript/index.d.ts.map +1 -1
  132. package/lib/typescript/models/interfaces.d.ts +15 -0
  133. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  134. package/lib/typescript/models/session.d.ts +1 -0
  135. package/lib/typescript/models/session.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/Avatar.d.ts +6 -7
  137. package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  139. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  140. package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
  141. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  142. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  143. package/lib/typescript/ui/index.d.ts +2 -2
  144. package/lib/typescript/ui/index.d.ts.map +1 -1
  145. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  146. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  147. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts +3 -3
  148. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
  149. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  153. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  154. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  155. package/lib/typescript/ui/stores/accountStore.d.ts +34 -0
  156. package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -0
  157. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  158. package/lib/typescript/ui/styles/authStyles.d.ts +18 -2
  159. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  160. package/lib/typescript/utils/asyncUtils.d.ts +2 -0
  161. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  162. package/lib/typescript/utils/cache.d.ts +128 -0
  163. package/lib/typescript/utils/cache.d.ts.map +1 -0
  164. package/lib/typescript/utils/index.d.ts +4 -0
  165. package/lib/typescript/utils/index.d.ts.map +1 -1
  166. package/lib/typescript/utils/languageUtils.d.ts +38 -0
  167. package/lib/typescript/utils/languageUtils.d.ts.map +1 -0
  168. package/lib/typescript/utils/requestUtils.d.ts +122 -0
  169. package/lib/typescript/utils/requestUtils.d.ts.map +1 -0
  170. package/lib/typescript/utils/sessionUtils.d.ts +55 -0
  171. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -0
  172. package/package.json +1 -1
  173. package/src/core/HttpClient.ts +277 -0
  174. package/src/core/OxyServices.ts +466 -351
  175. package/src/core/RequestManager.ts +240 -0
  176. package/src/core/index.ts +10 -0
  177. package/src/index.ts +10 -0
  178. package/src/models/interfaces.ts +19 -0
  179. package/src/models/session.ts +1 -1
  180. package/src/ui/components/Avatar.tsx +151 -35
  181. package/src/ui/components/FollowButton.tsx +1 -0
  182. package/src/ui/components/internal/TextField.tsx +7 -6
  183. package/src/ui/context/OxyContext.tsx +213 -217
  184. package/src/ui/hooks/useSessionSocket.ts +72 -18
  185. package/src/ui/index.ts +4 -1
  186. package/src/ui/screens/AccountSettingsScreen.tsx +34 -2
  187. package/src/ui/screens/AccountSwitcherScreen.tsx +102 -68
  188. package/src/ui/screens/FileManagementScreen.tsx +1 -1
  189. package/src/ui/screens/LanguageSelectorScreen.tsx +86 -143
  190. package/src/ui/screens/SignInScreen.tsx +0 -7
  191. package/src/ui/screens/SignUpScreen.tsx +14 -15
  192. package/src/ui/screens/WelcomeNewUserScreen.tsx +52 -15
  193. package/src/ui/screens/internal/SignInPasswordStep.tsx +4 -6
  194. package/src/ui/screens/steps/SignInPasswordStep.tsx +4 -8
  195. package/src/ui/screens/steps/SignInUsernameStep.tsx +110 -256
  196. package/src/ui/stores/accountStore.ts +285 -0
  197. package/src/ui/stores/authStore.ts +2 -1
  198. package/src/ui/styles/authStyles.ts +14 -7
  199. package/src/utils/asyncUtils.ts +10 -24
  200. package/src/utils/cache.ts +264 -0
  201. package/src/utils/index.ts +19 -0
  202. package/src/utils/languageUtils.ts +174 -0
  203. package/src/utils/requestUtils.ts +234 -0
  204. 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) || 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, normalizeLanguageCode]);
228
+ }, [storage, keys.language]);
229
229
 
230
- // Helper to map server sessions to client sessions
231
- const mapServerSessionsToClient = useCallback((serverSessions: Array<{
230
+ const mapSessionsToClient = useCallback((sessions: Array<{
232
231
  sessionId: string;
233
- deviceId: string;
232
+ deviceId?: string;
234
233
  expiresAt?: string;
235
234
  lastActive?: string;
235
+ user?: { id?: string; _id?: { toString(): string } };
236
236
  userId?: string;
237
- }>, fallbackUserId?: string): ClientSession[] => {
238
- return serverSessions.map(s => ({
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.map((ds: any) => ({
311
- sessionId: ds.sessionId,
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
- const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
324
- setSessions(mapServerSessionsToClient(serverSessions, fullUser.id));
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
- await clearAllStorage();
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('Session validation error', e);
402
+ console.error('Active session validation error', e);
333
403
  }
334
- await clearAllStorage();
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, mapServerSessionsToClient]);
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
- // Session is invalid, remove it from the sessions list
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
- // Get access token for this session
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
- // Refresh all device sessions after switching
386
- // Preserve existing sessions from other users to avoid losing accounts
387
- try {
388
- const deviceSessions = await oxyServices.getDeviceSessions(sessionId);
389
- const allDeviceSessions = deviceSessions.map((ds: any) => ({
390
- sessionId: ds.sessionId,
391
- deviceId: ds.deviceId,
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
- // Check if the error is due to invalid/expired session
426
- const isInvalidSession = error?.response?.status === 401 ||
427
- error?.message?.includes('Invalid or expired session') ||
428
- error?.message?.includes('Session is invalid');
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
- // Remove invalid session from the sessions list
432
- setSessions((prevSessions) => prevSessions.filter(s => s.sessionId !== sessionId));
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, mapServerSessionsToClient, onError, activeSessionId, sessions]);
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
- const userSessions = mapServerSessionsToClient(serverSessions, fullUser.id);
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
- loginSuccess(fullUser);
547
- setMinimalUser(sessionResponse.user);
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, mapServerSessionsToClient, onError, sessions, switchToSession]);
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
- setSessions(filteredSessions);
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
- setSessions([]);
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.map((ds: any) => ({
688
- sessionId: ds.sessionId,
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
- const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
701
- const userSessions = mapServerSessionsToClient(serverSessions, fullUser.id);
702
-
703
- // Merge with existing sessions to preserve other accounts
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
- try {
738
- // Get all device sessions to support multiple accounts
739
- const deviceSessions = await oxyServices.getDeviceSessions(activeSessionId);
740
- const allDeviceSessions = deviceSessions.map((ds: any) => ({
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
- // If the current session is invalid, try to find another valid session
772
- if (sessions.length > 1) {
773
- const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
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
- for (const session of otherSessions) {
776
- try {
777
- const validation = await oxyServices.validateSession(session.sessionId, {
778
- useHeaderValidation: true
779
- });
780
- if (validation.valid) {
781
- await switchToSession(session.sessionId);
782
- return;
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
- // No valid sessions found, clear all
791
- setSessions([]);
792
- setActiveSessionId(null);
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
- setSessions([]);
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,