@oxyhq/services 5.13.30 → 5.13.32

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 (26) hide show
  1. package/lib/commonjs/ui/components/OxyProvider.js +2 -2
  2. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  3. package/lib/commonjs/ui/context/OxyContext.js +19 -1
  4. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  5. package/lib/commonjs/ui/hooks/useSessionSocket.js +44 -9
  6. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  7. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +150 -13
  8. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  9. package/lib/module/ui/components/OxyProvider.js +2 -2
  10. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  11. package/lib/module/ui/context/OxyContext.js +19 -1
  12. package/lib/module/ui/context/OxyContext.js.map +1 -1
  13. package/lib/module/ui/hooks/useSessionSocket.js +44 -9
  14. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  15. package/lib/module/ui/screens/AccountSettingsScreen.js +150 -13
  16. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  17. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  18. package/lib/typescript/ui/hooks/useSessionSocket.d.ts +2 -1
  19. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  20. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts +1 -0
  21. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/ui/components/OxyProvider.tsx +2 -2
  24. package/src/ui/context/OxyContext.tsx +23 -1
  25. package/src/ui/hooks/useSessionSocket.ts +45 -10
  26. package/src/ui/screens/AccountSettingsScreen.tsx +158 -18
@@ -8,18 +8,20 @@ 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;
13
14
  onSessionRemoved?: (sessionId: string) => void;
14
15
  }
15
16
 
16
- export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
17
+ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
17
18
  const socketRef = useRef<any>(null);
18
19
  const joinedRoomRef = useRef<string | null>(null);
19
20
 
20
21
  // Store callbacks in refs to avoid re-joining when they change
21
22
  const refreshSessionsRef = useRef(refreshSessions);
22
23
  const logoutRef = useRef(logout);
24
+ const clearSessionStateRef = useRef(clearSessionState);
23
25
  const onRemoteSignOutRef = useRef(onRemoteSignOut);
24
26
  const onSessionRemovedRef = useRef(onSessionRemoved);
25
27
  const activeSessionIdRef = useRef(activeSessionId);
@@ -29,11 +31,12 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
29
31
  useEffect(() => {
30
32
  refreshSessionsRef.current = refreshSessions;
31
33
  logoutRef.current = logout;
34
+ clearSessionStateRef.current = clearSessionState;
32
35
  onRemoteSignOutRef.current = onRemoteSignOut;
33
36
  onSessionRemovedRef.current = onSessionRemoved;
34
37
  activeSessionIdRef.current = activeSessionId;
35
38
  currentDeviceIdRef.current = currentDeviceId;
36
- }, [refreshSessions, logout, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
39
+ }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
37
40
 
38
41
  useEffect(() => {
39
42
  if (!userId || !baseURL) {
@@ -77,7 +80,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
77
80
  }
78
81
  };
79
82
 
80
- const handleSessionUpdate = (data: {
83
+ const handleSessionUpdate = async (data: {
81
84
  type: string;
82
85
  sessionId?: string;
83
86
  deviceId?: string;
@@ -97,14 +100,22 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
97
100
  onSessionRemovedRef.current(data.sessionId);
98
101
  }
99
102
 
100
- // If the removed sessionId matches the current activeSessionId, immediately logout
103
+ // If the removed sessionId matches the current activeSessionId, immediately clear state
101
104
  if (data.sessionId === currentActiveSessionId) {
102
105
  if (onRemoteSignOutRef.current) {
103
106
  onRemoteSignOutRef.current();
104
107
  } else {
105
108
  toast.info('You have been signed out remotely.');
106
109
  }
107
- logoutRef.current();
110
+ // Use clearSessionState since session was already removed server-side
111
+ // Await to ensure storage cleanup completes before continuing
112
+ try {
113
+ await clearSessionStateRef.current();
114
+ } catch (error) {
115
+ if (__DEV__) {
116
+ console.error('Failed to clear session state after session_removed:', error);
117
+ }
118
+ }
108
119
  } else {
109
120
  // Otherwise, just refresh the sessions list (with error handling)
110
121
  refreshSessionsRef.current().catch((error) => {
@@ -122,14 +133,22 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
122
133
  }
123
134
  }
124
135
 
125
- // If the removed deviceId matches the current device, immediately logout
136
+ // If the removed deviceId matches the current device, immediately clear state
126
137
  if (data.deviceId && data.deviceId === currentDeviceId) {
127
138
  if (onRemoteSignOutRef.current) {
128
139
  onRemoteSignOutRef.current();
129
140
  } else {
130
141
  toast.info('This device has been removed. You have been signed out.');
131
142
  }
132
- logoutRef.current();
143
+ // Use clearSessionState since sessions were already removed server-side
144
+ // Await to ensure storage cleanup completes before continuing
145
+ try {
146
+ await clearSessionStateRef.current();
147
+ } catch (error) {
148
+ if (__DEV__) {
149
+ console.error('Failed to clear session state after device_removed:', error);
150
+ }
151
+ }
133
152
  } else {
134
153
  // Otherwise, refresh sessions and device list (with error handling)
135
154
  refreshSessionsRef.current().catch((error) => {
@@ -147,14 +166,22 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
147
166
  }
148
167
  }
149
168
 
150
- // If the current activeSessionId is in the removed sessionIds list, immediately logout
169
+ // If the current activeSessionId is in the removed sessionIds list, immediately clear state
151
170
  if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
152
171
  if (onRemoteSignOutRef.current) {
153
172
  onRemoteSignOutRef.current();
154
173
  } else {
155
174
  toast.info('You have been signed out remotely.');
156
175
  }
157
- logoutRef.current();
176
+ // Use clearSessionState since sessions were already removed server-side
177
+ // Await to ensure storage cleanup completes before continuing
178
+ try {
179
+ await clearSessionStateRef.current();
180
+ } catch (error) {
181
+ if (__DEV__) {
182
+ console.error('Failed to clear session state after sessions_removed:', error);
183
+ }
184
+ }
158
185
  } else {
159
186
  // Otherwise, refresh sessions list (with error handling)
160
187
  refreshSessionsRef.current().catch((error) => {
@@ -180,7 +207,15 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
180
207
  } else {
181
208
  toast.info('You have been signed out remotely.');
182
209
  }
183
- logoutRef.current();
210
+ // Use clearSessionState since session was already removed server-side
211
+ // Await to ensure storage cleanup completes before continuing
212
+ try {
213
+ await clearSessionStateRef.current();
214
+ } catch (error) {
215
+ if (__DEV__) {
216
+ console.error('Failed to clear session state after session_update:', error);
217
+ }
218
+ }
184
219
  }
185
220
  }
186
221
  };
@@ -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>