@oxyhq/services 5.13.0 → 5.13.1

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/lib/commonjs/core/OxyServices.js +7 -7
  2. package/lib/commonjs/core/OxyServices.js.map +1 -1
  3. package/lib/commonjs/i18n/index.js +37 -1
  4. package/lib/commonjs/i18n/index.js.map +1 -1
  5. package/lib/commonjs/i18n/locales/ar-SA.json +128 -0
  6. package/lib/commonjs/i18n/locales/ca-ES.json +128 -0
  7. package/lib/commonjs/i18n/locales/de-DE.json +128 -0
  8. package/lib/commonjs/i18n/locales/en-US.json +85 -12
  9. package/lib/commonjs/i18n/locales/es-ES.json +58 -6
  10. package/lib/commonjs/i18n/locales/fr-FR.json +128 -0
  11. package/lib/commonjs/i18n/locales/it-IT.json +128 -0
  12. package/lib/commonjs/i18n/locales/ja-JP.json +127 -0
  13. package/lib/commonjs/i18n/locales/ko-KR.json +128 -0
  14. package/lib/commonjs/i18n/locales/pt-PT.json +128 -0
  15. package/lib/commonjs/i18n/locales/zh-CN.json +128 -0
  16. package/lib/commonjs/ui/components/FontLoader.js +22 -42
  17. package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
  18. package/lib/commonjs/ui/components/OxyProvider.js +5 -8
  19. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  20. package/lib/commonjs/ui/components/StepBasedScreen.js +64 -44
  21. package/lib/commonjs/ui/components/StepBasedScreen.js.map +1 -1
  22. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +14 -35
  23. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  24. package/lib/commonjs/ui/components/internal/PinInput.js +2 -2
  25. package/lib/commonjs/ui/components/internal/PinInput.js.map +1 -1
  26. package/lib/commonjs/ui/context/OxyContext.js +434 -321
  27. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/SignInScreen.js +43 -39
  30. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +139 -125
  32. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +2 -4
  34. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  35. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js +45 -25
  36. package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  37. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js +88 -53
  38. package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  39. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js +79 -58
  40. package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  41. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js +61 -52
  42. package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  43. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +220 -31
  44. package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  45. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js +77 -50
  46. package/lib/commonjs/ui/screens/steps/SignInTotpStep.js.map +1 -1
  47. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +527 -66
  48. package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  49. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js +55 -30
  50. package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  51. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js +64 -46
  52. package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  53. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js +84 -146
  54. package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  55. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js +113 -34
  56. package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  57. package/lib/commonjs/ui/stores/authStore.js +16 -20
  58. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  59. package/lib/commonjs/ui/styles/authStyles.js +2 -1
  60. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  61. package/lib/commonjs/ui/styles/index.js +11 -0
  62. package/lib/commonjs/ui/styles/index.js.map +1 -1
  63. package/lib/commonjs/ui/styles/spacing.js +51 -0
  64. package/lib/commonjs/ui/styles/spacing.js.map +1 -0
  65. package/lib/commonjs/utils/validationUtils.js +1 -1
  66. package/lib/module/core/OxyServices.js +7 -7
  67. package/lib/module/core/OxyServices.js.map +1 -1
  68. package/lib/module/i18n/index.js +37 -1
  69. package/lib/module/i18n/index.js.map +1 -1
  70. package/lib/module/i18n/locales/ar-SA.json +128 -0
  71. package/lib/module/i18n/locales/ca-ES.json +128 -0
  72. package/lib/module/i18n/locales/de-DE.json +128 -0
  73. package/lib/module/i18n/locales/en-US.json +85 -12
  74. package/lib/module/i18n/locales/es-ES.json +58 -6
  75. package/lib/module/i18n/locales/fr-FR.json +128 -0
  76. package/lib/module/i18n/locales/it-IT.json +128 -0
  77. package/lib/module/i18n/locales/ja-JP.json +127 -0
  78. package/lib/module/i18n/locales/ko-KR.json +128 -0
  79. package/lib/module/i18n/locales/pt-PT.json +128 -0
  80. package/lib/module/i18n/locales/zh-CN.json +128 -0
  81. package/lib/module/ui/components/FontLoader.js +23 -43
  82. package/lib/module/ui/components/FontLoader.js.map +1 -1
  83. package/lib/module/ui/components/OxyProvider.js +6 -8
  84. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  85. package/lib/module/ui/components/StepBasedScreen.js +65 -45
  86. package/lib/module/ui/components/StepBasedScreen.js.map +1 -1
  87. package/lib/module/ui/components/internal/GroupedPillButtons.js +14 -35
  88. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  89. package/lib/module/ui/components/internal/PinInput.js +2 -2
  90. package/lib/module/ui/components/internal/PinInput.js.map +1 -1
  91. package/lib/module/ui/context/OxyContext.js +434 -321
  92. package/lib/module/ui/context/OxyContext.js.map +1 -1
  93. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  94. package/lib/module/ui/screens/SignInScreen.js +44 -40
  95. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  96. package/lib/module/ui/screens/WelcomeNewUserScreen.js +138 -126
  97. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  98. package/lib/module/ui/screens/internal/SignInUsernameStep.js +2 -4
  99. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  100. package/lib/module/ui/screens/steps/RecoverRequestStep.js +45 -25
  101. package/lib/module/ui/screens/steps/RecoverRequestStep.js.map +1 -1
  102. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js +89 -54
  103. package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
  104. package/lib/module/ui/screens/steps/RecoverSuccessStep.js +80 -59
  105. package/lib/module/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
  106. package/lib/module/ui/screens/steps/RecoverVerifyStep.js +62 -53
  107. package/lib/module/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
  108. package/lib/module/ui/screens/steps/SignInPasswordStep.js +221 -32
  109. package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
  110. package/lib/module/ui/screens/steps/SignInTotpStep.js +78 -51
  111. package/lib/module/ui/screens/steps/SignInTotpStep.js.map +1 -1
  112. package/lib/module/ui/screens/steps/SignInUsernameStep.js +530 -68
  113. package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
  114. package/lib/module/ui/screens/steps/SignUpIdentityStep.js +55 -30
  115. package/lib/module/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
  116. package/lib/module/ui/screens/steps/SignUpSecurityStep.js +65 -47
  117. package/lib/module/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
  118. package/lib/module/ui/screens/steps/SignUpSummaryStep.js +84 -146
  119. package/lib/module/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
  120. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js +114 -35
  121. package/lib/module/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
  122. package/lib/module/ui/stores/authStore.js +16 -20
  123. package/lib/module/ui/stores/authStore.js.map +1 -1
  124. package/lib/module/ui/styles/authStyles.js +2 -1
  125. package/lib/module/ui/styles/authStyles.js.map +1 -1
  126. package/lib/module/ui/styles/index.js +1 -0
  127. package/lib/module/ui/styles/index.js.map +1 -1
  128. package/lib/module/ui/styles/spacing.js +48 -0
  129. package/lib/module/ui/styles/spacing.js.map +1 -0
  130. package/lib/module/utils/validationUtils.js +1 -1
  131. package/lib/typescript/core/OxyServices.d.ts +4 -2
  132. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  133. package/lib/typescript/i18n/index.d.ts.map +1 -1
  134. package/lib/typescript/ui/components/FontLoader.d.ts +3 -3
  135. package/lib/typescript/ui/components/FontLoader.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/OxyProvider.d.ts +2 -2
  137. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/StepBasedScreen.d.ts.map +1 -1
  139. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  140. package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
  141. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  142. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  143. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  144. package/lib/typescript/ui/screens/steps/RecoverRequestStep.d.ts.map +1 -1
  145. package/lib/typescript/ui/screens/steps/RecoverResetPasswordStep.d.ts.map +1 -1
  146. package/lib/typescript/ui/screens/steps/RecoverSuccessStep.d.ts.map +1 -1
  147. package/lib/typescript/ui/screens/steps/RecoverVerifyStep.d.ts.map +1 -1
  148. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts +2 -0
  149. package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/steps/SignInTotpStep.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
  152. package/lib/typescript/ui/screens/steps/SignUpIdentityStep.d.ts.map +1 -1
  153. package/lib/typescript/ui/screens/steps/SignUpSecurityStep.d.ts.map +1 -1
  154. package/lib/typescript/ui/screens/steps/SignUpSummaryStep.d.ts.map +1 -1
  155. package/lib/typescript/ui/screens/steps/SignUpWelcomeStep.d.ts.map +1 -1
  156. package/lib/typescript/ui/stores/authStore.d.ts +7 -3
  157. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  158. package/lib/typescript/ui/styles/authStyles.d.ts +1 -0
  159. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
  160. package/lib/typescript/ui/styles/index.d.ts +1 -0
  161. package/lib/typescript/ui/styles/index.d.ts.map +1 -1
  162. package/lib/typescript/ui/styles/spacing.d.ts +43 -0
  163. package/lib/typescript/ui/styles/spacing.d.ts.map +1 -0
  164. package/lib/typescript/utils/validationUtils.d.ts +1 -1
  165. package/package.json +1 -1
  166. package/src/core/OxyServices.ts +10 -8
  167. package/src/i18n/index.ts +36 -0
  168. package/src/i18n/locales/ar-SA.json +128 -0
  169. package/src/i18n/locales/ca-ES.json +128 -0
  170. package/src/i18n/locales/de-DE.json +128 -0
  171. package/src/i18n/locales/en-US.json +85 -12
  172. package/src/i18n/locales/es-ES.json +58 -6
  173. package/src/i18n/locales/fr-FR.json +128 -0
  174. package/src/i18n/locales/it-IT.json +128 -0
  175. package/src/i18n/locales/ja-JP.json +127 -0
  176. package/src/i18n/locales/ko-KR.json +128 -0
  177. package/src/i18n/locales/pt-PT.json +128 -0
  178. package/src/i18n/locales/zh-CN.json +128 -0
  179. package/src/ui/components/FontLoader.tsx +17 -37
  180. package/src/ui/components/OxyProvider.tsx +14 -13
  181. package/src/ui/components/StepBasedScreen.tsx +66 -43
  182. package/src/ui/components/internal/GroupedPillButtons.tsx +15 -31
  183. package/src/ui/components/internal/PinInput.tsx +2 -2
  184. package/src/ui/context/OxyContext.tsx +404 -285
  185. package/src/ui/screens/FileManagementScreen.tsx +15 -15
  186. package/src/ui/screens/SignInScreen.tsx +59 -36
  187. package/src/ui/screens/WelcomeNewUserScreen.tsx +102 -91
  188. package/src/ui/screens/internal/SignInUsernameStep.tsx +1 -1
  189. package/src/ui/screens/steps/RecoverRequestStep.tsx +34 -24
  190. package/src/ui/screens/steps/RecoverResetPasswordStep.tsx +65 -36
  191. package/src/ui/screens/steps/RecoverSuccessStep.tsx +71 -47
  192. package/src/ui/screens/steps/RecoverVerifyStep.tsx +60 -50
  193. package/src/ui/screens/steps/SignInPasswordStep.tsx +191 -29
  194. package/src/ui/screens/steps/SignInTotpStep.tsx +68 -34
  195. package/src/ui/screens/steps/SignInUsernameStep.tsx +586 -57
  196. package/src/ui/screens/steps/SignUpIdentityStep.tsx +49 -35
  197. package/src/ui/screens/steps/SignUpSecurityStep.tsx +56 -39
  198. package/src/ui/screens/steps/SignUpSummaryStep.tsx +99 -89
  199. package/src/ui/screens/steps/SignUpWelcomeStep.tsx +88 -20
  200. package/src/ui/stores/authStore.ts +15 -19
  201. package/src/ui/styles/authStyles.ts +2 -1
  202. package/src/ui/styles/index.ts +1 -0
  203. package/src/ui/styles/spacing.ts +46 -0
  204. package/src/utils/validationUtils.ts +1 -1
