@oxyhq/services 5.13.29 → 5.13.31

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.
@@ -8,18 +8,22 @@ interface UseSessionSocketProps {
8
8
  currentDeviceId: string | null | undefined;
9
9
  refreshSessions: () => Promise<void>;
10
10
  logout: () => Promise<void>;
11
+ clearSessionState: () => Promise<void>;
11
12
  baseURL: string;
12
13
  onRemoteSignOut?: () => void;
14
+ onSessionRemoved?: (sessionId: string) => void;
13
15
  }
14
16
 
15
- export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, baseURL, onRemoteSignOut }: UseSessionSocketProps) {
17
+ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
16
18
  const socketRef = useRef<any>(null);
17
19
  const joinedRoomRef = useRef<string | null>(null);
18
20
 
19
21
  // Store callbacks in refs to avoid re-joining when they change
20
22
  const refreshSessionsRef = useRef(refreshSessions);
21
23
  const logoutRef = useRef(logout);
24
+ const clearSessionStateRef = useRef(clearSessionState);
22
25
  const onRemoteSignOutRef = useRef(onRemoteSignOut);
26
+ const onSessionRemovedRef = useRef(onSessionRemoved);
23
27
  const activeSessionIdRef = useRef(activeSessionId);
24
28
  const currentDeviceIdRef = useRef(currentDeviceId);
25
29
 
@@ -27,10 +31,12 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
27
31
  useEffect(() => {
28
32
  refreshSessionsRef.current = refreshSessions;
29
33
  logoutRef.current = logout;
34
+ clearSessionStateRef.current = clearSessionState;
30
35
  onRemoteSignOutRef.current = onRemoteSignOut;
36
+ onSessionRemovedRef.current = onSessionRemoved;
31
37
  activeSessionIdRef.current = activeSessionId;
32
38
  currentDeviceIdRef.current = currentDeviceId;
33
- }, [refreshSessions, logout, onRemoteSignOut, activeSessionId, currentDeviceId]);
39
+ }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
34
40
 
35
41
  useEffect(() => {
36
42
  if (!userId || !baseURL) {
@@ -89,47 +95,89 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
89
95
 
90
96
  // Handle different event types
91
97
  if (data.type === 'session_removed') {
92
- // If the removed sessionId matches the current activeSessionId, immediately logout
98
+ // Track removed session
99
+ if (data.sessionId && onSessionRemovedRef.current) {
100
+ onSessionRemovedRef.current(data.sessionId);
101
+ }
102
+
103
+ // If the removed sessionId matches the current activeSessionId, immediately clear state
93
104
  if (data.sessionId === currentActiveSessionId) {
94
105
  if (onRemoteSignOutRef.current) {
95
106
  onRemoteSignOutRef.current();
96
107
  } else {
97
108
  toast.info('You have been signed out remotely.');
98
109
  }
99
- logoutRef.current();
110
+ // Use clearSessionState since session was already removed server-side
111
+ clearSessionStateRef.current();
100
112
  } else {
101
- // Otherwise, just refresh the sessions list
102
- refreshSessionsRef.current();
113
+ // Otherwise, just refresh the sessions list (with error handling)
114
+ refreshSessionsRef.current().catch((error) => {
115
+ // Silently handle errors from refresh - they're expected if sessions were removed
116
+ if (__DEV__) {
117
+ console.debug('Failed to refresh sessions after session_removed:', error);
118
+ }
119
+ });
103
120
  }
104
121
  } else if (data.type === 'device_removed') {
105
- // If the removed deviceId matches the current device, immediately logout
122
+ // Track all removed sessions from this device
123
+ if (data.sessionIds && onSessionRemovedRef.current) {
124
+ for (const sessionId of data.sessionIds) {
125
+ onSessionRemovedRef.current(sessionId);
126
+ }
127
+ }
128
+
129
+ // If the removed deviceId matches the current device, immediately clear state
106
130
  if (data.deviceId && data.deviceId === currentDeviceId) {
107
131
  if (onRemoteSignOutRef.current) {
108
132
  onRemoteSignOutRef.current();
109
133
  } else {
110
134
  toast.info('This device has been removed. You have been signed out.');
111
135
  }
112
- logoutRef.current();
136
+ // Use clearSessionState since sessions were already removed server-side
137
+ clearSessionStateRef.current();
113
138
  } else {
114
- // Otherwise, refresh sessions and device list
115
- refreshSessionsRef.current();
139
+ // Otherwise, refresh sessions and device list (with error handling)
140
+ refreshSessionsRef.current().catch((error) => {
141
+ // Silently handle errors from refresh - they're expected if sessions were removed
142
+ if (__DEV__) {
143
+ console.debug('Failed to refresh sessions after device_removed:', error);
144
+ }
145
+ });
116
146
  }
117
147
  } else if (data.type === 'sessions_removed') {
118
- // If the current activeSessionId is in the removed sessionIds list, immediately logout
148
+ // Track all removed sessions
149
+ if (data.sessionIds && onSessionRemovedRef.current) {
150
+ for (const sessionId of data.sessionIds) {
151
+ onSessionRemovedRef.current(sessionId);
152
+ }
153
+ }
154
+
155
+ // If the current activeSessionId is in the removed sessionIds list, immediately clear state
119
156
  if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
120
157
  if (onRemoteSignOutRef.current) {
121
158
  onRemoteSignOutRef.current();
122
159
  } else {
123
160
  toast.info('You have been signed out remotely.');
124
161
  }
125
- logoutRef.current();
162
+ // Use clearSessionState since sessions were already removed server-side
163
+ clearSessionStateRef.current();
126
164
  } else {
127
- // Otherwise, refresh sessions list
128
- refreshSessionsRef.current();
165
+ // Otherwise, refresh sessions list (with error handling)
166
+ refreshSessionsRef.current().catch((error) => {
167
+ // Silently handle errors from refresh - they're expected if sessions were removed
168
+ if (__DEV__) {
169
+ console.debug('Failed to refresh sessions after sessions_removed:', error);
170
+ }
171
+ });
129
172
  }
130
173
  } else {
131
- // For other event types (e.g., session_created), refresh sessions
132
- refreshSessionsRef.current();
174
+ // For other event types (e.g., session_created), refresh sessions (with error handling)
175
+ refreshSessionsRef.current().catch((error) => {
176
+ // Log but don't throw - refresh errors shouldn't break the socket handler
177
+ if (__DEV__) {
178
+ console.debug('Failed to refresh sessions after session_update:', error);
179
+ }
180
+ });
133
181
 
134
182
  // If the current session was logged out (legacy behavior), handle it specially
135
183
  if (data.sessionId === currentActiveSessionId) {
@@ -138,7 +186,8 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
138
186
  } else {
139
187
  toast.info('You have been signed out remotely.');
140
188
  }
141
- logoutRef.current();
189
+ // Use clearSessionState since session was already removed server-side
190
+ clearSessionStateRef.current();
142
191
  }
143
192
  }
144
193
  };
