@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.
- package/lib/commonjs/core/mixins/OxyServices.user.js +14 -4
- package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +40 -75
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +7 -6
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +4 -3
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +349 -328
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +13 -6
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.user.js +14 -4
- package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +40 -75
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +7 -6
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +4 -3
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +349 -328
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js +13 -6
- package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.user.d.ts +2 -2
- package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/mixins/OxyServices.user.ts +14 -4
- package/src/ui/context/OxyContext.tsx +39 -75
- package/src/ui/hooks/mutations/useAccountMutations.ts +8 -6
- package/src/ui/hooks/queries/useAccountQueries.ts +4 -2
- package/src/ui/hooks/useSessionSocket.ts +153 -155
- 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
|
-
|
|
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
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
159
|
-
const currentDeviceId = currentDeviceIdRef.current;
|
|
210
|
+
const currentActiveSessionId = activeSessionIdRef.current;
|
|
211
|
+
const currentDeviceId = currentDeviceIdRef.current;
|
|
160
212
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
|
488
|
+
}, [userId, baseURL]); // Only depend on userId and baseURL - functions are in refs
|
|
468
489
|
}
|
|
469
490
|
//# sourceMappingURL=useSessionSocket.js.map
|