@@ -1,7 +1,6 @@
1
1
  import type React from 'react';
2
2
  import { createContext, useContext, useEffect, useCallback, useMemo, useRef, useState, type ReactNode } from 'react';
3
3
  import type { UseFollowHook } from '../hooks/useFollow.types';
4
- import { View, Text } from 'react-native';
5
4
  import { OxyServices } from '../../core';
6
5
  import type { User, ApiError } from '../../models/interfaces';
7
6
  import type { SessionLoginResponse, ClientSession, MinimalUserData } from '../../models/session';
@@ -24,6 +23,7 @@ export interface OxyContextState {
24
23
  activeSessionId: string | null;
25
24
  isAuthenticated: boolean; // Single source of truth for authentication - use this instead of service methods
26
25
  isLoading: boolean;
26
+ isTokenReady: boolean; // Whether the token has been loaded/restored and is ready for use
27
27
  error: string | null;
28
28
 
29
29
  // Language state
@@ -64,6 +64,26 @@ export interface OxyContextState {
64
64
  useFollow: UseFollowHook; // Back-compat; prefer direct import
65
65
  }
66
66
 
67
+ // Empty follow hook fallback
68
+ const createEmptyFollowHook = (): UseFollowHook => {
69
+ const emptyResult = {
70
+ isFollowing: false,
71
+ isLoading: false,
72
+ error: null,
73
+ toggleFollow: async () => { },
74
+ setFollowStatus: () => { },
75
+ fetchStatus: async () => { },
76
+ clearError: () => { },
77
+ followerCount: null,
78
+ followingCount: null,
79
+ isLoadingCounts: false,
80
+ fetchUserCounts: async () => { },
81
+ setFollowerCount: () => { },
82
+ setFollowingCount: () => { },
83
+ };
84
+ return () => emptyResult;
85
+ };
86
+
67
87
  // Create the context with default values
68
88
  const OxyContext = createContext<OxyContextState | null>(null);
69
89
 
@@ -177,6 +197,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
177
197
  const [storage, setStorage] = useState<StorageInterface | null>(null);
178
198
  const [currentLanguage, setCurrentLanguage] = useState<string>('en-US');
179
199
 
200
+ // Storage keys (memoized to prevent infinite loops) - declared early for use in helpers
201
+ const keys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
202
+
180
203
  // Normalize language codes to BCP-47 (e.g., en-US)
181
204
  const normalizeLanguageCode = useCallback((lang?: string | null): string | null => {
182
205
  if (!lang) return null;
@@ -187,21 +210,55 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
187
210
  };
188
211
  return map[lang] || lang;
189
212
  }, []);