@@ -33,12 +33,13 @@ const locationSearchCache = new TTLCache<any[]>(60 * 60 * 1000); // 1 hour cache
33
33
  registerCacheForCleanup(linkMetadataCache);
34
34
  registerCacheForCleanup(locationSearchCache);
35
35
 
36
- const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string }> = ({
36
+ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string; initialSection?: string }> = ({
37
37
  onClose,
38
38
  theme,
39
39
  goBack,
40
40
  navigate,
41
41
  initialField,
42
+ initialSection,
42
43
  }) => {
43
44
  const { user: userFromContext, oxyServices, isLoading: authLoading, isAuthenticated, showBottomSheet, activeSessionId } = useOxy();
44
45
  const { t } = useI18n();
@@ -52,6 +53,20 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
52
53
  const scrollViewRef = useRef<ScrollView>(null);
53
54
  const avatarSectionRef = useRef<View>(null);
54
55
  const [avatarSectionY, setAvatarSectionY] = useState<number | null>(null);
56
+
57
+ // Section refs for navigation
58
+ const profilePictureSectionRef = useRef<View>(null);
59
+ const basicInfoSectionRef = useRef<View>(null);
60
+ const aboutSectionRef = useRef<View>(null);
61
+ const quickActionsSectionRef = useRef<View>(null);
62
+ const securitySectionRef = useRef<View>(null);
63
+
64
+ // Section Y positions for scrolling
65
+ const [profilePictureSectionY, setProfilePictureSectionY] = useState<number | null>(null);
66
+ const [basicInfoSectionY, setBasicInfoSectionY] = useState<number | null>(null);
67
+ const [aboutSectionY, setAboutSectionY] = useState<number | null>(null);
68
+ const [quickActionsSectionY, setQuickActionsSectionY] = useState<number | null>(null);
69
+ const [securitySectionY, setSecuritySectionY] = useState<number | null>(null);
55
70
 
56
71
  // Two-Factor (TOTP) state
57
72
  const [totpSetupUrl, setTotpSetupUrl] = useState<string | null>(null);
@@ -246,19 +261,69 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
246
261
  // Use a ref to track if we've already set the initial field to avoid loops
247
262
  const hasSetInitialFieldRef = useRef(false);
248
263
  const previousInitialFieldRef = useRef<string | undefined>(undefined);
264
+ const initialFieldTimeoutRef = useRef<NodeJS.Timeout | null>(null);
265
+
266
+ // Delay constant for scroll completion
267
+ const SCROLL_DELAY_MS = 600;
268
+
269
+ // Helper to get current value for a field
270
+ const getFieldCurrentValue = useCallback((field: string): string => {
271
+ switch (field) {
272
+ case 'displayName':
273
+ return displayName;
274
+ case 'username':
275
+ return username;
276
+ case 'email':
277
+ return email;
278
+ case 'bio':
279
+ return bio;
280
+ case 'location':
281
+ case 'links':
282
+ case 'twoFactor':
283
+ return '';
284
+ default:
285
+ return '';
286
+ }
287
+ }, [displayName, username, email, bio]);
288
+
289
+ // Handle initialSection prop to scroll to specific section
290
+ const hasScrolledToSectionRef = useRef(false);
291
+ const previousInitialSectionRef = useRef<string | undefined>(undefined);
292
+ const SCROLL_OFFSET = 100; // Offset to show section near top of viewport
293
+
294
+ // Map section names to their Y positions
295
+ const sectionYPositions = useMemo(() => ({
296
+ profilePicture: profilePictureSectionY,
297
+ basicInfo: basicInfoSectionY,
298
+ about: aboutSectionY,
299
+ quickActions: quickActionsSectionY,
300
+ security: securitySectionY,
301
+ }), [profilePictureSectionY, basicInfoSectionY, aboutSectionY, quickActionsSectionY, securitySectionY]);
302
+
249
303
  useEffect(() => {
250
- // If initialField changed, reset the flag
251
- if (previousInitialFieldRef.current !== initialField) {
252
- hasSetInitialFieldRef.current = false;
253
- previousInitialFieldRef.current = initialField;
304
+ // If initialSection changed, reset the flag
305
+ if (previousInitialSectionRef.current !== initialSection) {
306
+ hasScrolledToSectionRef.current = false;
307
+ previousInitialSectionRef.current = initialSection;
254
308
  }
255
-
256
- // Set the editing field if initialField is provided and we haven't set it yet
257
- if (initialField && !hasSetInitialFieldRef.current) {
258
- setEditingField(initialField);
259
- hasSetInitialFieldRef.current = true;
309
+
310
+ // Scroll to the specified section if initialSection is provided and we haven't scrolled yet
311
+ if (initialSection && !hasScrolledToSectionRef.current) {
312
+ const sectionY = sectionYPositions[initialSection as keyof typeof sectionYPositions];
313
+
314
+ if (sectionY !== null && sectionY !== undefined && scrollViewRef.current) {
315
+ requestAnimationFrame(() => {
316
+ requestAnimationFrame(() => {
317
+ scrollViewRef.current?.scrollTo({
318
+ y: Math.max(0, sectionY - SCROLL_OFFSET),
319
+ animated: true,
320
+ });
321
+ hasScrolledToSectionRef.current = true;
322
+ });
323
+ });
324
+ }
260
325
  }
261
- }, [initialField]);
326
+ }, [initialSection, sectionYPositions]);
262
327
 
263
328
  const handleSave = async () => {
264
329
  if (!user) return;
@@ -400,7 +465,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
400
465
  });
401
466
  }, [showBottomSheet, oxyServices, avatarFileId, updateUser, user]);
