@oxyhq/services 5.16.24 → 5.16.25

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 (38) hide show
  1. package/lib/commonjs/core/mixins/OxyServices.user.js +14 -4
  2. package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
  3. package/lib/commonjs/ui/context/OxyContext.js +40 -75
  4. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  5. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +7 -6
  6. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  7. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +4 -3
  8. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  9. package/lib/commonjs/ui/hooks/useSessionSocket.js +349 -328
  10. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  11. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +13 -6
  12. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  13. package/lib/module/core/mixins/OxyServices.user.js +14 -4
  14. package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
  15. package/lib/module/ui/context/OxyContext.js +40 -75
  16. package/lib/module/ui/context/OxyContext.js.map +1 -1
  17. package/lib/module/ui/hooks/mutations/useAccountMutations.js +7 -6
  18. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  19. package/lib/module/ui/hooks/queries/useAccountQueries.js +4 -3
  20. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  21. package/lib/module/ui/hooks/useSessionSocket.js +349 -328
  22. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  23. package/lib/module/ui/screens/PrivacySettingsScreen.js +13 -6
  24. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  25. package/lib/typescript/core/mixins/OxyServices.user.d.ts +2 -2
  26. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
  27. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  28. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  29. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  30. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  31. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/core/mixins/OxyServices.user.ts +14 -4
  34. package/src/ui/context/OxyContext.tsx +39 -75
  35. package/src/ui/hooks/mutations/useAccountMutations.ts +8 -6
  36. package/src/ui/hooks/queries/useAccountQueries.ts +4 -2
  37. package/src/ui/hooks/useSessionSocket.ts +153 -155
  38. package/src/ui/screens/PrivacySettingsScreen.tsx +12 -6
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
4
4
  import io from 'socket.io-client';
5
5
  import { toast } from '../../lib/sonner';
6
6
  import { logger } from '../../utils/loggerUtils';