190
- // Add a new state to track token restoration
191
- const [tokenReady, setTokenReady] = useState(false);
192
213
 
193
- // Storage keys (memoized to prevent infinite loops)
194
- const keys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
214
+ // Helper to apply language preference from user/server
215
+ const applyLanguagePreference = useCallback(async (user: User): Promise<void> => {
216
+ const userLanguage = (user as Record<string, unknown>)?.language as string | undefined;
217
+ if (!userLanguage || !storage) return;
218
+
219
+ try {
220
+ const serverLang = normalizeLanguageCode(userLanguage) || userLanguage;
221
+ await storage.setItem(keys.language, serverLang);
222
+ setCurrentLanguage(serverLang);
223
+ } catch (e) {
224
+ if (__DEV__) {
225
+ console.warn('Failed to apply server language preference', e);
226
+ }
227
+ }
228
+ }, [storage, keys.language, normalizeLanguageCode]);
229
+
230
+ // Helper to map server sessions to client sessions
231
+ const mapServerSessionsToClient = useCallback((serverSessions: Array<{
232
+ sessionId: string;
233
+ deviceId: string;
234
+ expiresAt?: string;
235
+ lastActive?: string;
236
+ userId?: string;
237
+ }>, fallbackUserId?: string): ClientSession[] => {
238
+ return serverSessions.map(s => ({
239
+ sessionId: s.sessionId,
240
+ deviceId: s.deviceId,
241
+ expiresAt: s.expiresAt || new Date().toISOString(),
242
+ lastActive: s.lastActive || new Date().toISOString(),
243
+ userId: s.userId || fallbackUserId
244
+ }));
245
+ }, []);
195
246
 
196
- // Clear all storage - defined before initAuth to avoid dependency issues
247
+ // Token ready state - start optimistically so children render immediately
248
+ const [tokenReady, setTokenReady] = useState(true);
249
+
250
+ // Clear all storage
197
251
  const clearAllStorage = useCallback(async (): Promise<void> => {
198
252
  if (!storage) return;
199
253
  try {
200
254
  await storage.removeItem(keys.activeSessionId);
201
255
  } catch (err) {
202
- console.error('Clear storage error:', err);
256
+ if (__DEV__) {
257
+ console.error('Clear storage error:', err);
258
+ }
259
+ onError?.({ message: 'Failed to clear storage', code: 'STORAGE_ERROR', status: 500 });
203
260
  }
204
- }, [storage, keys]);
261
+ }, [storage, keys, onError]);
205
262
 
206
263
  // Initialize storage
207
264
  useEffect(() => {
@@ -210,21 +267,22 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
210
267
  const platformStorage = await getStorage();
211
268
  setStorage(platformStorage);
212
269
  } catch (error) {
213
- console.error('Init storage failed', error);
214
- useAuthStore.setState({ error: 'Failed to initialize storage' });
270
+ const errorMessage = error instanceof Error ? error.message : 'Failed to initialize storage';
271
+ useAuthStore.setState({ error: errorMessage });
272
+ onError?.({ message: errorMessage, code: 'STORAGE_INIT_ERROR', status: 500 });
215
273
  }
216
274
  };
217
275
  initStorage();
218
- }, []);
276
+ }, [onError]);
219
277
 
220
278
  // Initialize authentication state
