@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.
Files changed (97) hide show
  1. package/lib/commonjs/crypto/index.js +0 -23
  2. package/lib/commonjs/crypto/index.js.map +1 -1
  3. package/lib/commonjs/index.js +0 -15
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/components/Icon.js.map +1 -1
  6. package/lib/commonjs/ui/components/IconButton/utils.js.map +1 -1
  7. package/lib/commonjs/ui/components/TextField/Adornment/utils.js.map +1 -1
  8. package/lib/commonjs/ui/components/TextField/helpers.js.map +1 -1
  9. package/lib/commonjs/ui/components/TouchableRipple/utils.js.map +1 -1
  10. package/lib/commonjs/ui/components/Typography/AnimatedText.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +20 -35
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +41 -118
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/useSessionSocket.js +2 -26
  16. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  17. package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -1
  18. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  19. package/lib/commonjs/ui/stores/authStore.js +33 -13
  20. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  21. package/lib/commonjs/ui/utils/avatarUtils.js +2 -32
  22. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  23. package/lib/module/crypto/index.js +4 -6
  24. package/lib/module/crypto/index.js.map +1 -1
  25. package/lib/module/index.js +6 -3
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ui/components/Icon.js.map +1 -1
  28. package/lib/module/ui/components/IconButton/utils.js.map +1 -1
  29. package/lib/module/ui/components/TextField/Adornment/utils.js.map +1 -1
  30. package/lib/module/ui/components/TextField/helpers.js.map +1 -1
  31. package/lib/module/ui/components/TouchableRipple/utils.js.map +1 -1
  32. package/lib/module/ui/components/Typography/AnimatedText.js.map +1 -1
  33. package/lib/module/ui/context/OxyContext.js +20 -36
  34. package/lib/module/ui/context/OxyContext.js.map +1 -1
  35. package/lib/module/ui/context/hooks/useAuthOperations.js +41 -118
  36. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  37. package/lib/module/ui/hooks/useSessionSocket.js +2 -26
  38. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  39. package/lib/module/ui/screens/OxyAuthScreen.js +0 -1
  40. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  41. package/lib/module/ui/stores/authStore.js +33 -13
  42. package/lib/module/ui/stores/authStore.js.map +1 -1
  43. package/lib/module/ui/utils/avatarUtils.js +2 -32
  44. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  45. package/lib/typescript/crypto/index.d.ts +2 -5
  46. package/lib/typescript/crypto/index.d.ts.map +1 -1
  47. package/lib/typescript/crypto/types.d.ts +2 -2
  48. package/lib/typescript/index.d.ts +4 -2
  49. package/lib/typescript/index.d.ts.map +1 -1
  50. package/lib/typescript/ui/components/IconButton/utils.d.ts +1 -1
  51. package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts +1 -1
  52. package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts.map +1 -1
  53. package/lib/typescript/ui/components/TextField/helpers.d.ts +6 -6
  54. package/lib/typescript/ui/components/types.d.ts +0 -4
  55. package/lib/typescript/ui/components/types.d.ts.map +1 -1
  56. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  57. package/lib/typescript/ui/context/OxyContextBase.d.ts +2 -2
  58. package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
  59. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +2 -9
  60. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  61. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  62. package/lib/typescript/ui/stores/authStore.d.ts +5 -3
  63. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  64. package/lib/typescript/ui/utils/avatarUtils.d.ts +0 -2
  65. package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
  66. package/package.json +2 -2
  67. package/src/crypto/index.ts +3 -11
  68. package/src/crypto/types.ts +2 -2
  69. package/src/index.ts +6 -11
  70. package/src/ui/components/Icon.tsx +1 -1
  71. package/src/ui/components/IconButton/utils.ts +1 -1
  72. package/src/ui/components/TextField/Adornment/utils.ts +2 -2
  73. package/src/ui/components/TextField/helpers.tsx +8 -8
  74. package/src/ui/components/TouchableRipple/utils.ts +2 -2
  75. package/src/ui/components/Typography/AnimatedText.tsx +2 -2
  76. package/src/ui/components/types.tsx +0 -6
  77. package/src/ui/context/OxyContext.tsx +22 -27
  78. package/src/ui/context/OxyContextBase.tsx +4 -4
  79. package/src/ui/context/hooks/useAuthOperations.ts +61 -140
  80. package/src/ui/hooks/useSessionSocket.ts +3 -21
  81. package/src/ui/screens/OxyAuthScreen.tsx +1 -1
  82. package/src/ui/stores/authStore.ts +39 -18
  83. package/src/ui/utils/avatarUtils.ts +4 -36
  84. package/lib/commonjs/crypto/keyManager.js +0 -356
  85. package/lib/commonjs/crypto/keyManager.js.map +0 -1
  86. package/lib/commonjs/crypto/signatureService.js +0 -269
  87. package/lib/commonjs/crypto/signatureService.js.map +0 -1
  88. package/lib/module/crypto/keyManager.js +0 -353
  89. package/lib/module/crypto/keyManager.js.map +0 -1
  90. package/lib/module/crypto/signatureService.js +0 -266
  91. package/lib/module/crypto/signatureService.js.map +0 -1
  92. package/lib/typescript/crypto/keyManager.d.ts +0 -80
  93. package/lib/typescript/crypto/keyManager.d.ts.map +0 -1
  94. package/lib/typescript/crypto/signatureService.d.ts +0 -77
  95. package/lib/typescript/crypto/signatureService.d.ts.map +0 -1
  96. package/src/crypto/keyManager.ts +0 -379
  97. 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, // Always fetch fresh - Services never caches profile