7
+ import { tokenService } from '../../core/services/TokenService';
7
8
  export function useSessionSocket({
8
9
  userId,
9
10
  activeSessionId,
@@ -22,6 +23,9 @@ export function useSessionSocket({
22
23
  const joinedRoomRef = useRef(null);
23
24
  const accessTokenRef = useRef(null);
24
25
  const handlersSetupRef = useRef(false);
26
+ const lastRegisteredSocketIdRef = useRef(null);
27
+ const getAccessTokenRef = useRef(getAccessToken);
28
+ const getTransferCodeRef = useRef(getTransferCode);
25
29
 
26
30
  // Store callbacks in refs to avoid re-joining when they change
27
31
  const refreshSessionsRef = useRef(refreshSessions);
@@ -43,7 +47,9 @@ export function useSessionSocket({
43
47
  onIdentityTransferCompleteRef.current = onIdentityTransferComplete;
44
48
  activeSessionIdRef.current = activeSessionId;
45
49
  currentDeviceIdRef.current = currentDeviceId;
46
- }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, onIdentityTransferComplete, activeSessionId, currentDeviceId]);
50
+ getAccessTokenRef.current = getAccessToken;
51
+ getTransferCodeRef.current = getTransferCode;
52
+ }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, onIdentityTransferComplete, activeSessionId, currentDeviceId, getAccessToken, getTransferCode]);
47
53
  useEffect(() => {
48
54
  if (!userId || !baseURL) {
49
55
  // Clean up if userId or baseURL becomes invalid
@@ -54,416 +60,431 @@ export function useSessionSocket({
54
60
  }
55
61
  return;
56
62
  }
57
- const accessToken = getAccessToken();
58
- // Recreate socket if token changed or socket doesn't exist
59
- const tokenChanged = accessTokenRef.current !== accessToken;
60
- if (!socketRef.current || tokenChanged) {
61
- // Disconnect old socket if exists
62
- if (socketRef.current) {
63
- socketRef.current.disconnect();
64
- socketRef.current = null;
65
- }
66
-
67
- // Create new socket with authentication
68
- const socketOptions = {
69
- transports: ['websocket']
70
- };
71
- if (accessToken) {
72
- socketOptions.auth = {
73
- token: accessToken
74
- };
75
- if (__DEV__) {
76
- console.log('[useSessionSocket] Creating socket with auth token', {
77
- userId,
78
- hasToken: !!accessToken,
79
- tokenLength: accessToken.length
80
- });
81
- }
82
- } else {
83
- if (__DEV__) {
84
- console.warn('[useSessionSocket] No access token available for socket authentication');
85
- }
86
- }
87
- socketRef.current = io(baseURL, socketOptions);
88
- accessTokenRef.current = accessToken;
89
- joinedRoomRef.current = null; // Reset room tracking
90
- handlersSetupRef.current = false; // Reset handlers flag for new socket
91
- }
92
- const socket = socketRef.current;
93
63
 
94
- // Server auto-joins room on connection when authenticated, so we don't need to manually join
95
- // Just track that we're in the room
96
- if (!joinedRoomRef.current && socket.connected) {
97
- joinedRoomRef.current = `user:${userId}`;
98
- if (__DEV__) {
99
- console.log('[useSessionSocket] Socket connected, should be auto-joined to room', {
64
+ // Initialize socket with token refresh
65
+ const initializeSocket = async () => {
66
+ try {
67
+ // Refresh token if expiring soon before creating socket connection
68
+ await tokenService.refreshTokenIfNeeded();
69
+ } catch (error) {
70
+ // If refresh fails, log but continue with current token
71
+ logger.debug('Token refresh failed before socket connection', {
72
+ component: 'useSessionSocket',
100
73
  userId,
101
- room: `user:${userId}`
74
+ error
102
75
  });
103
76
  }
104
- }
77
+ const accessToken = getAccessTokenRef.current();
78
+ // Recreate socket if token changed or socket doesn't exist
79
+ const tokenChanged = accessTokenRef.current !== accessToken;
80
+ if (!socketRef.current || tokenChanged) {
81
+ // Disconnect old socket if exists
82
+ if (socketRef.current) {
83
+ socketRef.current.disconnect();
84
+ socketRef.current = null;
85
+ }
105
86
 
106
- // Set up event handlers (only once per socket instance)
107
- // Define handlers outside if block so they're always available
108
- const handleConnect = () => {
109
- const currentToken = getAccessToken();
110
- if (__DEV__) {
111
- console.log('[useSessionSocket] Socket connected', {
112
- socketId: socket.id,
113
- userId,
114
- room: `user:${userId}`,
115
- hasAuth: !!currentToken
116
- });
117
- logger.debug('Socket connected', {
118
- component: 'useSessionSocket',
119
- socketId: socket.id,
120
- userId
121
- });
87
+ // Create new socket with authentication
88
+ const socketOptions = {
89
+ transports: ['websocket']
90
+ };
91
+
92
+ // Get fresh token after potential refresh
93
+ const freshToken = getAccessTokenRef.current();
94
+ if (freshToken) {
95
+ socketOptions.auth = {
96
+ token: freshToken
97
+ };
98
+ } else {
99
+ logger.debug('No access token available for socket authentication', {
100
+ component: 'useSessionSocket',
101
+ userId
102
+ });
103
+ }
104
+ socketRef.current = io(baseURL, socketOptions);
105
+ accessTokenRef.current = freshToken;
106
+ joinedRoomRef.current = null; // Reset room tracking
107
+ handlersSetupRef.current = false; // Reset handlers flag for new socket
122
108
  }
123
- // Server auto-joins room on connection when authenticated
124
- // Just track that we're connected
125
- if (userId) {
109
+ const socket = socketRef.current;
110
+ if (!socket) return;
111
+ if (!joinedRoomRef.current && socket.connected) {
126
112
  joinedRoomRef.current = `user:${userId}`;
127
113
  }
128
- };
129
- const handleDisconnect = reason => {
130
- if (__DEV__) {
131
- console.log('[useSessionSocket] Socket disconnected:', reason);
114
+
115
+ // Set up event handlers (only once per socket instance)
116
+ // Define handlers - they reference socket from closure
117
+ const handleConnect = () => {
118
+ const currentToken = getAccessTokenRef.current();
119
+ if (__DEV__) {
120
+ console.log('[useSessionSocket] Socket connected', {
121
+ socketId: socket.id,
122
+ userId,
123
+ room: `user:${userId}`,
124
+ hasAuth: !!currentToken
125
+ });
126
+ logger.debug('Socket connected', {
127
+ component: 'useSessionSocket',
128
+ socketId: socket.id,
129
+ userId
130
+ });
131
+ }
132
+ // Server auto-joins room on connection when authenticated
133
+ // Just track that we're connected
134
+ if (userId) {
135
+ joinedRoomRef.current = `user:${userId}`;
136
+ }
137
+ };
138
+ const handleDisconnect = async reason => {
132
139
  logger.debug('Socket disconnected', {
133
140
  component: 'useSessionSocket',
134
141
  reason,
135
142
  userId
136
143
  });
137
- }
138
- joinedRoomRef.current = null; // Reset room tracking on disconnect
139
- };
140
- const handleError = error => {
141
- if (__DEV__) {
142
- console.error('[useSessionSocket] Socket error', error);
144
+ joinedRoomRef.current = null;
145
+
146
+ // If disconnected due to auth error, try to refresh token and reconnect
147
+ if (reason === 'io server disconnect' || reason.includes('auth') || reason.includes('Authentication')) {
148
+ try {
149
+ // Refresh token and reconnect
150
+ await tokenService.refreshTokenIfNeeded();
151
+ const freshToken = getAccessTokenRef.current();
152
+ if (freshToken && socketRef.current) {
153
+ // Update auth and reconnect
154
+ socketRef.current.auth = {
155
+ token: freshToken
156
+ };
157
+ socketRef.current.connect();
158
+ }
159
+ } catch (error) {
160
+ logger.debug('Failed to refresh token after disconnect', {
161
+ component: 'useSessionSocket',
162
+ userId,
163
+ error
164
+ });
165
+ }
166
+ }
167
+ };
168
+ const handleError = error => {
143
169
  logger.error('Socket error', error, {
144
170
  component: 'useSessionSocket',
145
171
  userId
146
172
  });
147
- }
148
- };
149
- const handleSessionUpdate = async data => {
150
- if (__DEV__) {
151
- console.log('[useSessionSocket] Received session_update event:', {
173
+ };
174
+ const handleConnectError = async error => {
175
+ logger.debug('Socket connection error', {
176
+ component: 'useSessionSocket',
177
+ userId,
178
+ error: error.message
179
+ });
180
+
181
+ // If error is due to expired/invalid token, try to refresh and reconnect
182
+ if (error.message.includes('Authentication') || error.message.includes('expired') || error.message.includes('token')) {
183
+ try {
184
+ await tokenService.refreshTokenIfNeeded();
185
+ const freshToken = getAccessTokenRef.current();
186
+ if (freshToken && socketRef.current) {
187
+ // Update auth and reconnect
188
+ socketRef.current.auth = {
189
+ token: freshToken
190
+ };
191
+ socketRef.current.connect();
192
+ }
193
+ } catch (refreshError) {
194
+ logger.debug('Failed to refresh token after connection error', {
195
+ component: 'useSessionSocket',
196
+ userId,
197
+ error: refreshError
198
+ });
199
+ }
200
+ }
201
+ };
202
+ const handleSessionUpdate = async data => {
203
+ logger.debug('Received session_update event', {
204
+ component: 'useSessionSocket',
152
205
  type: data.type,
153
206
  socketId: socket.id,
154
207
  socketConnected: socket.connected,
155
208
  roomId: joinedRoomRef.current
156
209
  });
157
- }
158
- const currentActiveSessionId = activeSessionIdRef.current;
159
- const currentDeviceId = currentDeviceIdRef.current;
210
+ const currentActiveSessionId = activeSessionIdRef.current;
211
+ const currentDeviceId = currentDeviceIdRef.current;
160
212
 
161
- // Handle different event types
162
- if (data.type === 'session_removed') {
163
- // Track removed session
164
- if (data.sessionId && onSessionRemovedRef.current) {
165
- onSessionRemovedRef.current(data.sessionId);
166
- }
167
-
168
- // If the removed sessionId matches the current activeSessionId, immediately clear state
169
- if (data.sessionId === currentActiveSessionId) {
170
- if (onRemoteSignOutRef.current) {
171
- onRemoteSignOutRef.current();
172
- } else {
173
- toast.info('You have been signed out remotely.');
213
+ // Handle different event types
214
+ if (data.type === 'session_removed') {
215
+ // Track removed session
216
+ if (data.sessionId && onSessionRemovedRef.current) {
217
+ onSessionRemovedRef.current(data.sessionId);
174
218
  }
175
- // Use clearSessionState since session was already removed server-side
176
- // Await to ensure storage cleanup completes before continuing
177
- try {
178
- await clearSessionStateRef.current();
179
- } catch (error) {
180
- if (__DEV__) {
181
- logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), {
182
- component: 'useSessionSocket'
183
- });
219
+
220
+ // If the removed sessionId matches the current activeSessionId, immediately clear state
221
+ if (data.sessionId === currentActiveSessionId) {
222
+ if (onRemoteSignOutRef.current) {
223
+ onRemoteSignOutRef.current();
224
+ } else {
225
+ toast.info('You have been signed out remotely.');
184
226
  }
185
- }
186
- } else {
187
- // Otherwise, just refresh the sessions list (with error handling)
188
- refreshSessionsRef.current().catch(error => {
189
- // Silently handle errors from refresh - they're expected if sessions were removed
190
- if (__DEV__) {
191
- logger.debug('Failed to refresh sessions after session_removed', {
192
- component: 'useSessionSocket'
193
- }, error);
227
+ // Use clearSessionState since session was already removed server-side
228
+ // Await to ensure storage cleanup completes before continuing
229
+ try {
230
+ await clearSessionStateRef.current();
231
+ } catch (error) {
232
+ if (__DEV__) {
233
+ logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), {
234
+ component: 'useSessionSocket'
235
+ });
236
+ }
194
237
  }
195
- });
196
- }
197
- } else if (data.type === 'device_removed') {
198
- // Track all removed sessions from this device
199
- if (data.sessionIds && onSessionRemovedRef.current) {
200
- for (const sessionId of data.sessionIds) {
201
- onSessionRemovedRef.current(sessionId);
202
- }
203
- }
204
-
205
- // If the removed deviceId matches the current device, immediately clear state
206
- if (data.deviceId && data.deviceId === currentDeviceId) {
207
- if (onRemoteSignOutRef.current) {
208
- onRemoteSignOutRef.current();
209
238
  } else {
210
- toast.info('This device has been removed. You have been signed out.');
211
- }
212
- // Use clearSessionState since sessions were already removed server-side
213
- // Await to ensure storage cleanup completes before continuing
214
- try {
215
- await clearSessionStateRef.current();
216
- } catch (error) {
217
- if (__DEV__) {
218
- logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), {
219
- component: 'useSessionSocket'
220
- });
221
- }
239
+ // Otherwise, just refresh the sessions list (with error handling)
240
+ refreshSessionsRef.current().catch(error => {
241
+ // Silently handle errors from refresh - they're expected if sessions were removed
242
+ if (__DEV__) {
243
+ logger.debug('Failed to refresh sessions after session_removed', {
244
+ component: 'useSessionSocket'
245
+ }, error);
246
+ }
247
+ });
222
248
  }
223
- } else {
224
- // Otherwise, refresh sessions and device list (with error handling)
225
- refreshSessionsRef.current().catch(error => {
226
- // Silently handle errors from refresh - they're expected if sessions were removed
227
- if (__DEV__) {
228
- logger.debug('Failed to refresh sessions after device_removed', {
229
- component: 'useSessionSocket'
230
- }, error);
249
+ } else if (data.type === 'device_removed') {
250
+ // Track all removed sessions from this device
251
+ if (data.sessionIds && onSessionRemovedRef.current) {
252
+ for (const sessionId of data.sessionIds) {
253
+ onSessionRemovedRef.current(sessionId);
231
254
  }
232
- });
233
- }
234
- } else if (data.type === 'sessions_removed') {
235
- // Track all removed sessions
236
- if (data.sessionIds && onSessionRemovedRef.current) {
237
- for (const sessionId of data.sessionIds) {
238
- onSessionRemovedRef.current(sessionId);
239
255
  }
240
- }
241
256
 
242
- // If the current activeSessionId is in the removed sessionIds list, immediately clear state
243
- if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
244
- if (onRemoteSignOutRef.current) {
245
- onRemoteSignOutRef.current();
257
+ // If the removed deviceId matches the current device, immediately clear state
258
+ if (data.deviceId && data.deviceId === currentDeviceId) {
259
+ if (onRemoteSignOutRef.current) {
260
+ onRemoteSignOutRef.current();
261
+ } else {
262
+ toast.info('This device has been removed. You have been signed out.');
263
+ }
264
+ // Use clearSessionState since sessions were already removed server-side
265
+ // Await to ensure storage cleanup completes before continuing
266
+ try {
267
+ await clearSessionStateRef.current();
268
+ } catch (error) {
269
+ if (__DEV__) {
270
+ logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), {
271
+ component: 'useSessionSocket'
272
+ });
273
+ }
274
+ }
246
275
  } else {
247
- toast.info('You have been signed out remotely.');
276
+ // Otherwise, refresh sessions and device list (with error handling)
277
+ refreshSessionsRef.current().catch(error => {
278
+ // Silently handle errors from refresh - they're expected if sessions were removed
279
+ if (__DEV__) {
280
+ logger.debug('Failed to refresh sessions after device_removed', {
281
+ component: 'useSessionSocket'
282
+ }, error);
283
+ }
284
+ });
248
285
  }
249
- // Use clearSessionState since sessions were already removed server-side
250
- // Await to ensure storage cleanup completes before continuing
251
- try {
252
- await clearSessionStateRef.current();
253
- } catch (error) {
254
- if (__DEV__) {
255
- logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), {
256
- component: 'useSessionSocket'
257
- });
286
+ } else if (data.type === 'sessions_removed') {
287
+ // Track all removed sessions
288
+ if (data.sessionIds && onSessionRemovedRef.current) {
289
+ for (const sessionId of data.sessionIds) {
290
+ onSessionRemovedRef.current(sessionId);
258
291
  }
259
292
  }
260
- } else {
261
- // Otherwise, refresh sessions list (with error handling)
262
- refreshSessionsRef.current().catch(error => {
263
- // Silently handle errors from refresh - they're expected if sessions were removed
264
- if (__DEV__) {
265
- logger.debug('Failed to refresh sessions after sessions_removed', {
266
- component: 'useSessionSocket'
267
- }, error);
293
+
294
+ // If the current activeSessionId is in the removed sessionIds list, immediately clear state
295
+ if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
296
+ if (onRemoteSignOutRef.current) {
297
+ onRemoteSignOutRef.current();
298
+ } else {
299
+ toast.info('You have been signed out remotely.');
268
300
  }
269
- });
270
- }
271
- } else if (data.type === 'identity_transfer_complete') {
272
- // Handle identity transfer completion notification
273
- const transferData = data;
274
- if (__DEV__) {
275
- console.log('[useSessionSocket] Received identity_transfer_complete event', {
301
+ // Use clearSessionState since sessions were already removed server-side
302
+ // Await to ensure storage cleanup completes before continuing
303
+ try {
304
+ await clearSessionStateRef.current();
305
+ } catch (error) {
306
+ if (__DEV__) {
307
+ logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), {
308
+ component: 'useSessionSocket'
309
+ });
310
+ }
311
+ }
312
+ } else {
313
+ // Otherwise, refresh sessions list (with error handling)
314
+ refreshSessionsRef.current().catch(error => {
315
+ // Silently handle errors from refresh - they're expected if sessions were removed
316
+ if (__DEV__) {
317
+ logger.debug('Failed to refresh sessions after sessions_removed', {
318
+ component: 'useSessionSocket'
319
+ }, error);
320
+ }
321
+ });
322
+ }
323
+ } else if (data.type === 'identity_transfer_complete') {
324
+ // Handle identity transfer completion notification
325
+ const transferData = data;
326
+ logger.debug('Received identity_transfer_complete event', {
327
+ component: 'useSessionSocket',
276
328
  transferId: transferData.transferId,
277
329
  sourceDeviceId: transferData.sourceDeviceId,
278
330
  currentDeviceId,
279
- hasActiveSession: activeSessionIdRef.current !== null,
280
- socketConnected: socket.connected
331
+ activeSessionId: activeSessionIdRef.current,
332
+ socketConnected: socket.connected,
333
+ userId,
334
+ room: joinedRoomRef.current,
335
+ publicKey: transferData.publicKey.substring(0, 16) + '...'
281
336
  });
282
- }
283
- logger.debug('Received identity_transfer_complete event', {
284
- component: 'useSessionSocket',
285
- transferId: transferData.transferId,
286
- sourceDeviceId: transferData.sourceDeviceId,
287
- currentDeviceId,
288
- activeSessionId: activeSessionIdRef.current,
289
- socketConnected: socket.connected,
290
- userId,
291
- room: joinedRoomRef.current,
292
- publicKey: transferData.publicKey.substring(0, 16) + '...'
293
- });
294
337
 
295
- // Only call handler on the SOURCE device (the one that initiated the transfer)
296
- // The source device is identified by:
297
- // 1. Matching deviceId with sourceDeviceId, OR
298
- // 2. Having a stored transfer code for this transferId (most reliable check)
299
- const deviceIdMatches = transferData.sourceDeviceId && transferData.sourceDeviceId === currentDeviceId;
338
+ // CRITICAL: Only call handler on the SOURCE device (the one that initiated the transfer)
339
+ // The new device (target) should NEVER process this event - it would delete its own identity!
300
340
 
301
- // Check if this device has a stored transfer code (meaning it's the source device)
302
- const hasStoredTransferCode = getTransferCode && !!getTransferCode(transferData.transferId);
341
+ // Check if this device has a stored transfer code (most reliable check - only source device has this)
342
+ const hasStoredTransferCode = getTransferCodeRef.current && !!getTransferCodeRef.current(transferData.transferId);
303
343
 
304
- // Only call handler if this is the source device
305
- // We check both deviceId match AND stored transfer code to handle cases where
306
- // deviceId might be null (logged out device) but transfer code still exists
307
- const shouldCallHandler = !!transferData.transferId && (deviceIdMatches || hasStoredTransferCode);
308
- if (shouldCallHandler) {
309
- const matchReason = deviceIdMatches ? 'deviceId-exact' : activeSessionIdRef.current !== null ? 'active-session' : 'transferId-only';
310
- if (__DEV__) {
311
- console.log('[useSessionSocket] Matched source device, calling handler', {
344
+ // Also check deviceId match (exact match required)
345
+ const deviceIdMatches = transferData.sourceDeviceId && currentDeviceId && transferData.sourceDeviceId === currentDeviceId;
346
+
347
+ // ONLY call handler if BOTH conditions are met:
348
+ // 1. Has stored transfer code (definitive proof this is the source device)
349
+ // 2. DeviceId matches (additional verification)
350
+ // If deviceId is null/undefined, we still allow if stored code exists (logged out source device)
351
+ // But we NEVER process if no stored code exists (definitely not the source device)
352
+ const shouldCallHandler = !!transferData.transferId && hasStoredTransferCode && (deviceIdMatches || !currentDeviceId); // Allow if deviceId matches OR device is logged out (but has stored code)
353
+
354
+ if (shouldCallHandler) {
355
+ const matchReason = deviceIdMatches ? 'deviceId-exact-with-stored-code' : currentDeviceId ? 'deviceId-mismatch-but-has-stored-code' : 'logged-out-source-device-with-stored-code';
356
+ logger.debug('Matched source device, calling transfer complete handler', {
357
+ component: 'useSessionSocket',
312
358
  transferId: transferData.transferId,
313
- matchReason,
314
359
  sourceDeviceId: transferData.sourceDeviceId,
315
- currentDeviceId
360
+ currentDeviceId,
361
+ matchReason,
362
+ hasHandler: !!onIdentityTransferCompleteRef.current,
363
+ socketConnected: socket.connected,
364
+ socketId: socket.id
316
365
  });
317
- }
318
- logger.debug('Matched source device, calling transfer complete handler', {
319
- component: 'useSessionSocket',
320
- transferId: transferData.transferId,
321
- sourceDeviceId: transferData.sourceDeviceId,
322
- currentDeviceId,
323
- matchReason,
324
- hasHandler: !!onIdentityTransferCompleteRef.current,
325
- socketConnected: socket.connected,
326
- socketId: socket.id
327
- });
328
-
329
- // Call the handler - it will verify using stored transfer codes
330
- if (onIdentityTransferCompleteRef.current) {
331
- try {
332
- if (__DEV__) {
333
- console.log('[useSessionSocket] Calling onIdentityTransferComplete handler', {
366
+ if (onIdentityTransferCompleteRef.current) {
367
+ try {
368
+ logger.debug('Calling onIdentityTransferComplete handler', {
369
+ component: 'useSessionSocket',
334
370
  transferId: transferData.transferId
335
371
  });
336
- }
337
- logger.debug('Calling onIdentityTransferComplete handler', {
338
- component: 'useSessionSocket',
339
- transferId: transferData.transferId
340
- });
341
- onIdentityTransferCompleteRef.current({
342
- transferId: transferData.transferId,
343
- sourceDeviceId: transferData.sourceDeviceId,
344
- publicKey: transferData.publicKey,
345
- transferCode: transferData.transferCode,
346
- completedAt: transferData.completedAt
347
- });
348
- if (__DEV__) {
349
- console.log('[useSessionSocket] Handler called successfully', {
372
+ onIdentityTransferCompleteRef.current({
373
+ transferId: transferData.transferId,
374
+ sourceDeviceId: transferData.sourceDeviceId,
375
+ publicKey: transferData.publicKey,
376
+ transferCode: transferData.transferCode,
377
+ completedAt: transferData.completedAt
378
+ });
379
+ logger.debug('onIdentityTransferComplete handler called successfully', {
380
+ component: 'useSessionSocket',
381
+ transferId: transferData.transferId
382
+ });
383
+ } catch (error) {
384
+ logger.error('Error calling onIdentityTransferComplete handler', error instanceof Error ? error : new Error(String(error)), {
385
+ component: 'useSessionSocket',
350
386
  transferId: transferData.transferId
351
387
  });
352
388
  }
353
- logger.debug('onIdentityTransferComplete handler called successfully', {
354
- component: 'useSessionSocket',
355
- transferId: transferData.transferId
356
- });
357
- } catch (error) {
358
- if (__DEV__) {
359
- console.error('[useSessionSocket] Error calling handler', error);
360
- }
361
- logger.error('Error calling onIdentityTransferComplete handler', error instanceof Error ? error : new Error(String(error)), {
389
+ } else {
390
+ logger.debug('No onIdentityTransferComplete handler registered', {
362
391
  component: 'useSessionSocket',
363
392
  transferId: transferData.transferId
364
393
  });
365
394
  }
366
395
  } else {
367
- if (__DEV__) {
368
- console.warn('[useSessionSocket] No handler registered');
369
- }
370
- logger.debug('No onIdentityTransferComplete handler registered', {
396
+ logger.debug('Not the source device, ignoring transfer completion', {
371
397
  component: 'useSessionSocket',
372
- transferId: transferData.transferId
373
- });
374
- }
375
- } else {
376
- if (__DEV__) {
377
- console.log('[useSessionSocket] Not matched, ignoring', {
378
398
  sourceDeviceId: transferData.sourceDeviceId,
379
399
  currentDeviceId,
380
400
  hasActiveSession: activeSessionIdRef.current !== null
381
401
  });
382
402
  }
383
- logger.debug('Not the source device, ignoring transfer completion', {
384
- component: 'useSessionSocket',
385
- sourceDeviceId: transferData.sourceDeviceId,
386
- currentDeviceId,
387
- hasActiveSession: activeSessionIdRef.current !== null
403
+ } else {
404
+ // For other event types (e.g., session_created), refresh sessions (with error handling)
405
+ refreshSessionsRef.current().catch(error => {
406
+ // Log but don't throw - refresh errors shouldn't break the socket handler
407
+ if (__DEV__) {
408
+ logger.debug('Failed to refresh sessions after session_update', {
409
+ component: 'useSessionSocket'
410
+ }, error);
411
+ }
388
412
  });
389
- }
390
- } else {
391
- // For other event types (e.g., session_created), refresh sessions (with error handling)
392
- refreshSessionsRef.current().catch(error => {
393
- // Log but don't throw - refresh errors shouldn't break the socket handler
394
- if (__DEV__) {
395
- logger.debug('Failed to refresh sessions after session_update', {
396
- component: 'useSessionSocket'
397
- }, error);
398
- }
399
- });
400
413
 
401
- // If the current session was logged out (legacy behavior), handle it specially
402
- if (data.sessionId === currentActiveSessionId) {
403
- if (onRemoteSignOutRef.current) {
404
- onRemoteSignOutRef.current();
405
- } else {
406
- toast.info('You have been signed out remotely.');
414
+ // If the current session was logged out (legacy behavior), handle it specially
415
+ if (data.sessionId === currentActiveSessionId) {
416
+ if (onRemoteSignOutRef.current) {
417
+ onRemoteSignOutRef.current();
418
+ } else {
419
+ toast.info('You have been signed out remotely.');
420
+ }
421
+ // Use clearSessionState since session was already removed server-side
422
+ // Await to ensure storage cleanup completes before continuing
423
+ try {
424
+ await clearSessionStateRef.current();
425
+ } catch (error) {
426
+ logger.error('Failed to clear session state after session_update', error instanceof Error ? error : new Error(String(error)), {
427
+ component: 'useSessionSocket'
428
+ });
429
+ }
407
430
  }
408
- // Use clearSessionState since session was already removed server-side
409
- // Await to ensure storage cleanup completes before continuing
431
+ }
432
+ };
433
+
434
+ // Register event handlers (only once per socket instance)
435
+ // Track by socket.id to prevent duplicate registrations when socket reconnects
436
+ const currentSocketId = socket.id || 'pending';
437
+ if (!handlersSetupRef.current || lastRegisteredSocketIdRef.current !== currentSocketId) {
438
+ // Remove old handlers if socket changed (reconnection)
439
+ if (socketRef.current && handlersSetupRef.current && lastRegisteredSocketIdRef.current) {
410
440
  try {
411
- await clearSessionStateRef.current();
441
+ socketRef.current.off('connect', handleConnect);
442
+ socketRef.current.off('disconnect', handleDisconnect);
443
+ socketRef.current.off('error', handleError);
444
+ socketRef.current.off('session_update', handleSessionUpdate);
412
445
  } catch (error) {
413
- if (__DEV__) {
414
- console.error('Failed to clear session state after session_update:', error);
415
- }
446
+ // Ignore errors when removing handlers
416
447
  }
417
448
  }
418
- }
419
- };
420
449
 
421
- // Register event handlers (only once per socket instance)
422
- if (!handlersSetupRef.current) {
423
- socket.on('connect', handleConnect);
424
- socket.on('disconnect', handleDisconnect);
425
- socket.on('error', handleError);
426
- socket.on('session_update', handleSessionUpdate);
427
- handlersSetupRef.current = true;
428
- if (__DEV__) {
429
- console.log('[useSessionSocket] Event handlers set up', {
450
+ // Register handlers on current socket
451
+ socket.on('connect', handleConnect);
452
+ socket.on('disconnect', handleDisconnect);
453
+ socket.on('error', handleError);
454
+ socket.on('connect_error', handleConnectError);
455
+ socket.on('session_update', handleSessionUpdate);
456
+ handlersSetupRef.current = true;
457
+ lastRegisteredSocketIdRef.current = currentSocketId;
458
+ logger.debug('Event handlers set up', {
459
+ component: 'useSessionSocket',
430
460
  socketId: socket.id,
431
461
  userId
432
462
  });
433
463
  }
434
- }
435
-
436
- // Ensure socket is connected before proceeding
437
- if (!socket.connected) {
438
- if (__DEV__) {
439
- console.log('[useSessionSocket] Socket not connected, connecting...', {
440
- userId
441
- });
442
- logger.debug('Socket not connected, waiting for connection', {
464
+ if (!socket.connected) {
465
+ logger.debug('Socket not connected, connecting...', {
443
466
  component: 'useSessionSocket',
444
467
  userId
445
468
  });
469
+ socket.connect();
446
470
  }
447
- socket.connect();
448
- } else {
449
- if (__DEV__) {
450
- console.log('[useSessionSocket] Socket already connected', {
451
- socketId: socket.id,
452
- userId,
453
- connected: socket.connected
454
- });
455
- }
456
- }
471
+ };
472
+ initializeSocket();
457
473
  return () => {
458
474
  // Only clean up handlers if socket still exists and handlers were set up
459
475
  if (socketRef.current && handlersSetupRef.current) {
460
- socketRef.current.off('connect', handleConnect);
461
- socketRef.current.off('disconnect', handleDisconnect);
462
- socketRef.current.off('error', handleError);
463
- socketRef.current.off('session_update', handleSessionUpdate);
476
+ try {
477
+ socketRef.current.off('connect');
478
+ socketRef.current.off('disconnect');
479
+ socketRef.current.off('error');
480
+ socketRef.current.off('connect_error');
481
+ socketRef.current.off('session_update');
482
+ } catch (error) {
483
+ // Ignore errors when removing handlers
484
+ }
464
485
  handlersSetupRef.current = false;
465
486
  }
466
487
  };
467
- }, [userId, baseURL, getAccessToken]); // Depend on userId, baseURL, and getAccessToken
488
+ }, [userId, baseURL]); // Only depend on userId and baseURL - functions are in refs
468
489
  }
469
490
  //# sourceMappingURL=useSessionSocket.js.map