@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.
- package/lib/commonjs/core/OxyServices.js +86 -8
- package/lib/commonjs/core/OxyServices.js.map +1 -1
- package/lib/commonjs/i18n/index.js +37 -1
- package/lib/commonjs/i18n/index.js.map +1 -1
- package/lib/commonjs/i18n/locales/ar-SA.json +128 -0
- package/lib/commonjs/i18n/locales/ca-ES.json +128 -0
- package/lib/commonjs/i18n/locales/de-DE.json +128 -0
- package/lib/commonjs/i18n/locales/en-US.json +85 -12
- package/lib/commonjs/i18n/locales/es-ES.json +58 -6
- package/lib/commonjs/i18n/locales/fr-FR.json +128 -0
- package/lib/commonjs/i18n/locales/it-IT.json +128 -0
- package/lib/commonjs/i18n/locales/ja-JP.json +127 -0
- package/lib/commonjs/i18n/locales/ko-KR.json +128 -0
- package/lib/commonjs/i18n/locales/pt-PT.json +128 -0
- package/lib/commonjs/i18n/locales/zh-CN.json +128 -0
- package/lib/commonjs/ui/components/FontLoader.js +22 -42
- package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +5 -8
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/StepBasedScreen.js +64 -44
- package/lib/commonjs/ui/components/StepBasedScreen.js.map +1 -1
- package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +14 -35
- package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
- package/lib/commonjs/ui/components/internal/PinInput.js +2 -2
- package/lib/commonjs/ui/components/internal/PinInput.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +434 -321
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +56 -5
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js +43 -39
- package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +139 -125
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +2 -4
- package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js +45 -25
- package/lib/commonjs/ui/screens/steps/RecoverRequestStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js +88 -53
- package/lib/commonjs/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js +79 -58
- package/lib/commonjs/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js +61 -52
- package/lib/commonjs/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js +220 -31
- package/lib/commonjs/ui/screens/steps/SignInPasswordStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignInTotpStep.js +77 -50
- package/lib/commonjs/ui/screens/steps/SignInTotpStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js +527 -66
- package/lib/commonjs/ui/screens/steps/SignInUsernameStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js +55 -30
- package/lib/commonjs/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js +64 -46
- package/lib/commonjs/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js +84 -146
- package/lib/commonjs/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
- package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js +113 -34
- package/lib/commonjs/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
- package/lib/commonjs/ui/stores/authStore.js +16 -20
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/commonjs/ui/styles/authStyles.js +2 -1
- package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
- package/lib/commonjs/ui/styles/index.js +11 -0
- package/lib/commonjs/ui/styles/index.js.map +1 -1
- package/lib/commonjs/ui/styles/spacing.js +51 -0
- package/lib/commonjs/ui/styles/spacing.js.map +1 -0
- package/lib/commonjs/utils/validationUtils.js +1 -1
- package/lib/module/core/OxyServices.js +86 -8
- package/lib/module/core/OxyServices.js.map +1 -1
- package/lib/module/i18n/index.js +37 -1
- package/lib/module/i18n/index.js.map +1 -1
- package/lib/module/i18n/locales/ar-SA.json +128 -0
- package/lib/module/i18n/locales/ca-ES.json +128 -0
- package/lib/module/i18n/locales/de-DE.json +128 -0
- package/lib/module/i18n/locales/en-US.json +85 -12
- package/lib/module/i18n/locales/es-ES.json +58 -6
- package/lib/module/i18n/locales/fr-FR.json +128 -0
- package/lib/module/i18n/locales/it-IT.json +128 -0
- package/lib/module/i18n/locales/ja-JP.json +127 -0
- package/lib/module/i18n/locales/ko-KR.json +128 -0
- package/lib/module/i18n/locales/pt-PT.json +128 -0
- package/lib/module/i18n/locales/zh-CN.json +128 -0
- package/lib/module/ui/components/FontLoader.js +23 -43
- package/lib/module/ui/components/FontLoader.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +6 -8
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/StepBasedScreen.js +65 -45
- package/lib/module/ui/components/StepBasedScreen.js.map +1 -1
- package/lib/module/ui/components/internal/GroupedPillButtons.js +14 -35
- package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
- package/lib/module/ui/components/internal/PinInput.js +2 -2
- package/lib/module/ui/components/internal/PinInput.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +434 -321
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +56 -5
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInScreen.js +44 -40
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +138 -126
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/screens/internal/SignInUsernameStep.js +2 -4
- package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
- package/lib/module/ui/screens/steps/RecoverRequestStep.js +45 -25
- package/lib/module/ui/screens/steps/RecoverRequestStep.js.map +1 -1
- package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js +89 -54
- package/lib/module/ui/screens/steps/RecoverResetPasswordStep.js.map +1 -1
- package/lib/module/ui/screens/steps/RecoverSuccessStep.js +80 -59
- package/lib/module/ui/screens/steps/RecoverSuccessStep.js.map +1 -1
- package/lib/module/ui/screens/steps/RecoverVerifyStep.js +62 -53
- package/lib/module/ui/screens/steps/RecoverVerifyStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignInPasswordStep.js +221 -32
- package/lib/module/ui/screens/steps/SignInPasswordStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignInTotpStep.js +78 -51
- package/lib/module/ui/screens/steps/SignInTotpStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignInUsernameStep.js +530 -68
- package/lib/module/ui/screens/steps/SignInUsernameStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignUpIdentityStep.js +55 -30
- package/lib/module/ui/screens/steps/SignUpIdentityStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignUpSecurityStep.js +65 -47
- package/lib/module/ui/screens/steps/SignUpSecurityStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignUpSummaryStep.js +84 -146
- package/lib/module/ui/screens/steps/SignUpSummaryStep.js.map +1 -1
- package/lib/module/ui/screens/steps/SignUpWelcomeStep.js +114 -35
- package/lib/module/ui/screens/steps/SignUpWelcomeStep.js.map +1 -1
- package/lib/module/ui/stores/authStore.js +16 -20
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/module/ui/styles/authStyles.js +2 -1
- package/lib/module/ui/styles/authStyles.js.map +1 -1
- package/lib/module/ui/styles/index.js +1 -0
- package/lib/module/ui/styles/index.js.map +1 -1
- package/lib/module/ui/styles/spacing.js +48 -0
- package/lib/module/ui/styles/spacing.js.map +1 -0
- package/lib/module/utils/validationUtils.js +1 -1
- package/lib/typescript/core/OxyServices.d.ts +38 -2
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/i18n/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/FontLoader.d.ts +3 -3
- package/lib/typescript/ui/components/FontLoader.d.ts.map +1 -1
- package/lib/typescript/ui/components/OxyProvider.d.ts +2 -2
- package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/ui/components/StepBasedScreen.d.ts.map +1 -1
- package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts +10 -0
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/RecoverRequestStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/RecoverResetPasswordStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/RecoverSuccessStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/RecoverVerifyStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts +2 -0
- package/lib/typescript/ui/screens/steps/SignInPasswordStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignInTotpStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignInUsernameStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignUpIdentityStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignUpSecurityStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignUpSummaryStep.d.ts.map +1 -1
- package/lib/typescript/ui/screens/steps/SignUpWelcomeStep.d.ts.map +1 -1
- package/lib/typescript/ui/stores/authStore.d.ts +7 -3
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/ui/styles/authStyles.d.ts +1 -0
- package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -1
- package/lib/typescript/ui/styles/index.d.ts +1 -0
- package/lib/typescript/ui/styles/index.d.ts.map +1 -1
- package/lib/typescript/ui/styles/spacing.d.ts +43 -0
- package/lib/typescript/ui/styles/spacing.d.ts.map +1 -0
- package/lib/typescript/utils/validationUtils.d.ts +1 -1
- package/package.json +1 -1
- package/src/core/OxyServices.ts +96 -10
- package/src/i18n/index.ts +36 -0
- package/src/i18n/locales/ar-SA.json +128 -0
- package/src/i18n/locales/ca-ES.json +128 -0
- package/src/i18n/locales/de-DE.json +128 -0
- package/src/i18n/locales/en-US.json +85 -12
- package/src/i18n/locales/es-ES.json +58 -6
- package/src/i18n/locales/fr-FR.json +128 -0
- package/src/i18n/locales/it-IT.json +128 -0
- package/src/i18n/locales/ja-JP.json +127 -0
- package/src/i18n/locales/ko-KR.json +128 -0
- package/src/i18n/locales/pt-PT.json +128 -0
- package/src/i18n/locales/zh-CN.json +128 -0
- package/src/ui/components/FontLoader.tsx +17 -37
- package/src/ui/components/OxyProvider.tsx +14 -13
- package/src/ui/components/StepBasedScreen.tsx +66 -43
- package/src/ui/components/internal/GroupedPillButtons.tsx +15 -31
- package/src/ui/components/internal/PinInput.tsx +2 -2
- package/src/ui/context/OxyContext.tsx +404 -285
- package/src/ui/screens/FileManagementScreen.tsx +81 -4
- package/src/ui/screens/SignInScreen.tsx +59 -36
- package/src/ui/screens/WelcomeNewUserScreen.tsx +102 -91
- package/src/ui/screens/internal/SignInUsernameStep.tsx +1 -1
- package/src/ui/screens/steps/RecoverRequestStep.tsx +34 -24
- package/src/ui/screens/steps/RecoverResetPasswordStep.tsx +65 -36
- package/src/ui/screens/steps/RecoverSuccessStep.tsx +71 -47
- package/src/ui/screens/steps/RecoverVerifyStep.tsx +60 -50
- package/src/ui/screens/steps/SignInPasswordStep.tsx +191 -29
- package/src/ui/screens/steps/SignInTotpStep.tsx +68 -34
- package/src/ui/screens/steps/SignInUsernameStep.tsx +586 -57
- package/src/ui/screens/steps/SignUpIdentityStep.tsx +49 -35
- package/src/ui/screens/steps/SignUpSecurityStep.tsx +56 -39
- package/src/ui/screens/steps/SignUpSummaryStep.tsx +99 -89
- package/src/ui/screens/steps/SignUpWelcomeStep.tsx +88 -20
- package/src/ui/stores/authStore.ts +15 -19
- package/src/ui/styles/authStyles.ts +2 -1
- package/src/ui/styles/index.ts +1 -0
- package/src/ui/styles/spacing.ts +46 -0
- 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
|
-
//
|
|
194
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
useAuthStore.setState({ error:
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
+
if (__DEV__) {
|
|
340
|
+
console.error('Auth init error', e);
|
|
341
|
+
}
|
|
277
342
|
await clearAllStorage();
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
389
|
-
setActiveSessionId((response as SessionLoginResponse).sessionId);
|
|
390
|
-
await saveActiveSessionId((response as SessionLoginResponse).sessionId);
|
|
492
|
+
const sessionResponse = response as SessionLoginResponse;
|
|
391
493
|
|
|
392
|
-
|
|
393
|
-
await oxyServices.
|
|
494
|
+
await oxyServices.getTokenBySession(sessionResponse.sessionId);
|
|
495
|
+
const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
394
496
|
|
|
395
|
-
//
|
|
396
|
-
|
|
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(
|
|
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
|
-
|
|
412
|
-
|
|
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
|
|
417
|
-
|
|
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,
|
|
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
|
-
|
|
455
|
-
|
|
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,
|
|
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
|
-
|
|
465
|
-
useAuthStore.setState({ error:
|
|
466
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
if (onAuthStateChange) {
|
|
489
|
-
onAuthStateChange(null);
|
|
490
|
-
console.log('Auth state change callback called');
|
|
491
|
-
}
|
|
633
|
+
onAuthStateChange?.(null);
|
|
492
634
|
} catch (error) {
|
|
493
|
-
|
|
494
|
-
useAuthStore.setState({ 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,
|
|
640
|
+
}, [activeSessionId, oxyServices, logoutStore, clearAllStorage, onAuthStateChange, onError]);
|
|
498
641
|
|
|
499
|
-
//
|
|
500
|
-
|
|
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
|
-
|
|
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
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
711
|
+
onAuthStateChange?.(fullUser);
|
|
592
712
|
return fullUser;
|
|
593
|
-
} catch (error
|
|
594
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
if (!activeSessionId) {
|
|
616
|
-
console.log('refreshSessions: No activeSessionId, returning');
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
735
|
+
if (!activeSessionId) return;
|
|
619
736
|
|
|
620
737
|
try {
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
await oxyServices.validateSession(session.sessionId, {
|
|
777
|
+
const validation = await oxyServices.validateSession(session.sessionId, {
|
|
649
778
|
useHeaderValidation: true
|
|
650
779
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
continue;
|
|
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
|
-
//
|
|
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,
|
|
799
|
+
}, [activeSessionId, oxyServices, user?.id, sessions, switchToSession, logoutStore, clearAllStorage, onAuthStateChange, mapServerSessionsToClient]);
|
|
674
800
|
|
|
675
801
|
// Device management methods
|
|
676
|
-
const getDeviceSessions = useCallback(async (): Promise<
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
-
|
|
818
|
-
|
|
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
|
-
//
|
|
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
|
+
|