@oxyhq/services 5.17.8 → 5.17.10
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/crypto/index.js +0 -23
- package/lib/commonjs/crypto/index.js.map +1 -1
- package/lib/commonjs/index.js +0 -15
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/Icon.js.map +1 -1
- package/lib/commonjs/ui/components/IconButton/utils.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/Adornment/utils.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/helpers.js.map +1 -1
- package/lib/commonjs/ui/components/TouchableRipple/utils.js.map +1 -1
- package/lib/commonjs/ui/components/Typography/AnimatedText.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +20 -35
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +41 -118
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +2 -26
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/authStore.js +33 -13
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +2 -32
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/crypto/index.js +4 -6
- package/lib/module/crypto/index.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/Icon.js.map +1 -1
- package/lib/module/ui/components/IconButton/utils.js.map +1 -1
- package/lib/module/ui/components/TextField/Adornment/utils.js.map +1 -1
- package/lib/module/ui/components/TextField/helpers.js.map +1 -1
- package/lib/module/ui/components/TouchableRipple/utils.js.map +1 -1
- package/lib/module/ui/components/Typography/AnimatedText.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +20 -36
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +41 -118
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +2 -26
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +0 -1
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/stores/authStore.js +33 -13
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +2 -32
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/crypto/index.d.ts +2 -5
- package/lib/typescript/crypto/index.d.ts.map +1 -1
- package/lib/typescript/crypto/types.d.ts +2 -2
- package/lib/typescript/index.d.ts +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/IconButton/utils.d.ts +1 -1
- package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts +1 -1
- package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts.map +1 -1
- package/lib/typescript/ui/components/TextField/helpers.d.ts +6 -6
- package/lib/typescript/ui/components/types.d.ts +0 -4
- package/lib/typescript/ui/components/types.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContextBase.d.ts +2 -2
- package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +2 -9
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/ui/stores/authStore.d.ts +5 -3
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/ui/utils/avatarUtils.d.ts +0 -2
- package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/crypto/index.ts +3 -11
- package/src/crypto/types.ts +2 -2
- package/src/index.ts +6 -11
- package/src/ui/components/Icon.tsx +1 -1
- package/src/ui/components/IconButton/utils.ts +1 -1
- package/src/ui/components/TextField/Adornment/utils.ts +2 -2
- package/src/ui/components/TextField/helpers.tsx +8 -8
- package/src/ui/components/TouchableRipple/utils.ts +2 -2
- package/src/ui/components/Typography/AnimatedText.tsx +2 -2
- package/src/ui/components/types.tsx +0 -6
- package/src/ui/context/OxyContext.tsx +22 -27
- package/src/ui/context/OxyContextBase.tsx +4 -4
- package/src/ui/context/hooks/useAuthOperations.ts +61 -140
- package/src/ui/hooks/useSessionSocket.ts +3 -21
- package/src/ui/screens/OxyAuthScreen.tsx +1 -1
- package/src/ui/stores/authStore.ts +39 -18
- package/src/ui/utils/avatarUtils.ts +4 -36
- package/lib/commonjs/crypto/keyManager.js +0 -356
- package/lib/commonjs/crypto/keyManager.js.map +0 -1
- package/lib/commonjs/crypto/signatureService.js +0 -269
- package/lib/commonjs/crypto/signatureService.js.map +0 -1
- package/lib/module/crypto/keyManager.js +0 -353
- package/lib/module/crypto/keyManager.js.map +0 -1
- package/lib/module/crypto/signatureService.js +0 -266
- package/lib/module/crypto/signatureService.js.map +0 -1
- package/lib/typescript/crypto/keyManager.d.ts +0 -80
- package/lib/typescript/crypto/keyManager.d.ts.map +0 -1
- package/lib/typescript/crypto/signatureService.d.ts +0 -77
- package/lib/typescript/crypto/signatureService.d.ts.map +0 -1
- package/src/crypto/keyManager.ts +0 -379
- package/src/crypto/signatureService.ts +0 -301
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
type OxyContextProviderProps,
|
|
42
42
|
} from './OxyContextBase';
|
|
43
43
|
|
|
44
|
-
// Re-export for backwards compatibility
|
|
45
44
|
export { OxyContext, useOxy, type OxyContextState, type OxyContextProviderProps };
|
|
46
45
|
|
|
47
46
|
let cachedUseFollowHook: UseFollowHook | null = null;
|
|
@@ -98,16 +97,20 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
98
97
|
|
|
99
98
|
const {
|
|
100
99
|
isAuthenticated,
|
|
100
|
+
isOnline,
|
|
101
101
|
isLoading,
|
|
102
102
|
error,
|
|
103
|
+
setOnline,
|
|
103
104
|
loginSuccess,
|
|
104
105
|
loginFailure,
|
|
105
106
|
logoutStore,
|
|
106
107
|
} = useAuthStore(
|
|
107
108
|
useShallow((state: AuthState) => ({
|
|
108
109
|
isAuthenticated: state.isAuthenticated,
|
|
110
|
+
isOnline: state.isOnline,
|
|
109
111
|
isLoading: state.isLoading,
|
|
110
112
|
error: state.error,
|
|
113
|
+
setOnline: state.setOnline,
|
|
111
114
|
loginSuccess: state.loginSuccess,
|
|
112
115
|
loginFailure: state.loginFailure,
|
|
113
116
|
logoutStore: state.logout,
|
|
@@ -132,9 +135,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
132
135
|
|
|
133
136
|
const { storage, isReady: isStorageReady } = useStorage({ onError, logger });
|
|
134
137
|
|
|
135
|
-
// Offline queuing is now handled by TanStack Query mutations
|
|
136
|
-
// No need for custom offline queue
|
|
137
|
-
|
|
138
138
|
const {
|
|
139
139
|
currentLanguage,
|
|
140
140
|
metadata: currentLanguageMetadata,
|
|
@@ -176,10 +176,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
176
176
|
queryClient,
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
// Get current user from query (no persistent cache - always fetch fresh)
|
|
180
|
-
// Services never caches profile - always fetch from backend
|
|
181
|
-
// Note: We use useQuery directly here instead of useCurrentUser to avoid
|
|
182
|
-
// circular dependency (useCurrentUser calls useOxy, but we're inside the provider)
|
|
183
179
|
const { data: userData } = useQuery({
|
|
184
180
|
queryKey: queryKeys.accounts.current(),
|
|
185
181
|
queryFn: async () => {
|
|
@@ -189,14 +185,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
189
185
|
return await oxyServices.getUserBySession(activeSessionId);
|
|
190
186
|
},
|
|
191
187
|
enabled: isAuthenticated && !!activeSessionId && tokenReady,
|
|
192
|
-
staleTime: 0,
|
|
193
|
-
gcTime: 0,
|
|
194
|
-
retry: false,
|
|
188
|
+
staleTime: 0,
|
|
189
|
+
gcTime: 0,
|
|
190
|
+
retry: false,
|
|
195
191
|
});
|
|
196
192
|
const user = userData ?? null;
|
|
197
193
|
|
|
198
194
|
const {
|
|
199
|
-
|
|
195
|
+
completeSignIn,
|
|
200
196
|
logout,
|
|
201
197
|
logoutAll,
|
|
202
198
|
} = useAuthOperations({
|
|
@@ -219,13 +215,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
219
215
|
logger,
|
|
220
216
|
});
|
|
221
217
|
|
|
222
|
-
// Clear all account data when logging out (for accounts app)
|
|
223
|
-
// In accounts app, identity = account, so losing identity means losing everything
|
|
224
218
|
const clearAllAccountData = useCallback(async (): Promise<void> => {
|
|
225
|
-
// Clear TanStack Query cache (in-memory)
|
|
226
219
|
queryClient.clear();
|
|
227
220
|
|
|
228
|
-
// Clear persisted query cache
|
|
229
221
|
if (storage) {
|
|
230
222
|
try {
|
|
231
223
|
await clearQueryCache(storage);
|
|
@@ -234,18 +226,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
234
226
|
}
|
|
235
227
|
}
|
|
236
228
|
|
|
237
|
-
// Clear session state (sessions, activeSessionId, storage)
|
|
238
229
|
await clearSessionState();
|
|
239
230
|
|
|
240
|
-
// Reset account store
|
|
241
231
|
useAccountStore.getState().reset();
|
|
242
232
|
|
|
243
|
-
// Clear HTTP service cache
|
|
244
233
|
oxyServices.clearCache();
|
|
245
234
|
}, [queryClient, storage, clearSessionState, logger, oxyServices]);
|
|
246
235
|
|
|
247
|
-
// Network reconnect - TanStack Query automatically retries mutations on reconnect
|
|
248
|
-
// Network reconnect - TanStack Query automatically retries mutations on reconnect
|
|
249
236
|
useEffect(() => {
|
|
250
237
|
if (!storage) return;
|
|
251
238
|
|
|
@@ -268,14 +255,22 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
268
255
|
|
|
269
256
|
// If we were offline and now we're online
|
|
270
257
|
if (wasOffline) {
|
|
271
|
-
logger('Network reconnected');
|
|
258
|
+
logger('Network reconnected - setting online state');
|
|
259
|
+
setOnline(true);
|
|
272
260
|
// TanStack Query will automatically retry pending mutations
|
|
261
|
+
// Session management will handle token refresh if needed
|
|
273
262
|
wasOffline = false;
|
|
263
|
+
} else {
|
|
264
|
+
// We're online and were already online
|
|
265
|
+
setOnline(true);
|
|
274
266
|
}
|
|
275
267
|
} catch (error) {
|
|
276
268
|
// Network check failed - we're offline
|
|
277
|
-
if (!wasOffline
|
|
278
|
-
|
|
269
|
+
if (!wasOffline) {
|
|
270
|
+
if (__DEV__) {
|
|
271
|
+
logger('Network appears offline - suspending authentication');
|
|
272
|
+
}
|
|
273
|
+
setOnline(false); // This will set isAuthenticated to false
|
|
279
274
|
}
|
|
280
275
|
wasOffline = true;
|
|
281
276
|
} finally {
|
|
@@ -291,7 +286,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
291
286
|
clearTimeout(checkTimeout);
|
|
292
287
|
}
|
|
293
288
|
};
|
|
294
|
-
}, [oxyServices, storage, logger]);
|
|
289
|
+
}, [oxyServices, storage, logger, setOnline]);
|
|
295
290
|
|
|
296
291
|
const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
|
|
297
292
|
oxyServices,
|
|
@@ -503,7 +498,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
503
498
|
currentLanguageMetadata,
|
|
504
499
|
currentLanguageName,
|
|
505
500
|
currentNativeLanguageName,
|
|
506
|
-
|
|
501
|
+
completeSignIn,
|
|
507
502
|
logout,
|
|
508
503
|
logoutAll,
|
|
509
504
|
switchSession: switchSessionForContext,
|
|
@@ -522,7 +517,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
522
517
|
}), [
|
|
523
518
|
activeSessionId,
|
|
524
519
|
currentDeviceId,
|
|
525
|
-
|
|
520
|
+
completeSignIn,
|
|
526
521
|
currentLanguage,
|
|
527
522
|
currentLanguageMetadata,
|
|
528
523
|
currentLanguageName,
|
|
@@ -2,11 +2,10 @@ import { createContext, useContext } from 'react';
|
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { OxyServices } from '../../core';
|
|
4
4
|
import type { User, ApiError } from '../../models/interfaces';
|
|
5
|
-
import type { ClientSession } from '../../models/session';
|
|
5
|
+
import type { ClientSession, SessionLoginResponse } from '../../models/session';
|
|
6
6
|
import type { UseFollowHook } from '../hooks/useFollow.types';
|
|
7
7
|
import type { useLanguageManagement } from '../hooks/useLanguageManagement';
|
|
8
8
|
import type { RouteName } from '../navigation/routes';
|
|
9
|
-
import type { BackupData } from '../../crypto';
|
|
10
9
|
|
|
11
10
|
export interface OxyContextState {
|
|
12
11
|
user: User | null;
|
|
@@ -24,8 +23,9 @@ export interface OxyContextState {
|
|
|
24
23
|
currentNativeLanguageName: string;
|
|
25
24
|
|
|
26
25
|
// Authentication (Services SDK only handles tokens/sessions, NOT identity)
|
|
27
|
-
// Identity management (
|
|
28
|
-
|
|
26
|
+
// Identity management (KeyManager, SignatureService, challenge signing) belongs ONLY in Accounts app
|
|
27
|
+
// completeSignIn accepts already-verified session data from Accounts app
|
|
28
|
+
completeSignIn: (sessionResponse: SessionLoginResponse) => Promise<User>;
|
|
29
29
|
|
|
30
30
|
// Session management
|
|
31
31
|
logout: (targetSessionId?: string) => Promise<void>;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import type { ApiError, User } from '../../../models/interfaces';
|
|
3
3
|
import type { AuthState } from '../../stores/authStore';
|
|
4
|
-
import type { ClientSession } from '../../../models/session';
|
|
5
|
-
import { DeviceManager } from '../../../utils/deviceManager';
|
|
4
|
+
import type { ClientSession, SessionLoginResponse } from '../../../models/session';
|
|
6
5
|
import { fetchSessionsWithFallback } from '../../utils/sessionHelpers';
|
|
7
6
|
import { handleAuthError, isInvalidSessionError } from '../../utils/errorHandlers';
|
|
8
7
|
import type { StorageInterface } from '../../utils/storageHelpers';
|
|
9
8
|
import type { OxyServices } from '../../../core';
|
|
10
|
-
import { KeyManager, SignatureService } from '../../../crypto';
|
|
11
9
|
|
|
12
10
|
export interface UseAuthOperationsOptions {
|
|
13
11
|
oxyServices: OxyServices;
|
|
@@ -30,23 +28,15 @@ export interface UseAuthOperationsOptions {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export interface UseAuthOperationsResult {
|
|
33
|
-
|
|
34
|
-
signIn: (deviceName?: string) => Promise<User>;
|
|
35
|
-
/** Logout from current session */
|
|
31
|
+
completeSignIn: (sessionResponse: SessionLoginResponse) => Promise<User>;
|
|
36
32
|
logout: (targetSessionId?: string) => Promise<void>;
|
|
37
|
-
/** Logout from all sessions */
|
|
38
33
|
logoutAll: () => Promise<void>;
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
|
|
42
36
|
const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR';
|
|
43
37
|
const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
|
|
44
38
|
|
|
45
39
|
|
|
46
|
-
/**
|
|
47
|
-
* Authentication operations using public key cryptography.
|
|
48
|
-
* No passwords required - identity is based on ECDSA key pairs.
|
|
49
|
-
*/
|
|
50
40
|
export const useAuthOperations = ({
|
|
51
41
|
oxyServices,
|
|
52
42
|
storage,
|
|
@@ -67,149 +57,66 @@ export const useAuthOperations = ({
|
|
|
67
57
|
logger,
|
|
68
58
|
}: UseAuthOperationsOptions): UseAuthOperationsResult => {
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const performSignIn = useCallback(
|
|
74
|
-
async (publicKey: string, deviceName?: string): Promise<User> => {
|
|
75
|
-
const deviceFingerprintObj = DeviceManager.getDeviceFingerprint();
|
|
76
|
-
const deviceFingerprint = JSON.stringify(deviceFingerprintObj);
|
|
77
|
-
const deviceInfo = await DeviceManager.getDeviceInfo();
|
|
78
|
-
const defaultDeviceName = deviceInfo.deviceName || DeviceManager.getDefaultDeviceName();
|
|
79
|
-
const finalDeviceName = deviceName || defaultDeviceName;
|
|
80
|
-
|
|
81
|
-
// Look up user by public key
|
|
82
|
-
const userLookup = await oxyServices.getUserByPublicKey(publicKey);
|
|
83
|
-
const userId = userLookup.id;
|
|
84
|
-
|
|
85
|
-
// Request challenge
|
|
86
|
-
const challengeResponse = await oxyServices.requestChallenge(userId);
|
|
87
|
-
const challenge = challengeResponse.challenge;
|
|
60
|
+
const completeSignIn = useCallback(
|
|
61
|
+
async (sessionResponse: SessionLoginResponse): Promise<User> => {
|
|
62
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
88
63
|
|
|
89
|
-
|
|
90
|
-
const { challenge: signature, timestamp } = await SignatureService.signChallenge(challenge);
|
|
64
|
+
setAuthState({ isLoading: true, error: null });
|
|
91
65
|
|
|
92
|
-
// Verify and create session
|
|
93
|
-
let sessionResponse;
|
|
94
66
|
try {
|
|
95
|
-
sessionResponse
|
|
96
|
-
userId,
|
|
97
|
-
challenge,
|
|
98
|
-
signature,
|
|
99
|
-
timestamp,
|
|
100
|
-
finalDeviceName,
|
|
101
|
-
deviceFingerprint,
|
|
102
|
-
);
|
|
103
|
-
} catch (verifyError) {
|
|
104
|
-
if (__DEV__) {
|
|
105
|
-
console.error('[useAuthOperations] verifyChallenge failed:', {
|
|
106
|
-
error: verifyError,
|
|
107
|
-
userId,
|
|
108
|
-
challengeLength: challenge?.length,
|
|
109
|
-
signatureLength: signature?.length,
|
|
110
|
-
timestamp,
|
|
111
|
-
timeSinceChallenge: Date.now() - timestamp,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
throw verifyError;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Store tokens
|
|
118
|
-
oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
|
|
67
|
+
oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
|
|
119
68
|
|
|
120
|
-
|
|
121
|
-
const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
69
|
+
const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
122
70
|
|
|
123
|
-
|
|
124
|
-
let allDeviceSessions: ClientSession[] = [];
|
|
125
|
-
try {
|
|
126
|
-
allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
|
|
127
|
-
fallbackDeviceId: sessionResponse.deviceId,
|
|
128
|
-
fallbackUserId: fullUser.id,
|
|
129
|
-
logger,
|
|
130
|
-
});
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (__DEV__) {
|
|
133
|
-
console.warn('Failed to fetch device sessions after login:', error);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check for existing session for same user
|
|
138
|
-
const existingSession = allDeviceSessions.find(
|
|
139
|
-
(session) =>
|
|
140
|
-
session.userId?.toString() === fullUser.id?.toString() &&
|
|
141
|
-
session.sessionId !== sessionResponse.sessionId,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if (existingSession) {
|
|
145
|
-
// Logout duplicate session
|
|
71
|
+
let allDeviceSessions: ClientSession[] = [];
|
|
146
72
|
try {
|
|
147
|
-
await oxyServices
|
|
148
|
-
|
|
73
|
+
allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
|
|
74
|
+
fallbackDeviceId: sessionResponse.deviceId,
|
|
75
|
+
fallbackUserId: fullUser.id,
|
|
76
|
+
logger,
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
149
79
|
if (__DEV__) {
|
|
150
|
-
console.warn('Failed to
|
|
80
|
+
console.warn('Failed to fetch device sessions after login:', error);
|
|
151
81
|
}
|
|
152
82
|
}
|
|
153
|
-
await switchSession(existingSession.sessionId);
|
|
154
|
-
updateSessions(
|
|
155
|
-
allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
|
|
156
|
-
{ merge: false },
|
|
157
|
-
);
|
|
158
|
-
onAuthStateChange?.(fullUser);
|
|
159
|
-
return fullUser;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
setActiveSessionId(sessionResponse.sessionId);
|
|
163
|
-
await saveActiveSessionId(sessionResponse.sessionId);
|
|
164
|
-
updateSessions(allDeviceSessions, { merge: true });
|
|
165
|
-
|
|
166
|
-
await applyLanguagePreference(fullUser);
|
|
167
|
-
loginSuccess();
|
|
168
|
-
onAuthStateChange?.(fullUser);
|
|
169
|
-
|
|
170
|
-
return fullUser;
|
|
171
|
-
},
|
|
172
|
-
[
|
|
173
|
-
applyLanguagePreference,
|
|
174
|
-
logger,
|
|
175
|
-
loginSuccess,
|
|
176
|
-
onAuthStateChange,
|
|
177
|
-
oxyServices,
|
|
178
|
-
saveActiveSessionId,
|
|
179
|
-
setActiveSessionId,
|
|
180
|
-
switchSession,
|
|
181
|
-
updateSessions,
|
|
182
|
-
],
|
|
183
|
-
);
|
|
184
83
|
|
|
84
|
+
const existingSession = allDeviceSessions.find(
|
|
85
|
+
(session) =>
|
|
86
|
+
session.userId?.toString() === fullUser.id?.toString() &&
|
|
87
|
+
session.sessionId !== sessionResponse.sessionId,
|
|
88
|
+
);
|
|
185
89
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
90
|
+
if (existingSession) {
|
|
91
|
+
try {
|
|
92
|
+
await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
|
|
93
|
+
} catch (logoutError) {
|
|
94
|
+
if (__DEV__) {
|
|
95
|
+
console.warn('Failed to logout duplicate session:', logoutError);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await switchSession(existingSession.sessionId);
|
|
99
|
+
updateSessions(
|
|
100
|
+
allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
|
|
101
|
+
{ merge: false },
|
|
102
|
+
);
|
|
103
|
+
onAuthStateChange?.(fullUser);
|
|
104
|
+
return fullUser;
|
|
200
105
|
}
|
|
201
106
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
throw new Error('Identity is not registered. Please register your identity in the Accounts app before signing in.');
|
|
206
|
-
}
|
|
107
|
+
setActiveSessionId(sessionResponse.sessionId);
|
|
108
|
+
await saveActiveSessionId(sessionResponse.sessionId);
|
|
109
|
+
updateSessions(allDeviceSessions, { merge: true });
|
|
207
110
|
|
|
208
|
-
|
|
111
|
+
await applyLanguagePreference(fullUser);
|
|
112
|
+
loginSuccess();
|
|
113
|
+
onAuthStateChange?.(fullUser);
|
|
114
|
+
|
|
115
|
+
return fullUser;
|
|
209
116
|
} catch (error) {
|
|
210
117
|
const message = handleAuthError(error, {
|
|
211
118
|
defaultMessage: 'Sign in failed',
|
|
212
|
-
code:
|
|
119
|
+
code: 'COMPLETE_SIGNIN_ERROR',
|
|
213
120
|
onError,
|
|
214
121
|
setAuthError: (msg: string) => setAuthState({ error: msg }),
|
|
215
122
|
logger,
|
|
@@ -220,7 +127,21 @@ export const useAuthOperations = ({
|
|
|
220
127
|
setAuthState({ isLoading: false });
|
|
221
128
|
}
|
|
222
129
|
},
|
|
223
|
-
[
|
|
130
|
+
[
|
|
131
|
+
storage,
|
|
132
|
+
setAuthState,
|
|
133
|
+
oxyServices,
|
|
134
|
+
logger,
|
|
135
|
+
saveActiveSessionId,
|
|
136
|
+
setActiveSessionId,
|
|
137
|
+
switchSession,
|
|
138
|
+
updateSessions,
|
|
139
|
+
applyLanguagePreference,
|
|
140
|
+
loginSuccess,
|
|
141
|
+
onAuthStateChange,
|
|
142
|
+
onError,
|
|
143
|
+
loginFailure,
|
|
144
|
+
],
|
|
224
145
|
);
|
|
225
146
|
|
|
226
147
|
|
|
@@ -304,7 +225,7 @@ export const useAuthOperations = ({
|
|
|
304
225
|
}, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState]);
|
|
305
226
|
|
|
306
227
|
return {
|
|
307
|
-
|
|
228
|
+
completeSignIn,
|
|
308
229
|
logout,
|
|
309
230
|
logoutAll,
|
|
310
231
|
};
|
|
@@ -25,7 +25,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
25
25
|
const lastRegisteredSocketIdRef = useRef<string | null>(null);
|
|
26
26
|
const getAccessTokenRef = useRef(getAccessToken);
|
|
27
27
|
|
|
28
|
-
// Store callbacks in refs to avoid re-joining when they change
|
|
29
28
|
const refreshSessionsRef = useRef(refreshSessions);
|
|
30
29
|
const logoutRef = useRef(logout);
|
|
31
30
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
@@ -34,7 +33,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
34
33
|
const activeSessionIdRef = useRef(activeSessionId);
|
|
35
34
|
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
36
35
|
|
|
37
|
-
// Update refs when callbacks change
|
|
38
36
|
useEffect(() => {
|
|
39
37
|
refreshSessionsRef.current = refreshSessions;
|
|
40
38
|
logoutRef.current = logout;
|
|
@@ -48,7 +46,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
48
46
|
|
|
49
47
|
useEffect(() => {
|
|
50
48
|
if (!userId || !baseURL) {
|
|
51
|
-
// Clean up if userId or baseURL becomes invalid
|
|
52
49
|
if (socketRef.current) {
|
|
53
50
|
socketRef.current.disconnect();
|
|
54
51
|
socketRef.current = null;
|
|
@@ -56,14 +53,9 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
56
53
|
}
|
|
57
54
|
return;
|
|
58
55
|
}
|
|
59
|
-
|
|
60
|
-
// IMPORTANT: If userId is set but we have no token, defer socket creation
|
|
61
|
-
// This prevents socket reconnection race condition during auth flow
|
|
62
|
-
// (e.g., when userId changes but tokens aren't set yet in transfer flow)
|
|
63
56
|
const freshToken = getAccessTokenRef.current();
|
|
64
57
|
if (!freshToken) {
|
|
65
58
|
logger.debug('Deferring socket creation - no access token available yet', { component: 'useSessionSocket', userId });
|
|
66
|
-
// Disconnect existing socket if it exists but we have no token
|
|
67
59
|
if (socketRef.current) {
|
|
68
60
|
socketRef.current.disconnect();
|
|
69
61
|
socketRef.current = null;
|
|
@@ -72,32 +64,25 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
72
64
|
return;
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
// Initialize socket with token refresh
|
|
76
67
|
const initializeSocket = async () => {
|
|
77
68
|
try {
|
|
78
|
-
// Refresh token if expiring soon before creating socket connection
|
|
79
69
|
await tokenService.refreshTokenIfNeeded();
|
|
80
70
|
} catch (error) {
|
|
81
|
-
// If refresh fails, log but continue with current token
|
|
82
71
|
logger.debug('Token refresh failed before socket connection', { component: 'useSessionSocket', userId, error });
|
|
83
72
|
}
|
|
84
73
|
|
|
85
74
|
const accessToken = getAccessTokenRef.current();
|
|
86
|
-
// Recreate socket if token changed or socket doesn't exist
|
|
87
75
|
const tokenChanged = accessTokenRef.current !== accessToken;
|
|
88
76
|
if (!socketRef.current || tokenChanged) {
|
|
89
|
-
// Disconnect old socket if exists
|
|
90
77
|
if (socketRef.current) {
|
|
91
78
|
socketRef.current.disconnect();
|
|
92
79
|
socketRef.current = null;
|
|
93
80
|
}
|
|
94
|
-
|
|
95
|
-
// Create new socket with authentication
|
|
81
|
+
|
|
96
82
|
const socketOptions: any = {
|
|
97
83
|
transports: ['websocket'],
|
|
98
84
|
};
|
|
99
85
|
|
|
100
|
-
// Get fresh token after potential refresh
|
|
101
86
|
const freshToken = getAccessTokenRef.current();
|
|
102
87
|
if (freshToken) {
|
|
103
88
|
socketOptions.auth = {
|
|
@@ -105,14 +90,13 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
105
90
|
};
|
|
106
91
|
} else {
|
|
107
92
|
logger.debug('No access token available for socket authentication', { component: 'useSessionSocket', userId });
|
|
108
|
-
// Defer socket creation if token is still missing
|
|
109
93
|
return;
|
|
110
94
|
}
|
|
111
95
|
|
|
112
96
|
socketRef.current = io(baseURL, socketOptions);
|
|
113
97
|
accessTokenRef.current = freshToken;
|
|
114
|
-
joinedRoomRef.current = null;
|
|
115
|
-
handlersSetupRef.current = false;
|
|
98
|
+
joinedRoomRef.current = null;
|
|
99
|
+
handlersSetupRef.current = false;
|
|
116
100
|
}
|
|
117
101
|
|
|
118
102
|
const socket = socketRef.current;
|
|
@@ -122,8 +106,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
122
106
|
joinedRoomRef.current = `user:${userId}`;
|
|
123
107
|
}
|
|
124
108
|
|
|
125
|
-
// Set up event handlers (only once per socket instance)
|
|
126
|
-
// Define handlers - they reference socket from closure
|
|
127
109
|
const handleConnect = () => {
|
|
128
110
|
const currentToken = getAccessTokenRef.current();
|
|
129
111
|
if (__DEV__) {
|
|
@@ -60,7 +60,7 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
60
60
|
}) => {
|
|
61
61
|
const themeValue = (theme === 'light' || theme === 'dark') ? theme : 'light';
|
|
62
62
|
const colors = useThemeColors(themeValue);
|
|
63
|
-
const { oxyServices,
|
|
63
|
+
const { oxyServices, switchSession } = useOxy();
|
|
64
64
|
|
|
65
65
|
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
|
66
66
|
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -1,44 +1,65 @@
|
|
|
1
|
+
/** Auth store for Services SDK (sessions/tokens only). */
|
|
2
|
+
|
|
1
3
|
import { create } from 'zustand';
|
|
2
4
|
|
|
3
5
|
export interface AuthState {
|
|
4
6
|
isAuthenticated: boolean;
|
|
7
|
+
isOnline: boolean;
|
|
5
8
|
isLoading: boolean;
|
|
6
9
|
error: string | null;
|
|
10
|
+
identitySynced: boolean;
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
isIdentitySynced: boolean;
|
|
10
|
-
isSyncing: boolean;
|
|
11
|
-
|
|
12
|
+
setOnline: (online: boolean) => void;
|
|
12
13
|
loginSuccess: () => void;
|
|
13
14
|
loginFailure: (error: string) => void;
|
|
14
15
|
logout: () => void;
|
|
15
|
-
|
|
16
|
-
// Identity sync actions
|
|
17
16
|
setIdentitySynced: (synced: boolean) => void;
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
canAuthenticate: () => boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const useAuthStore = create<AuthState>((set
|
|
21
|
+
export const useAuthStore = create<AuthState>((set, get) => ({
|
|
22
22
|
isAuthenticated: false,
|
|
23
|
+
isOnline: true, // Assume online initially
|
|
23
24
|
isLoading: false,
|
|
24
25
|
error: null,
|
|
26
|
+
identitySynced: false,
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
setOnline: (online: boolean) => {
|
|
29
|
+
set({ isOnline: online });
|
|
30
|
+
// If we go offline, we can't be authenticated
|
|
31
|
+
if (!online) {
|
|
32
|
+
set({ isAuthenticated: false });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
29
35
|
|
|
30
36
|
loginSuccess: () => set({
|
|
31
37
|
isLoading: false,
|
|
32
|
-
isAuthenticated:
|
|
33
|
-
|
|
38
|
+
isAuthenticated: get().isOnline, // Only authenticated if online
|
|
39
|
+
error: null,
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
loginFailure: (error: string) => set({
|
|
43
|
+
isLoading: false,
|
|
44
|
+
isAuthenticated: false,
|
|
45
|
+
error
|
|
34
46
|
}),
|
|
35
|
-
|
|
47
|
+
|
|
36
48
|
logout: () => set({
|
|
37
49
|
isAuthenticated: false,
|
|
38
|
-
|
|
50
|
+
error: null,
|
|
51
|
+
identitySynced: false,
|
|
39
52
|
}),
|
|
53
|
+
|
|
54
|
+
// Track whether identity registration is confirmed with backend
|
|
55
|
+
setIdentitySynced: (synced: boolean) => set({ identitySynced: synced }),
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Check if user can authenticate
|
|
59
|
+
* Requires both valid tokens (checked by caller) and network
|
|
60
|
+
*/
|
|
61
|
+
canAuthenticate: () => {
|
|
62
|
+
const state = get();
|
|
63
|
+
return state.isOnline;
|
|
64
|
+
},
|
|
44
65
|
}));
|