@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
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { createContext, useContext, useEffect, useCallback, useMemo, useRef, useState } from 'react';
4
4
  import { OxyServices } from '../../core';
5
+ import { normalizeAndSortSessions, mergeSessions, sessionsArraysEqual } from '../../utils/sessionUtils';
5
6
  import { DeviceManager } from '../../utils/deviceManager';
6
7
  import { useSessionSocket } from '../hooks/useSessionSocket';
7
8
  import { toast } from '../../lib/sonner';
8
9
  import { useAuthStore } from '../stores/authStore';
10
+ import { getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode } from '../../utils/languageUtils';
9
11
 
10
12
  // Define the context shape
11
13
  // NOTE: We intentionally avoid importing useFollow here to prevent a require cycle.
@@ -83,6 +85,8 @@ const getStorage = async () => {
83
85
  const getStorageKeys = (prefix = 'oxy_session') => ({
84
86
  activeSessionId: `${prefix}_active_session_id`,
85
87
  // Only store the active session ID
88
+ sessionIds: `${prefix}_session_ids`,
89
+ // Store all session IDs for quick account loading
86
90
  language: `${prefix}_language` // Store the selected language
87
91
  });
88
92
  export const OxyProvider = ({
@@ -122,38 +126,21 @@ export const OxyProvider = ({
122
126
  const [minimalUser, setMinimalUser] = useState(null);
123
127
  const [sessions, setSessions] = useState([]);
124
128
  const [activeSessionId, setActiveSessionId] = useState(null);
129
+
130
+ // Track in-flight refresh to prevent duplicate calls
131
+ const refreshInFlightRef = useRef(null);
125
132
  const [storage, setStorage] = useState(null);
126
133
  const [currentLanguage, setCurrentLanguage] = useState('en-US');
127
134
 
128
135
  // Storage keys (memoized to prevent infinite loops) - declared early for use in helpers
129
136
  const keys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
130
137
 
131
- // Normalize language codes to BCP-47 (e.g., en-US)
132
- const normalizeLanguageCode = useCallback(lang => {
133
- if (!lang) return null;
134
- if (lang.includes('-')) return lang;
135
- const map = {
136
- en: 'en-US',
137
- es: 'es-ES',
138
- ca: 'ca-ES',
139
- fr: 'fr-FR',
140
- de: 'de-DE',
141
- it: 'it-IT',
142
- pt: 'pt-PT',
143
- ja: 'ja-JP',
144
- ko: 'ko-KR',
145
- zh: 'zh-CN',
146
- ar: 'ar-SA'
147
- };
148
- return map[lang] || lang;
149
- }, []);
150
-
151
138
  // Helper to apply language preference from user/server
152
139
  const applyLanguagePreference = useCallback(async user => {
153
140
  const userLanguage = user?.language;
154
141
  if (!userLanguage || !storage) return;
155
142
  try {
156
- const serverLang = normalizeLanguageCode(userLanguage) || userLanguage;
143
+ const serverLang = normalizeLanguageCode(userLanguage);
157
144
  await storage.setItem(keys.language, serverLang);
158
145
  setCurrentLanguage(serverLang);
159
146
  } catch (e) {
@@ -161,19 +148,45 @@ export const OxyProvider = ({
161
148
  console.warn('Failed to apply server language preference', e);
162
149
  }
163
150
  }
164
- }, [storage, keys.language, normalizeLanguageCode]);
165
-
166
- // Helper to map server sessions to client sessions
167
- const mapServerSessionsToClient = useCallback((serverSessions, fallbackUserId) => {
168
- return serverSessions.map(s => ({
151
+ }, [storage, keys.language]);
152
+ const mapSessionsToClient = useCallback((sessions, fallbackDeviceId, fallbackUserId) => {
153
+ return sessions.map(s => ({
169
154
  sessionId: s.sessionId,
170
- deviceId: s.deviceId,
171
- expiresAt: s.expiresAt || new Date().toISOString(),
155
+ deviceId: s.deviceId || fallbackDeviceId || '',
156
+ expiresAt: s.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
172
157
  lastActive: s.lastActive || new Date().toISOString(),
173
- userId: s.userId || fallbackUserId
158
+ userId: s.user?.id || s.userId || s.user?._id?.toString() || fallbackUserId || '',
159
+ isCurrent: Boolean(s.isCurrent)
174
160
  }));
175
161
  }, []);
176
162
 
163
+ // Save all session IDs to storage for quick loading on initialization
164
+ const saveSessionIds = useCallback(async sessionIds => {
165
+ if (!storage) return;
166
+ try {
167
+ const uniqueIds = Array.from(new Set(sessionIds));
168
+ await storage.setItem(keys.sessionIds, JSON.stringify(uniqueIds));
169
+ } catch (err) {
170
+ if (__DEV__) {
171
+ console.warn('Failed to save session IDs:', err);
172
+ }
173
+ }
174
+ }, [storage, keys.sessionIds]);
175
+ const updateSessions = useCallback((newSessions, mergeWithExisting = false) => {
176
+ setSessions(prevSessions => {
177
+ const sessionsToProcess = mergeWithExisting ? mergeSessions(prevSessions, newSessions, activeSessionId, false) : normalizeAndSortSessions(newSessions, activeSessionId, false);
178
+
179
+ // Save all session IDs to storage
180
+ if (storage) {
181
+ const allSessionIds = sessionsToProcess.map(s => s.sessionId);
182
+ saveSessionIds(allSessionIds).catch(() => {
183
+ // Ignore errors - non-critical
184
+ });
185
+ }
186
+ return sessionsArraysEqual(prevSessions, sessionsToProcess) ? prevSessions : sessionsToProcess;
187
+ });
188
+ }, [activeSessionId, storage, saveSessionIds]);
189
+
177
190
  // Token ready state - start optimistically so children render immediately
178
191
  const [tokenReady, setTokenReady] = useState(true);
179
192
 
@@ -182,6 +195,7 @@ export const OxyProvider = ({
182
195
  if (!storage) return;
183
196
  try {
184
197
  await storage.removeItem(keys.activeSessionId);
198
+ await storage.removeItem(keys.sessionIds);
185
199
  } catch (err) {
186
200
  if (__DEV__) {
187
201
  console.error('Clear storage error:', err);
@@ -230,8 +244,51 @@ export const OxyProvider = ({
230
244
  setCurrentLanguage(savedLanguage);
231
245
  }
232
246
 
247
+ // Load all stored session IDs and validate them
248
+ const storedSessionIdsJson = await storage.getItem(keys.sessionIds);
249
+ const storedSessionIds = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
250
+
233
251
  // Try to restore active session from storage
234
252
  const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
253
+ const validSessions = [];
254
+
255
+ // If we have stored session IDs, validate them (even without active session)
256
+ if (storedSessionIds.length > 0) {
257
+ if (__DEV__) {
258
+ console.log('Loading stored sessions on init:', storedSessionIds.length);
259
+ }
260
+
261
+ // Validate each stored session ID and build session list
262
+ for (const sessionId of storedSessionIds) {
263
+ try {
264
+ const validation = await oxyServices.validateSession(sessionId, {
265
+ useHeaderValidation: true
266
+ });
267
+ if (validation.valid && validation.user) {
268
+ validSessions.push({
269
+ sessionId,
270
+ userId: validation.user.id?.toString() || '',
271
+ deviceId: '',
272
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
273
+ lastActive: new Date().toISOString(),
274
+ isCurrent: sessionId === storedActiveSessionId
275
+ });
276
+ }
277
+ } catch (e) {
278
+ // Session invalid, skip it
279
+ if (__DEV__) {
280
+ console.warn('Session validation failed for:', sessionId, e);
281
+ }
282
+ }
283
+ }
284
+
285
+ // Update sessions list with validated sessions (even if no active session)
286
+ if (validSessions.length > 0) {
287
+ updateSessions(validSessions, false);
288
+ }
289
+ }
290
+
291
+ // If we have an active session, authenticate with it
235
292
  if (storedActiveSessionId) {
236
293
  try {
237
294
  const validation = await oxyServices.validateSession(storedActiveSessionId, {
@@ -248,35 +305,31 @@ export const OxyProvider = ({
248
305
  avatar: fullUser.avatar
249
306
  });
250
307
  await applyLanguagePreference(fullUser);
251
-
252
- // Get all device sessions to support multiple accounts
253
308
  try {
254
309
  const deviceSessions = await oxyServices.getDeviceSessions(storedActiveSessionId);
255
- const allDeviceSessions = deviceSessions.map(ds => ({
256
- sessionId: ds.sessionId,
257
- deviceId: ds.deviceId,
258
- expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
259
- lastActive: ds.lastActive || new Date().toISOString(),
260
- userId: ds.user?.id || ds.userId || fullUser.id
261
- }));
262
- setSessions(allDeviceSessions);
310
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
311
+ updateSessions(allDeviceSessions, true);
263
312
  } catch (e) {
264
- // Fallback to user sessions
265
313
  if (__DEV__) {
266
314
  console.warn('Failed to get device sessions on init, falling back to user sessions:', e);
267
315
  }
268
316
  const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
269
- setSessions(mapServerSessionsToClient(serverSessions, fullUser.id));
317
+ updateSessions(mapSessionsToClient(serverSessions, undefined, fullUser.id), false);
270
318
  }
271
319
  onAuthStateChange?.(fullUser);
272
320
  } else {
273
- await clearAllStorage();
321
+ // Active session invalid, remove it but keep other sessions
322
+ await storage.removeItem(keys.activeSessionId);
323
+ // Update session list to remove invalid active session
324
+ updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
274
325
  }
275
326
  } catch (e) {
276
327
  if (__DEV__) {
277
- console.error('Session validation error', e);
328
+ console.error('Active session validation error', e);
278
329
  }
279
- await clearAllStorage();
330
+ // Remove invalid active session but keep other sessions
331
+ await storage.removeItem(keys.activeSessionId);
332
+ updateSessions(validSessions.filter(s => s.sessionId !== storedActiveSessionId), false);
280
333
  }
281
334
  }
282
335
  setTokenReady(true);
@@ -289,34 +342,28 @@ export const OxyProvider = ({
289
342
  }
290
343
  };
291
344
  initAuth();
292
- }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference, mapServerSessionsToClient]);
345
+ }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference, mapSessionsToClient, updateSessions]);
293
346
 
294
347
  // Save active session ID to storage (only session ID, no user data)
295
348
  const saveActiveSessionId = useCallback(async sessionId => {
296
349
  if (!storage) return;
297
350
  await storage.setItem(keys.activeSessionId, sessionId);
298
351
  }, [storage, keys.activeSessionId]);
299
-
300
- // Switch to a different session
301
352
  const switchToSession = useCallback(async sessionId => {
302
353
  try {
303
- // Don't set isLoading - session switches should happen silently in background
304
- // Validate session first before attempting to switch
305
354
  const validation = await oxyServices.validateSession(sessionId, {
306
355
  useHeaderValidation: true
307
356
  });
308
357
  if (!validation.valid) {
309
- // Session is invalid, remove it from the sessions list
310
- setSessions(prevSessions => prevSessions.filter(s => s.sessionId !== sessionId));
358
+ updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
311
359
  throw new Error('Session is invalid or expired');
312
360
  }
313
-
314
- // Get access token for this session
361
+ if (!validation.user) {
362
+ throw new Error('User data not available from session validation');
363
+ }
364
+ const fullUser = validation.user;
315
365
  await oxyServices.getTokenBySession(sessionId);
316
366
  setTokenReady(true);
317
-
318
- // Load full user data - use user from validation if available, otherwise fetch
319
- const fullUser = validation.user || (await oxyServices.getUserBySession(sessionId));
320
367
  setActiveSessionId(sessionId);
321
368
  loginSuccess(fullUser);
322
369
  setMinimalUser({
@@ -326,53 +373,17 @@ export const OxyProvider = ({
326
373
  });
327
374
  await saveActiveSessionId(sessionId);
328
375
  await applyLanguagePreference(fullUser);
329
-
330
- // Refresh all device sessions after switching
331
- // Preserve existing sessions from other users to avoid losing accounts
332
- try {
333
- const deviceSessions = await oxyServices.getDeviceSessions(sessionId);
334
- const allDeviceSessions = deviceSessions.map(ds => ({
335
- sessionId: ds.sessionId,
336
- deviceId: ds.deviceId,
337
- expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
338
- lastActive: ds.lastActive || new Date().toISOString(),
339
- userId: ds.user?.id || ds.userId || fullUser.id
340
- }));
341
- // Merge with existing sessions to preserve other accounts
342
- setSessions(prevSessions => {
343
- const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
344
- const newSessions = allDeviceSessions.filter(s => !existingSessionIds.has(s.sessionId));
345
- // Combine existing sessions with new ones, prioritizing new data for existing sessions
346
- const sessionMap = new Map(prevSessions.map(s => [s.sessionId, s]));
347
- allDeviceSessions.forEach(s => sessionMap.set(s.sessionId, s));
348
- return Array.from(sessionMap.values());
349
- });
350
- } catch (error) {
351
- // Fallback to user sessions - merge with existing to preserve other accounts
352
- if (__DEV__) {
353
- console.warn('Failed to get device sessions after switch, falling back to user sessions:', error);
354
- }
355
- const serverSessions = await oxyServices.getSessionsBySessionId(sessionId);
356
- const userSessions = mapServerSessionsToClient(serverSessions, fullUser.id);
357
- // Merge with existing sessions to preserve other accounts
358
- setSessions(prevSessions => {
359
- const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
360
- const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
361
- // Combine existing sessions with new ones, prioritizing new data for existing sessions
362
- const sessionMap = new Map(prevSessions.map(s => [s.sessionId, s]));
363
- userSessions.forEach(s => sessionMap.set(s.sessionId, s));
364
- return Array.from(sessionMap.values());
365
- });
366
- }
376
+ oxyServices.getDeviceSessions(sessionId).then(deviceSessions => {
377
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
378
+ updateSessions(allDeviceSessions, true);
379
+ }).catch(error => {
380
+ if (__DEV__) console.warn('Failed to get device sessions after switch:', error);
381
+ });
367
382
  onAuthStateChange?.(fullUser);
368
383
  } catch (error) {
369
- // Check if the error is due to invalid/expired session
370
384
  const isInvalidSession = error?.response?.status === 401 || error?.message?.includes('Invalid or expired session') || error?.message?.includes('Session is invalid');
371
385
  if (isInvalidSession) {
372
- // Remove invalid session from the sessions list
373
- setSessions(prevSessions => prevSessions.filter(s => s.sessionId !== sessionId));
374
-
375
- // If this was the active session, try to switch to another valid session
386
+ updateSessions(sessions.filter(s => s.sessionId !== sessionId), false);
376
387
  if (sessionId === activeSessionId && sessions.length > 1) {
377
388
  const otherSessions = sessions.filter(s => s.sessionId !== sessionId);
378
389
  for (const otherSession of otherSessions) {
@@ -406,9 +417,7 @@ export const OxyProvider = ({
406
417
  setTokenReady(false);
407
418
  throw error; // Re-throw so calling code can handle it
408
419
  }
409
- }, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference, mapServerSessionsToClient, onError, activeSessionId, sessions]);
410
-
411
- // Login method - only store session ID, retrieve data from backend
420
+ }, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference, mapSessionsToClient, onError, activeSessionId, sessions]);
412
421
  const login = useCallback(async (username, password, deviceName) => {
413
422
  if (!storage) throw new Error('Storage not initialized');
414
423
  useAuthStore.setState({
@@ -431,43 +440,20 @@ export const OxyProvider = ({
431
440
  const sessionResponse = response;
432
441
  await oxyServices.getTokenBySession(sessionResponse.sessionId);
433
442
  const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
434
-
435
- // Get all device sessions to check for duplicates BEFORE setting the new session as active
436
- // This returns all sessions on the device, not just for the current user
437
443
  let allDeviceSessions = [];
438
444
  try {
439
445
  const deviceSessions = await oxyServices.getDeviceSessions(sessionResponse.sessionId);
440
-
441
- // Map device sessions to client format
442
- // Device sessions include user info, so we can map them directly
443
- allDeviceSessions = deviceSessions.map(ds => ({
444
- sessionId: ds.sessionId,
445
- deviceId: ds.deviceId || sessionResponse.deviceId,
446
- expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
447
- lastActive: ds.lastActive || new Date().toISOString(),
448
- userId: ds.user?.id || ds.userId || ds.user?._id?.toString() || fullUser.id
449
- }));
446
+ allDeviceSessions = mapSessionsToClient(deviceSessions, sessionResponse.deviceId, fullUser.id);
450
447
  } catch (error) {
451
- // Fallback to user sessions if device sessions fail
452
448
  if (__DEV__) {
453
449
  console.warn('Failed to get device sessions, falling back to user sessions:', error);
454
450
  }
455
451
  const serverSessions = await oxyServices.getSessionsBySessionId(sessionResponse.sessionId);
456
- const userSessions = mapServerSessionsToClient(serverSessions, fullUser.id);
457
-
458
- // Merge with existing sessions to preserve other accounts
459
- const existingSessionIds = new Set((sessions || []).map(s => s.sessionId));
460
- const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
461
- allDeviceSessions = [...(sessions || []), ...newSessions];
452
+ allDeviceSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
462
453
  }
463
-
464
- // Check if this user is already signed in with another session on this device
465
- // Compare userId as string to handle both string and ObjectId formats
466
454
  const userUserId = fullUser.id?.toString();
467
455
  const existingSession = allDeviceSessions.find(s => s.userId?.toString() === userUserId && s.sessionId !== sessionResponse.sessionId);
468
456
  if (existingSession) {
469
- // User is already signed in on this device, switch to existing session instead
470
- // Logout the newly created session to clean it up
471
457
  try {
472
458
  await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
473
459
  } catch (logoutError) {
@@ -475,24 +461,18 @@ export const OxyProvider = ({
475
461
  console.warn('Failed to logout duplicate session:', logoutError);
476
462
  }
477
463
  }
478
-
479
- // Switch to the existing session
480
464
  await switchToSession(existingSession.sessionId);
481
465
  loginSuccess(fullUser);
482
466
  setMinimalUser(sessionResponse.user);
483
-
484
- // Update sessions list (excluding the duplicate we just created)
485
- setSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId));
467
+ updateSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId), false);
486
468
  onAuthStateChange?.(fullUser);
487
469
  return fullUser;
488
470
  }
489
-
490
- // No duplicate found, proceed with the new session
491
471
  setActiveSessionId(sessionResponse.sessionId);
492
472
  await saveActiveSessionId(sessionResponse.sessionId);
493
473
  loginSuccess(fullUser);
494
474
  setMinimalUser(sessionResponse.user);
495
- setSessions(allDeviceSessions);
475
+ updateSessions(allDeviceSessions, true);
496
476
  onAuthStateChange?.(fullUser);
497
477
  return fullUser;
498
478
  } catch (error) {
@@ -509,7 +489,7 @@ export const OxyProvider = ({
509
489
  isLoading: false
510
490
  });
511
491
  }
512
- }, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure, mapServerSessionsToClient, onError, sessions, switchToSession]);
492
+ }, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure, mapSessionsToClient, onError, sessions, switchToSession]);
513
493
 
514
494
  // Logout method
515
495
  const logout = useCallback(async targetSessionId => {
@@ -517,18 +497,12 @@ export const OxyProvider = ({
517
497
  try {
518
498
  const sessionToLogout = targetSessionId || activeSessionId;
519
499
  await oxyServices.logoutSession(activeSessionId, sessionToLogout);
520
-
521
- // Remove session from local state
522
500
  const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
523
- setSessions(filteredSessions);
524
-
525
- // If logging out active session
501
+ updateSessions(filteredSessions, false);
526
502
  if (sessionToLogout === activeSessionId) {
527
503
  if (filteredSessions.length > 0) {
528
- // Switch to another session
529
504
  await switchToSession(filteredSessions[0].sessionId);
530
505
  } else {
531
- // No sessions left
532
506
  setActiveSessionId(null);
533
507
  logoutStore();
534
508
  setMinimalUser(null);
@@ -553,8 +527,6 @@ export const OxyProvider = ({
553
527
  });
554
528
  }
555
529
  }, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, storage, keys.activeSessionId, onAuthStateChange, onError]);
556
-
557
- // Logout all sessions
558
530
  const logoutAll = useCallback(async () => {
559
531
  if (!activeSessionId) {
560
532
  const error = new Error('No active session found');
@@ -570,7 +542,7 @@ export const OxyProvider = ({
570
542
  }
571
543
  try {
572
544
  await oxyServices.logoutAllSessions(activeSessionId);
573
- setSessions([]);
545
+ updateSessions([], false);
574
546
  setActiveSessionId(null);
575
547
  logoutStore();
576
548
  setMinimalUser(null);
@@ -648,28 +620,16 @@ export const OxyProvider = ({
648
620
  // Get all device sessions to support multiple accounts
649
621
  try {
650
622
  const deviceSessions = await oxyServices.getDeviceSessions(response.sessionId);
651
- const allDeviceSessions = deviceSessions.map(ds => ({
652
- sessionId: ds.sessionId,
653
- deviceId: ds.deviceId,
654
- expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
655
- lastActive: ds.lastActive || new Date().toISOString(),
656
- userId: ds.user?.id || ds.userId || fullUser.id
657
- }));
658
- setSessions(allDeviceSessions);
623
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, fullUser.id);
624
+ updateSessions(allDeviceSessions, true);
659
625
  } catch (error) {
660
626
  // Fallback to user sessions if device sessions fail
661
627
  if (__DEV__) {
662
628
  console.warn('Failed to get device sessions for MFA, falling back to user sessions:', error);
663
629
  }
664
630
  const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
665
- const userSessions = mapServerSessionsToClient(serverSessions, fullUser.id);
666
-
667
- // Merge with existing sessions to preserve other accounts
668
- setSessions(prevSessions => {
669
- const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
670
- const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
671
- return [...prevSessions, ...newSessions];
672
- });
631
+ const userSessions = mapSessionsToClient(serverSessions, undefined, fullUser.id);
632
+ updateSessions(userSessions, true);
673
633
  }
674
634
  onAuthStateChange?.(fullUser);
675
635
  return fullUser;
@@ -688,82 +648,73 @@ export const OxyProvider = ({
688
648
  });
689
649
  }
690
650
  }, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange, applyLanguagePreference, onError]);
691
-
692
- // Switch session method (wrapper for consistency)
693
651
  const switchSession = useCallback(async sessionId => {
694
652
  await switchToSession(sessionId);
695
653
  }, [switchToSession]);
696
-
697
- // Remove session method (wrapper for consistency)
698
654
  const removeSession = useCallback(async sessionId => {
699
655
  await logout(sessionId);
700
656
  }, [logout]);
701
-
702
- // Refresh sessions method
703
657
  const refreshSessions = useCallback(async () => {
704
658
  if (!activeSessionId) return;
705
- try {
706
- // Get all device sessions to support multiple accounts
707
- const deviceSessions = await oxyServices.getDeviceSessions(activeSessionId);
708
- const allDeviceSessions = deviceSessions.map(ds => ({
709
- sessionId: ds.sessionId,
710
- deviceId: ds.deviceId,
711
- expiresAt: ds.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
712
- lastActive: ds.lastActive || new Date().toISOString(),
713
- userId: ds.user?.id || ds.userId || user?.id
714
- }));
715
- setSessions(allDeviceSessions);
716
- } catch (error) {
717
- // Fallback to user sessions if device sessions fail
718
- // Merge with existing sessions to preserve other accounts
719
- if (__DEV__) {
720
- console.warn('Failed to refresh device sessions, falling back to user sessions:', error);
721
- }
659
+
660
+ // If a refresh is already in progress, return the existing promise
661
+ if (refreshInFlightRef.current) {
662
+ return refreshInFlightRef.current;
663
+ }
664
+
665
+ // Create the refresh promise
666
+ const refreshPromise = (async () => {
722
667
  try {
723
- const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
724
- const userSessions = mapServerSessionsToClient(serverSessions, user?.id);
725
- // Merge with existing sessions to preserve other accounts
726
- setSessions(prevSessions => {
727
- const existingSessionIds = new Set(prevSessions.map(s => s.sessionId));
728
- const newSessions = userSessions.filter(s => !existingSessionIds.has(s.sessionId));
729
- // Combine existing sessions with new ones, prioritizing new data for existing sessions
730
- const sessionMap = new Map(prevSessions.map(s => [s.sessionId, s]));
731
- userSessions.forEach(s => sessionMap.set(s.sessionId, s));
732
- return Array.from(sessionMap.values());
733
- });
734
- } catch (fallbackError) {
668
+ const deviceSessions = await oxyServices.getDeviceSessions(activeSessionId);
669
+ const allDeviceSessions = mapSessionsToClient(deviceSessions, undefined, user?.id);
670
+ updateSessions(allDeviceSessions, true);
671
+ } catch (error) {
735
672
  if (__DEV__) {
736
- console.error('Refresh sessions error:', fallbackError);
673
+ console.warn('Failed to refresh device sessions, falling back to user sessions:', error);
737
674
  }
675
+ try {
676
+ const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
677
+ const userSessions = mapSessionsToClient(serverSessions, undefined, user?.id);
678
+ updateSessions(userSessions, true);
679
+ } catch (fallbackError) {
680
+ if (__DEV__) {
681
+ console.error('Refresh sessions error:', fallbackError);
682
+ }
738
683
 
739
- // If the current session is invalid, try to find another valid session
740
- if (sessions.length > 1) {
741
- const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
742
- for (const session of otherSessions) {
743
- try {
744
- const validation = await oxyServices.validateSession(session.sessionId, {
745
- useHeaderValidation: true
746
- });
747
- if (validation.valid) {
748
- await switchToSession(session.sessionId);
749
- return;
684
+ // If the current session is invalid, try to find another valid session
685
+ if (sessions.length > 1) {
686
+ const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
687
+ for (const session of otherSessions) {
688
+ try {
689
+ const validation = await oxyServices.validateSession(session.sessionId, {
690
+ useHeaderValidation: true
691
+ });
692
+ if (validation.valid) {
693
+ await switchToSession(session.sessionId);
694
+ return;
695
+ }
696
+ } catch {
697
+ continue;
750
698
  }
751
- } catch {
752
- continue;
753
699
  }
754
700
  }
755
- }
756
701
 
757
- // No valid sessions found, clear all
758
- setSessions([]);
759
- setActiveSessionId(null);
760
- logoutStore();
761
- setMinimalUser(null);
762
- await clearAllStorage();
763
- onAuthStateChange?.(null);
702
+ // No valid sessions found, clear all
703
+ updateSessions([], false);
704
+ setActiveSessionId(null);
705
+ logoutStore();
706
+ setMinimalUser(null);
707
+ await clearAllStorage();
708
+ onAuthStateChange?.(null);
709
+ }
710
+ } finally {
711
+ // Clear the in-flight ref when done
712
+ refreshInFlightRef.current = null;
764
713
  }
765
- }
766
- }, [activeSessionId, oxyServices, user?.id, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapServerSessionsToClient]);
714
+ })();
715
+ refreshInFlightRef.current = refreshPromise;
716
+ return refreshPromise;
717
+ }, [activeSessionId, oxyServices, user?.id, updateSessions, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapSessionsToClient]);
767
718
 
768
719
  // Device management methods
769
720
  const getDeviceSessions = useCallback(async () => {
@@ -784,7 +735,7 @@ export const OxyProvider = ({
784
735
  if (!activeSessionId) throw new Error('No active session');
785
736
  try {
786
737
  await oxyServices.logoutAllDeviceSessions(activeSessionId);
787
- setSessions([]);
738
+ updateSessions([], false);
788
739
  setActiveSessionId(null);
789
740
  logoutStore();
790
741
  setMinimalUser(null);
@@ -910,6 +861,11 @@ export const OxyProvider = ({
910
861
  return createEmptyFollowHook()(userId);
911
862
  }
912
863
  };
864
+
865
+ // Compute language metadata from currentLanguage
866
+ const languageMetadata = useMemo(() => getLanguageMetadata(currentLanguage), [currentLanguage]);
867
+ const languageName = useMemo(() => getLanguageName(currentLanguage), [currentLanguage]);
868
+ const nativeLanguageName = useMemo(() => getNativeLanguageName(currentLanguage), [currentLanguage]);
913
869
  const contextValue = useMemo(() => ({
914
870
  user,
915
871
  minimalUser,
@@ -920,6 +876,9 @@ export const OxyProvider = ({
920
876
  isTokenReady: tokenReady,
921
877
  error,
922
878
  currentLanguage,
879
+ currentLanguageMetadata: languageMetadata,
880
+ currentLanguageName: languageName,
881
+ currentNativeLanguageName: nativeLanguageName,
923
882
  login,
924
883
  logout,
925
884
  logoutAll,
@@ -941,7 +900,7 @@ export const OxyProvider = ({
941
900
  // Only depend on user ID, not the entire user object
942
901
  minimalUser?.id, sessions.length,
943
902
  // Only depend on sessions count, not the entire array
944
- activeSessionId, isAuthenticated, isLoading, tokenReady, error, currentLanguage, login, logout, logoutAll, signUp, completeMfaLogin, switchSession, removeSession, refreshSessions, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, oxyServices, bottomSheetRef, showBottomSheet, hideBottomSheet]);
903
+ activeSessionId, isAuthenticated, isLoading, tokenReady, error, currentLanguage, languageMetadata, languageName, nativeLanguageName, login, logout, logoutAll, signUp, completeMfaLogin, switchSession, removeSession, refreshSessions, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, oxyServices, bottomSheetRef, showBottomSheet, hideBottomSheet]);
945
904
 
946
905
  // Always render children - let the consuming app decide how to handle token loading state
947
906
  return /*#__PURE__*/_jsx(OxyContext.Provider, {