279
+ // Note: We don't set isLoading during initialization to avoid showing spinners
280
+ // Children render immediately and can check isTokenReady/isAuthenticated themselves
221
281
  useEffect(() => {
222
282
  const initAuth = async () => {
223
283
  if (!storage) return;
224
- useAuthStore.setState({ isLoading: true });
284
+ // Don't set isLoading during initialization - let it happen in background
225
285
  try {
226
- setTokenReady(false);
227
-
228
286
  // Load saved language preference
229
287
  const savedLanguageRaw = await storage.getItem(keys.language);
230
288
  const savedLanguage = normalizeLanguageCode(savedLanguageRaw) || savedLanguageRaw;
@@ -243,68 +301,50 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
243
301
  const fullUser = await oxyServices.getUserBySession(storedActiveSessionId);
244
302
  loginSuccess(fullUser);
245
303
  setMinimalUser({ id: fullUser.id, username: fullUser.username, avatar: fullUser.avatar });
246
- // Apply server language if present
247
- if ((fullUser as any)?.language) {
248
- try {
249
- const serverLang = normalizeLanguageCode((fullUser as any).language) || (fullUser as any).language;
250
- await storage.setItem(keys.language, serverLang);
251
- setCurrentLanguage(serverLang);
252
- } catch (e) {
253
- console.warn('Failed to apply server language preference', e);
304
+
305
+ await applyLanguagePreference(fullUser);
306
+
307
+ // Get all device sessions to support multiple accounts
308
+ try {
309
+ 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);
318
+ } catch (e) {
319
+ // Fallback to user sessions
320
+ if (__DEV__) {
321
+ console.warn('Failed to get device sessions on init, falling back to user sessions:', e);
254
322
  }
255
- }
256
323
  const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
257
- const clientSessions: ClientSession[] = serverSessions.map(s => ({
258
- sessionId: s.sessionId,
259
- deviceId: s.deviceId,
260
- expiresAt: s.expiresAt || new Date().toISOString(),
261
- lastActive: s.lastActive || new Date().toISOString(),
262
- userId: s.userId || fullUser.id
263
- }));
264
- setSessions(clientSessions);
324
+ setSessions(mapServerSessionsToClient(serverSessions, fullUser.id));
325
+ }
265
326
  onAuthStateChange?.(fullUser);
266
327
  } else {
267
328
  await clearAllStorage();
268
329
  }
269
330
  } catch (e) {
270
- console.error('Session validation error', e);
331
+ if (__DEV__) {
332
+ console.error('Session validation error', e);
333
+ }
271
334
  await clearAllStorage();
272
335
  }
273
336
  }
274
337
  setTokenReady(true);
275
338
  } catch (e) {
276
- console.error('Auth init error', e);
339
+ if (__DEV__) {
340
+ console.error('Auth init error', e);
341
+ }
277
342
  await clearAllStorage();
278
- } finally {
279
- useAuthStore.setState({ isLoading: false });
343
+ setTokenReady(true);
280
344
  }
281
345
  };
282
346
  initAuth();
283
- }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage]);
284
-
285
-
286
-
287
- // Remove invalid session - refresh sessions from backend
288
- const removeInvalidSession = useCallback(async (sessionId: string): Promise<void> => {
289
- // Remove from local state
290
- const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
291
- setSessions(filteredSessions);
292
-
293
- // If there are other sessions, switch to the first one
294
- if (filteredSessions.length > 0) {
295
- await switchToSession(filteredSessions[0].sessionId);
296
- } else {
297
- // No valid sessions left
298
- setActiveSessionId(null);
299
- logoutStore();
300
- setMinimalUser(null);
301
- await storage?.removeItem(keys.activeSessionId);
302
-
303
- if (onAuthStateChange) {
304
- onAuthStateChange(null);
305
- }
306
- }
307
- }, [sessions, storage, keys, onAuthStateChange, logoutStore]);
347
+ }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage, applyLanguagePreference, mapServerSessionsToClient]);
308
348
 
309
349
  // Save active session ID to storage (only session ID, no user data)
310
350
  const saveActiveSessionId = useCallback(async (sessionId: string): Promise<void> => {
@@ -315,13 +355,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
315
355
  // Switch to a different session
316
356
  const switchToSession = useCallback(async (sessionId: string): Promise<void> => {
317
357
  try {
318
- useAuthStore.setState({ isLoading: true });
358
+ // Don't set isLoading - session switches should happen silently in background
359
+ // Validate session first before attempting to switch
360
+ const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
361
+ if (!validation.valid) {
362
+ // Session is invalid, remove it from the sessions list
363
+ setSessions((prevSessions) => prevSessions.filter(s => s.sessionId !== sessionId));
364
+ throw new Error('Session is invalid or expired');
365
+ }
319
366
 
320
367
  // Get access token for this session
321
368
  await oxyServices.getTokenBySession(sessionId);
369
+ setTokenReady(true);
322
370
 
323
- // Load full user data
324
- const fullUser = await oxyServices.getUserBySession(sessionId);
371
+ // Load full user data - use user from validation if available, otherwise fetch
372
+ const fullUser = validation.user || await oxyServices.getUserBySession(sessionId);
325
373
 
326
374
  setActiveSessionId(sessionId);
327
375
  loginSuccess(fullUser);
@@ -332,27 +380,85 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
332
380
  });
333
381
 
334
382
  await saveActiveSessionId(sessionId);
335
- // Apply server language if present
336
- if ((fullUser as any)?.language) {
337
- try {
338
- const serverLang = normalizeLanguageCode((fullUser as any).language) || (fullUser as any).language;
339
- await storage?.setItem(keys.language, serverLang);
340
- setCurrentLanguage(serverLang);
341
- } catch (e) {
342
- console.warn('Failed to apply server language after switch', e);
383
+ 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);
343
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());
420
+ });
344
421
  }
345
-
346
- if (onAuthStateChange) {
347
- onAuthStateChange(fullUser);
422
+
423
+ onAuthStateChange?.(fullUser);
424
+ } 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
+
430
+ 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
435
+ if (sessionId === activeSessionId && sessions.length > 1) {
436
+ const otherSessions = sessions.filter(s => s.sessionId !== sessionId);
437
+ for (const otherSession of otherSessions) {
438
+ try {
439
+ const otherValidation = await oxyServices.validateSession(otherSession.sessionId, { useHeaderValidation: true });
440
+ if (otherValidation.valid) {
441
+ await switchToSession(otherSession.sessionId);
442
+ return;
443
+ }
444
+ } catch {
445
+ // Continue to next session
446
+ continue;
447
+ }
448
+ }
449
+ }
348
450
  }
349
- } catch (error) {
350
- console.error('Switch session error:', error);
351
- useAuthStore.setState({ error: 'Failed to switch session' });
352
- } finally {
353
- useAuthStore.setState({ isLoading: false });
451
+
452
+ const errorMessage = error instanceof Error ? error.message : 'Failed to switch session';
453
+ if (__DEV__) {
454
+ console.error('Switch session error:', error);
455
+ }
456
+ useAuthStore.setState({ error: errorMessage });
457
+ onError?.({ message: errorMessage, code: isInvalidSession ? 'INVALID_SESSION' : 'SESSION_SWITCH_ERROR', status: isInvalidSession ? 401 : 500 });
458
+ setTokenReady(false);
459
+ throw error; // Re-throw so calling code can handle it
354
460
  }
355
- }, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId]);
461
+ }, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId, applyLanguagePreference, mapServerSessionsToClient, onError, activeSessionId, sessions]);
356
462
 
357
463
  // Login method - only store session ID, retrieve data from backend