193
- gcTime: 0, // No garbage collection time - always fetch fresh
194
- retry: false, // Don't retry on 401 - session is invalid
188
+ staleTime: 0,
189
+ gcTime: 0,
190
+ retry: false,
195
191
  });
196
192
  const user = userData ?? null;
197
193
 
198
194
  const {
199
- signIn,
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 && __DEV__) {
278
- logger('Network appears offline');
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
- signIn,
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
- signIn,
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 (createIdentity, importIdentity, etc.) belongs in Accounts app
28
- signIn: (deviceName?: string) => Promise<User>;
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
- /** Sign in with existing identity on device */
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
- * Internal function to perform challenge-response sign in
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
- // Sign the challenge
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 = await oxyServices.verifyChallenge(
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
- // Get full user data
121
- const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
69
+ const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
122
70
 
123
- // Fetch device sessions
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.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
148
- } catch (logoutError) {
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 logout duplicate session:', logoutError);
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
- * Sign in with existing identity on device
188
- */
189
- const signIn = useCallback(
190
- async (deviceName?: string): Promise<User> => {
191
- if (!storage) throw new Error('Storage not initialized');
192
-
193
- setAuthState({ isLoading: true, error: null });
194
-
195
- try {
196
- // Get stored public key
197
- const publicKey = await KeyManager.getPublicKey();
198
- if (!publicKey) {
199
- throw new Error('No identity found on this device. Please create or import an identity.');
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
- // Ensure identity is registered before attempting sign-in
203
- const { registered } = await oxyServices.checkPublicKeyRegistered(publicKey);
204
- if (!registered) {
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
- return await performSignIn(publicKey, deviceName);
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: LOGIN_ERROR_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
- [storage, setAuthState, performSignIn, loginFailure, onError, logger, oxyServices],
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
- signIn,
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; // Reset room tracking
115
- handlersSetupRef.current = false; // Reset handlers flag for new socket
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, signIn, switchSession } = useOxy();
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
- // Identity sync state (offline-first)
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
- setSyncing: (syncing: boolean) => void;
17
+
18
+ canAuthenticate: () => boolean;
19
19
  }
20
20
 
21
- export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>) => void) => ({
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
- // Identity sync state (offline-first)
27
- isIdentitySynced: false, // Registration/identity sync not confirmed until done
28
- isSyncing: false,
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: true,
33
- isIdentitySynced: true, // If login succeeded, registration is complete
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
- loginFailure: (error: string) => set({ isLoading: false, error }),
47
+
36
48
  logout: () => set({
37
49
  isAuthenticated: false,
38
- isSyncing: false,
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
- // Identity sync actions
42
- setIdentitySynced: (synced: boolean) => set({ isIdentitySynced: synced }),
43
- setSyncing: (syncing: boolean) => set({ isSyncing: syncing }),
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
  }));