402
467
 
403
- const startEditing = (type: string, currentValue: string) => {
468
+ const startEditing = useCallback((type: string, currentValue: string) => {
404
469
  switch (type) {
405
470
  case 'displayName':
406
471
  setTempDisplayName(displayName);
@@ -429,7 +494,50 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
429
494
  break;
430
495
  }
431
496
  setEditingField(type);
432
- };
497
+ }, [displayName, lastName]);
498
+
499
+ // Handle initialField prop - must be after startEditing and openAvatarPicker are declared
500
+ useEffect(() => {
501
+ // Clear any pending timeout
502
+ if (initialFieldTimeoutRef.current) {
503
+ clearTimeout(initialFieldTimeoutRef.current);
504
+ initialFieldTimeoutRef.current = null;
505
+ }
506
+
507
+ // If initialField changed, reset the flag
508
+ if (previousInitialFieldRef.current !== initialField) {
509
+ hasSetInitialFieldRef.current = false;
510
+ previousInitialFieldRef.current = initialField;
511
+ }
512
+
513
+ // Set the editing field if initialField is provided and we haven't set it yet
514
+ if (initialField && !hasSetInitialFieldRef.current) {
515
+ // Special handling for avatar - open avatar picker directly
516
+ if (initialField === 'avatar') {
517
+ // Wait for section to be scrolled, then open picker
518
+ initialFieldTimeoutRef.current = setTimeout(() => {
519
+ openAvatarPicker();
520
+ hasSetInitialFieldRef.current = true;
521
+ }, SCROLL_DELAY_MS);
522
+ } else {
523
+ // For other fields, get current value and start editing after scroll
524
+ const currentValue = getFieldCurrentValue(initialField);
525
+
526
+ // Wait for section to be scrolled, then start editing
527
+ initialFieldTimeoutRef.current = setTimeout(() => {
528
+ startEditing(initialField, currentValue);
529
+ hasSetInitialFieldRef.current = true;
530
+ }, SCROLL_DELAY_MS);
531
+ }
532
+ }
533
+
534
+ return () => {
535
+ if (initialFieldTimeoutRef.current) {
536
+ clearTimeout(initialFieldTimeoutRef.current);
537
+ initialFieldTimeoutRef.current = null;
538
+ }
539
+ };
540
+ }, [initialField, getFieldCurrentValue, startEditing, openAvatarPicker]);
433
541
 