358
464
  const login = useCallback(async (username: string, password: string, deviceName?: string): Promise<User> => {
@@ -360,16 +466,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
360
466
  useAuthStore.setState({ isLoading: true, error: null });
361
467
 
362
468
  try {
363
- // Get device fingerprint for enhanced device identification
364
469
  const deviceFingerprint = DeviceManager.getDeviceFingerprint();
365
-
366
- // Get or generate persistent device info
367
470
  const deviceInfo = await DeviceManager.getDeviceInfo();
368
471
 
369
- console.log('Auth - Using device fingerprint:', deviceFingerprint);
370
- console.log('Auth - Using device ID:', deviceInfo.deviceId);
371
-
372
- const response: any = await oxyServices.signIn(
472
+ const response = await oxyServices.signIn(
373
473
  username,
374
474
  password,
375
475
  deviceName || deviceInfo.deviceName || DeviceManager.getDefaultDeviceName(),
@@ -377,49 +477,102 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
377
477
  );
378
478
 
379
479
  // Handle MFA requirement
380
- if (response && response.mfaRequired) {
381
- const err: any = new Error('Multi-factor authentication required');
382
- err.code = 'MFA_REQUIRED';
383
- err.mfaToken = response.mfaToken;
384
- err.expiresAt = response.expiresAt;
385
- throw err;
480
+ if (response && 'mfaRequired' in response && response.mfaRequired) {
481
+ const mfaError = new Error('Multi-factor authentication required') as Error & {
482
+ code: string;
483
+ mfaToken?: string;
484
+ expiresAt?: string;
485
+ };
486
+ mfaError.code = 'MFA_REQUIRED';
487
+ mfaError.mfaToken = (response as { mfaToken?: string }).mfaToken;
488
+ mfaError.expiresAt = (response as { expiresAt?: string }).expiresAt;
489
+ throw mfaError;
386
490
  }
387
491
 
388
- // Set as active session (only store session ID)
389
- setActiveSessionId((response as SessionLoginResponse).sessionId);
390
- await saveActiveSessionId((response as SessionLoginResponse).sessionId);
492
+ const sessionResponse = response as SessionLoginResponse;
391
493
 
392
- // Get access token for API calls
393
- await oxyServices.getTokenBySession(response.sessionId);
494
+ await oxyServices.getTokenBySession(sessionResponse.sessionId);
495
+ const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
394
496
 
395
- // Load full user data from backend
396
- const fullUser = await oxyServices.getUserBySession((response as SessionLoginResponse).sessionId);
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
+ let allDeviceSessions: ClientSession[] = [];
500
+ try {
501
+ 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
+ }));
512
+ } catch (error) {
513
+ // Fallback to user sessions if device sessions fail
514
+ if (__DEV__) {
515
+ console.warn('Failed to get device sessions, falling back to user sessions:', error);
516
+ }
517
+ 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];
524
+ }
525
+
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
+ const userUserId = fullUser.id?.toString();
529
+ const existingSession = allDeviceSessions.find(
530
+ s => s.userId?.toString() === userUserId && s.sessionId !== sessionResponse.sessionId
531
+ );
532
+
533
+ 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
+ try {
537
+ await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
538
+ } catch (logoutError) {
539
+ if (__DEV__) {
540
+ console.warn('Failed to logout duplicate session:', logoutError);
541
+ }
542
+ }
543
+
544
+ // Switch to the existing session
545
+ await switchToSession(existingSession.sessionId);
397
546
  loginSuccess(fullUser);
398
- setMinimalUser(response.user);
399
-
400
- // Load sessions from backend
401
- const serverSessions = await oxyServices.getSessionsBySessionId((response as SessionLoginResponse).sessionId);
402
- const clientSessions: ClientSession[] = serverSessions.map(serverSession => ({
403
- sessionId: serverSession.sessionId,
404
- deviceId: serverSession.deviceId,
405
- expiresAt: serverSession.expiresAt || new Date().toISOString(),
406
- lastActive: serverSession.lastActive || new Date().toISOString(),
407
- userId: serverSession.userId || fullUser.id
408
- }));
409
- setSessions(clientSessions);
547
+ setMinimalUser(sessionResponse.user);
410
548
 
411
- if (onAuthStateChange) {
412
- onAuthStateChange(fullUser);
549
+ // Update sessions list (excluding the duplicate we just created)
550
+ setSessions(allDeviceSessions.filter(s => s.sessionId !== sessionResponse.sessionId));
551
+
552
+ onAuthStateChange?.(fullUser);
553
+ return fullUser;
413
554
  }
414
555
 
556
+ // No duplicate found, proceed with the new session
557
+ setActiveSessionId(sessionResponse.sessionId);
558
+ await saveActiveSessionId(sessionResponse.sessionId);
559
+
560
+ loginSuccess(fullUser);
561
+ setMinimalUser(sessionResponse.user);
562
+
563
+ setSessions(allDeviceSessions);
564
+
565
+ onAuthStateChange?.(fullUser);
415
566
  return fullUser;
416
- } catch (error: any) {
417
- loginFailure(error.message || 'Login failed');
567
+ } catch (error) {
568
+ const errorMessage = error instanceof Error ? error.message : 'Login failed';
569
+ loginFailure(errorMessage);
570
+ onError?.({ message: errorMessage, code: 'LOGIN_ERROR', status: 401 });
418
571
  throw error;
419
572
  } finally {
420
573
  useAuthStore.setState({ isLoading: false });
421
574
  }
422
- }, [storage, oxyServices, saveActiveSessionId, loginSuccess, setMinimalUser, onAuthStateChange, loginFailure]);
575
+ }, [storage, oxyServices, saveActiveSessionId, loginSuccess, onAuthStateChange, loginFailure, mapServerSessionsToClient, onError, sessions, switchToSession]);
423
576
 
424
577
  // Logout method
425
578
  const logout = useCallback(async (targetSessionId?: string): Promise<void> => {
@@ -451,70 +604,43 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
451
604
  }
452
605
  }
453
606
  } catch (error) {
454
- console.error('Logout error:', error);
455
- useAuthStore.setState({ error: 'Logout failed' });
607
+ const errorMessage = error instanceof Error ? error.message : 'Logout failed';
608
+ if (__DEV__) {
609
+ console.error('Logout error:', error);
610
+ }
611
+ useAuthStore.setState({ error: errorMessage });
612
+ onError?.({ message: errorMessage, code: 'LOGOUT_ERROR', status: 500 });
456
613
  }
