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