434
542
  const saveField = (type: string) => {
435
543
  animateSaveButton(0.95); // Scale down slightly for animation
@@ -1247,11 +1355,15 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
1247
1355
  )}
1248
1356
  {/* Profile Picture Section */}
1249
1357
  <View
1250
- ref={avatarSectionRef}
1358
+ ref={(ref) => {
1359
+ avatarSectionRef.current = ref;
1360
+ profilePictureSectionRef.current = ref;
1361
+ }}
1251
1362
  style={styles.section}
1252
1363
  onLayout={(event) => {
1253
1364
  const { y } = event.nativeEvent.layout;
1254
1365
  setAvatarSectionY(y);
1366
+ setProfilePictureSectionY(y);
1255
1367
  }}
1256
1368
  >
1257
1369
  <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
@@ -1319,7 +1431,14 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
1319
1431
  </View>
1320
1432
 
1321
1433
  {/* Basic Information */}
1322
- <View style={styles.section}>
1434
+ <View
1435
+ ref={basicInfoSectionRef}
1436
+ style={styles.section}
1437
+ onLayout={(event) => {
1438
+ const { y } = event.nativeEvent.layout;
1439
+ setBasicInfoSectionY(y);
1440
+ }}
1441
+ >
1323
1442
  <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1324
1443
  {t('editProfile.sections.basicInfo') || 'BASIC INFORMATION'}
1325
1444
  </Text>
@@ -1357,7 +1476,14 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
1357
1476
  </View>
1358
1477
 
1359
1478
  {/* About You */}
1360
- <View style={styles.section}>
1479
+ <View
1480
+ ref={aboutSectionRef}
1481
+ style={styles.section}
1482
+ onLayout={(event) => {
1483
+ const { y } = event.nativeEvent.layout;
1484
+ setAboutSectionY(y);
1485
+ }}
1486
+ >
1361
1487
  <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1362
1488
  {t('editProfile.sections.about') || 'ABOUT YOU'}
1363
1489
  </Text>
@@ -1457,7 +1583,14 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
1457
1583
  </View>
1458
1584
 
1459
1585
  {/* Quick Actions */}
1460
- <View style={styles.section}>
1586
+ <View
1587
+ ref={quickActionsSectionRef}
1588
+ style={styles.section}
1589
+ onLayout={(event) => {
1590
+ const { y } = event.nativeEvent.layout;
1591
+ setQuickActionsSectionY(y);
1592
+ }}
1593
+ >
1461
1594
  <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1462
1595
  {t('editProfile.sections.quickActions') || 'QUICK ACTIONS'}
1463
1596
  </Text>
@@ -1495,7 +1628,14 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string
1495
1628
  </View>
1496
1629
 
1497
1630
  {/* Security */}
1498
- <View style={styles.section}>
1631
+ <View
1632
+ ref={securitySectionRef}
1633
+ style={styles.section}
1634
+ onLayout={(event) => {
1635
+ const { y } = event.nativeEvent.layout;
1636
+ setSecuritySectionY(y);
1637
+ }}
1638
+ >
1499
1639
  <Text style={[styles.sectionTitle, { color: themeStyles.isDarkTheme ? '#8E8E93' : '#8E8E93' }]}>
1500
1640
  {t('editProfile.sections.security') || 'SECURITY'}
1501
1641
  </Text>