457
- }, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, setMinimalUser, storage, keys.activeSessionId, onAuthStateChange]);
614
+ }, [activeSessionId, oxyServices, sessions, switchToSession, logoutStore, storage, keys.activeSessionId, onAuthStateChange, onError]);
458
615
 
459
616
  // Logout all sessions
460
617
  const logoutAll = useCallback(async (): Promise<void> => {
461
- console.log('logoutAll called with activeSessionId:', activeSessionId);
462
-
463
618
  if (!activeSessionId) {
464
- console.error('No active session ID found, cannot logout all');
465
- useAuthStore.setState({ error: 'No active session found' });
466
- throw new Error('No active session found');
467
- }
468
-
469
- if (!oxyServices) {
470
- console.error('OxyServices not initialized');
471
- useAuthStore.setState({ error: 'Service not available' });
472
- throw new Error('Service not available');
619
+ const error = new Error('No active session found');
620
+ useAuthStore.setState({ error: error.message });
621
+ onError?.({ message: error.message, code: 'NO_SESSION_ERROR', status: 404 });
622
+ throw error;
473
623
  }
474
624
 
475
625
  try {
476
- console.log('Calling oxyServices.logoutAllSessions with sessionId:', activeSessionId);
477
626
  await oxyServices.logoutAllSessions(activeSessionId);
478
- console.log('logoutAllSessions completed successfully');
479
627
 
480
- // Clear all local data
481
628
  setSessions([]);
482
629
  setActiveSessionId(null);
483
630
  logoutStore();
484
631
  setMinimalUser(null);
485
632
  await clearAllStorage();
486
- console.log('Local storage cleared');
487
-
488
- if (onAuthStateChange) {
489
- onAuthStateChange(null);
490
- console.log('Auth state change callback called');
491
- }
633
+ onAuthStateChange?.(null);
492
634
  } catch (error) {
493
- console.error('Logout all error:', error);
494
- useAuthStore.setState({ error: `Logout all failed: ${error instanceof Error ? error.message : 'Unknown error'}` });
635
+ const errorMessage = error instanceof Error ? error.message : 'Logout all failed';
636
+ useAuthStore.setState({ error: errorMessage });
637
+ onError?.({ message: errorMessage, code: 'LOGOUT_ALL_ERROR', status: 500 });
495
638
  throw error;
496
639
  }
497
- }, [activeSessionId, oxyServices, logoutStore, setMinimalUser, clearAllStorage, onAuthStateChange]);
640
+ }, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
498
641
 
499
- // Effect to restore token on app load or session switch
500
- useEffect(() => {
501
- const restoreToken = async () => {
502
- if (activeSessionId && oxyServices) {
503
- try {
504
- await oxyServices.getTokenBySession(activeSessionId);
505
- setTokenReady(true);
506
- } catch (err) {
507
- // If token restoration fails, force logout
508
- await logout();
509
- setTokenReady(false);
510
- }
511
- } else {
512
- setTokenReady(true); // No session, so token is not needed
513
- }
514
- };
515
- restoreToken();
516
- // Only run when activeSessionId or oxyServices changes
517
- }, [activeSessionId, oxyServices, logout]);
642
+ // Token restoration is handled in initAuth and switchToSession
643
+ // No separate effect needed - children render immediately with isTokenReady available
518
644
 
519
645
  // Sign up method
520
646
  const signUp = useCallback(async (username: string, email: string, password: string): Promise<User> => {
@@ -523,23 +649,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
523
649
  useAuthStore.setState({ isLoading: true, error: null });
524
650
 
525
651
  try {
526
- // Create new account using the OxyServices signUp method
527
- const response = await oxyServices.signUp(username, email, password);
528
-
529
- console.log('SignUp successful:', response);
530
-
531
- // Now log the user in to create a session
532
- // This will handle the session creation and device registration
652
+ await oxyServices.signUp(username, email, password);
533
653
  const user = await login(username, password);
534
-
535
654
  return user;
536
- } catch (error: any) {
537
- loginFailure(error.message || 'Sign up failed');
655
+ } catch (error) {
656
+ const errorMessage = error instanceof Error ? error.message : 'Sign up failed';
657
+ loginFailure(errorMessage);
658
+ onError?.({ message: errorMessage, code: 'SIGNUP_ERROR', status: 400 });
538
659
  throw error;
539
660
  } finally {
540
661
  useAuthStore.setState({ isLoading: false });
541
662
  }
542
- }, [storage, oxyServices, login, loginFailure]);
663
+ }, [storage, oxyServices, login, loginFailure, onError]);
543
664
 
544
665
  // Complete MFA login by verifying TOTP
545
666
  const completeMfaLogin = useCallback(async (mfaToken: string, code: string): Promise<User> => {
@@ -555,190 +676,190 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
555
676
  // Fetch access token and user data
556
677
  await oxyServices.getTokenBySession(response.sessionId);
557
678
  const fullUser = await oxyServices.getUserBySession(response.sessionId);
679
+
558
680
  loginSuccess(fullUser);
559
681
  setMinimalUser({ id: fullUser.id, username: fullUser.username, avatar: fullUser.avatar });
560
- // Apply server language if present
561
- if ((fullUser as any)?.language) {
562
- try {
563
- const serverLang = normalizeLanguageCode((fullUser as any).language) || (fullUser as any).language;
564
- await storage.setItem(keys.language, serverLang);
565
- setCurrentLanguage(serverLang);
566
- } catch (e) {
567
- console.warn('Failed to apply server language on MFA login', e);
568
- }
569
- }
682
+ await applyLanguagePreference(fullUser);
570
683
 
571
- // Load sessions list
572
- const serverSessions = await oxyServices.getSessionsBySessionId(response.sessionId);
573
- const clientSessions: ClientSession[] = serverSessions.map(s => ({
574
- sessionId: s.sessionId,
575
- deviceId: s.deviceId,
576
- expiresAt: s.expiresAt || new Date().toISOString(),
577
- lastActive: s.lastActive || new Date().toISOString(),
578
- userId: s.userId || fullUser.id
579
- }));
580
- setSessions(clientSessions);
581
- // Apply server language if present
582
- if ((fullUser as any)?.language) {
583
- try {
584
- await storage.setItem(keys.language, (fullUser as any).language);
585
- setCurrentLanguage((fullUser as any).language);
586
- } catch (e) {
587
- console.warn('Failed to apply server language on MFA login', e);
684
+ // Get all device sessions to support multiple accounts
685
+ try {
686
+ 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);
695
+ } catch (error) {
696
+ // Fallback to user sessions if device sessions fail
697
+ if (__DEV__) {
698
+ console.warn('Failed to get device sessions for MFA, falling back to user sessions:', error);
588
699
  }
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
+ });
589
709
  }
590
710
 
591
- if (onAuthStateChange) onAuthStateChange(fullUser);
711
+ onAuthStateChange?.(fullUser);
592
712
  return fullUser;
593
- } catch (error: any) {
594
- loginFailure(error.message || 'MFA verification failed');
713
+ } catch (error) {
714
+ const errorMessage = error instanceof Error ? error.message : 'MFA verification failed';
715
+ loginFailure(errorMessage);
716
+ onError?.({ message: errorMessage, code: 'MFA_ERROR', status: 401 });
595
717
  throw error;
596
718
  } finally {
597
719
  useAuthStore.setState({ isLoading: false });
598
720
  }
599
- }, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange]);
721
+ }, [storage, oxyServices, loginSuccess, loginFailure, saveActiveSessionId, onAuthStateChange, applyLanguagePreference, onError]);
600
722
 
601
- // Switch session method
723
+ // Switch session method (wrapper for consistency)
602
724
  const switchSession = useCallback(async (sessionId: string): Promise<void> => {
603
725
  await switchToSession(sessionId);
604
726
  }, [switchToSession]);
605
727
 
606
- // Remove session method
728
+ // Remove session method (wrapper for consistency)
607
729
  const removeSession = useCallback(async (sessionId: string): Promise<void> => {
608
730
  await logout(sessionId);
609
731
  }, [logout]);
610
732
 
611
733
  // Refresh sessions method
612
734
  const refreshSessions = useCallback(async (): Promise<void> => {
613
- console.log('refreshSessions called with activeSessionId:', activeSessionId);
614
-
615
- if (!activeSessionId) {
616
- console.log('refreshSessions: No activeSessionId, returning');
617
- return;
618
- }
735
+ if (!activeSessionId) return;
619
736
 
620
737
  try {
621
- console.log('refreshSessions: Calling getSessionsBySessionId...');
622
- const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
623
- console.log('refreshSessions: Server sessions received:', serverSessions);
624
-
625
- // Update local sessions with server data
626
- const updatedSessions: ClientSession[] = serverSessions.map(serverSession => ({
627
- sessionId: serverSession.sessionId,
628
- deviceId: serverSession.deviceId,
629
- expiresAt: serverSession.expiresAt || new Date().toISOString(),
630
- lastActive: serverSession.lastActive || new Date().toISOString(),
631
- userId: serverSession.userId || user?.id
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,
632
746
  }));
633
-
634
- console.log('refreshSessions: Updated sessions:', updatedSessions);
635
- setSessions(updatedSessions);
636
- console.log('refreshSessions: Sessions updated in state');
747
+ setSessions(allDeviceSessions);
637
748
  } catch (error) {
638
- console.error('Refresh sessions error:', 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
+ }
639
770
 
640
771
  // If the current session is invalid, try to find another valid session
641
772
  if (sessions.length > 1) {
642
- console.log('Current session invalid, trying to switch to another session...');
643
773
  const otherSessions = sessions.filter(s => s.sessionId !== activeSessionId);
644
774
 
645
775
  for (const session of otherSessions) {
646
776
  try {
647
- // Try to validate this session
648
- await oxyServices.validateSession(session.sessionId, {
777
+ const validation = await oxyServices.validateSession(session.sessionId, {
649
778
  useHeaderValidation: true
650
779
  });
651
- console.log('Found valid session, switching to:', session.sessionId);
652
- await switchToSession(session.sessionId);
653
- return; // Successfully switched to another session
654
- } catch (sessionError) {
655
- console.log('Session validation failed for:', session.sessionId, sessionError);
656
- continue; // Try next session
780
+ if (validation.valid) {
781
+ await switchToSession(session.sessionId);
782
+ return;
783
+ }
784
+ } catch {
785
+ continue;
657
786
  }
658
787
  }
659
788
  }
660
789
 
661
- // If no valid sessions found, clear all sessions
662
- console.log('No valid sessions found, clearing all sessions');
790
+ // No valid sessions found, clear all
663
791
  setSessions([]);
664
792
  setActiveSessionId(null);
665
793
  logoutStore();
666
794
  setMinimalUser(null);
667
795
  await clearAllStorage();
668
-
669
- if (onAuthStateChange) {
670
- onAuthStateChange(null);
796
+ onAuthStateChange?.(null);
671
797
  }
672
798
  }
673
- }, [activeSessionId, oxyServices, user?.id, sessions, switchToSession, logoutStore, setMinimalUser, clearAllStorage, onAuthStateChange]);
799
+ }, [activeSessionId, oxyServices, user?.id, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapServerSessionsToClient]);
674
800
 
675
801
  // Device management methods
676
- const getDeviceSessions = useCallback(async (): Promise<any[]> => {
802
+ const getDeviceSessions = useCallback(async (): Promise<Array<{
803
+ sessionId: string;
804
+ deviceId: string;
805
+ deviceName?: string;
806
+ lastActive?: string;
807
+ expiresAt?: string;
808
+ }>> => {
677
809
  if (!activeSessionId) throw new Error('No active session');
678
-
679
810
  try {
680
811
  return await oxyServices.getDeviceSessions(activeSessionId);
681
812
  } catch (error) {
682
- console.error('Get device sessions error:', error);
813
+ const errorMessage = error instanceof Error ? error.message : 'Failed to get device sessions';
814
+ onError?.({ message: errorMessage, code: 'GET_DEVICE_SESSIONS_ERROR', status: 500 });
683
815
  throw error;
684
816
  }
685
- }, [activeSessionId, oxyServices]);
817
+ }, [activeSessionId, oxyServices, onError]);
686
818
 
687
819
  const logoutAllDeviceSessions = useCallback(async (): Promise<void> => {
688
820
  if (!activeSessionId) throw new Error('No active session');
689
821
 
690
822
  try {
691
823
  await oxyServices.logoutAllDeviceSessions(activeSessionId);
692
-
693
- // Clear all local sessions since we logged out from all devices
694
824
  setSessions([]);
695
825
  setActiveSessionId(null);
696
826
  logoutStore();
697
827
  setMinimalUser(null);
698
828
  await clearAllStorage();
699
-
700
- if (onAuthStateChange) {
701
- onAuthStateChange(null);
702
- }
829
+ onAuthStateChange?.(null);
703
830
  } catch (error) {
704
- console.error('Logout all device sessions error:', error);
831
+ const errorMessage = error instanceof Error ? error.message : 'Failed to logout all device sessions';
832
+ onError?.({ message: errorMessage, code: 'LOGOUT_ALL_DEVICES_ERROR', status: 500 });
705
833
  throw error;
706
834
  }
707
- }, [activeSessionId, oxyServices, logoutStore, setMinimalUser, clearAllStorage, onAuthStateChange]);
835
+ }, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
708
836
 
709
837
  const updateDeviceName = useCallback(async (deviceName: string): Promise<void> => {
710
838
  if (!activeSessionId) throw new Error('No active session');
711
839
 
712
840
  try {
713
841
  await oxyServices.updateDeviceName(activeSessionId, deviceName);
714
-
715
- // Update local device info
716
842
  await DeviceManager.updateDeviceName(deviceName);
717
843
  } catch (error) {
718
- console.error('Update device name error:', error);
844
+ const errorMessage = error instanceof Error ? error.message : 'Failed to update device name';
845
+ onError?.({ message: errorMessage, code: 'UPDATE_DEVICE_NAME_ERROR', status: 500 });
719
846
  throw error;
720
847
  }
721
- }, [activeSessionId, oxyServices]);
848
+ }, [activeSessionId, oxyServices, onError]);
722
849
 
723
850
  // Language management method
724
851
  const setLanguage = useCallback(async (languageId: string): Promise<void> => {
725
852
  if (!storage) throw new Error('Storage not initialized');
726
853
 
727
854
  try {
728
- // Save language preference
729
855
  await storage.setItem(keys.language, languageId);
730
856
  setCurrentLanguage(languageId);
731
-
732
- console.log(`Language changed to ${languageId}`);
733
-
734
- // TODO: Here you can add any additional logic needed for app-wide language updates
735
- // such as updating i18n configuration, refreshing translations, etc.
736
-
737
857
  } catch (error) {
738
- console.error('Error saving language preference:', error);
858
+ const errorMessage = error instanceof Error ? error.message : 'Failed to save language preference';
859
+ onError?.({ message: errorMessage, code: 'LANGUAGE_SAVE_ERROR', status: 500 });
739
860
  throw error;
740
861
  }
741
- }, [storage, keys.language]);
862
+ }, [storage, keys.language, onError]);
742
863
 
743
864
  // Bottom sheet control methods
744
865
  const showBottomSheet = useCallback((screenOrConfig?: RouteName | string | { screen: RouteName | string; props?: Record<string, any> }) => {
@@ -754,9 +875,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
754
875
  } else if (bottomSheetRef.current.present) {
755
876
  if (__DEV__) console.log('Presenting bottom sheet');
756
877
  bottomSheetRef.current.present();
757
- } else {
878
+ } else if (__DEV__) {
758
879
  console.warn('No expand or present method available on bottomSheetRef');
759
- if (__DEV__) console.log('Available methods on bottomSheetRef.current:', Object.keys(bottomSheetRef.current as any));
760
880
  }
761
881
 
762
882
  // Then navigate to the specified screen if provided
@@ -774,10 +894,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
774
894
  }
775
895
  }, 100);
776
896
  }
777
- } else {
778
- console.warn('bottomSheetRef is not available');
779
- console.warn('To fix this, ensure you pass a bottomSheetRef to OxyProvider:');
780
- console.warn('<OxyProvider baseURL="..." bottomSheetRef={yourBottomSheetRef}>');
897
+ } else if (__DEV__) {
898
+ console.warn('bottomSheetRef is not available. Pass a bottomSheetRef to OxyProvider.');
781
899
  }
782
900
  }, [bottomSheetRef]);
783
901
 
@@ -811,11 +929,15 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
811
929
  if (mod && typeof mod.useFollow === 'function') {
812
930
  return mod.useFollow(userId);
813
931
  }
814
- console.warn('useFollow module did not export a function as expected');
815
- return { isFollowing: false, isLoading: false, error: null, toggleFollow: async () => { }, setFollowStatus: () => { }, fetchStatus: async () => { }, clearError: () => { }, followerCount: null, followingCount: null, isLoadingCounts: false, fetchUserCounts: async () => { }, setFollowerCount: () => { }, setFollowingCount: () => { } } as any;
932
+ if (__DEV__) {
933
+ console.warn('useFollow module did not export a function as expected');
934
+ }
935
+ return createEmptyFollowHook()(userId);
816
936
  } catch (e) {
817
- console.warn('Failed to dynamically load useFollow hook:', e);
818
- return { isFollowing: false, isLoading: false, error: null, toggleFollow: async () => { }, setFollowStatus: () => { }, fetchStatus: async () => { }, clearError: () => { }, followerCount: null, followingCount: null, isLoadingCounts: false, fetchUserCounts: async () => { }, setFollowerCount: () => { }, setFollowingCount: () => { } } as any;
937
+ if (__DEV__) {
938
+ console.warn('Failed to dynamically load useFollow hook:', e);
939
+ }
940
+ return createEmptyFollowHook()(userId);
819
941
  }
820
942
  };
821
943
 
@@ -826,6 +948,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
826
948
  activeSessionId,
827
949
  isAuthenticated,
828
950
  isLoading,
951
+ isTokenReady: tokenReady,
829
952
  error,
830
953
  currentLanguage,
831
954
  login,
@@ -852,6 +975,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
852
975
  activeSessionId,
853
976
  isAuthenticated,
854
977
  isLoading,
978
+ tokenReady,
855
979
  error,
856
980
  currentLanguage,
857
981
  login,
@@ -872,13 +996,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
872
996
  hideBottomSheet,
873
997
  ]);
874
998
 
875
- // Wrap children rendering to block until token is ready
876
- if (!tokenReady) {
877
- return <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
878
- <Text>Loading authentication...</Text>
879
- </View>;
880
- }
881
-
999
+ // Always render children - let the consuming app decide how to handle token loading state
882
1000
  return (
883
1001
  <OxyContext.Provider value={contextValue}>
884
1002
  {children}
@@ -899,3 +1017,4 @@ export const useOxy = (): OxyContextState => {
899
1017
  };
900
1018
 
901
1019
  export default OxyContext;
1020
+