@oxyhq/services 5.3.5 → 5.3.6

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,6 +8,7 @@ import {
8
8
  ScrollView,
9
9
  Alert,
10
10
  Platform,
11
+ Image,
11
12
  Dimensions,
12
13
  } from 'react-native';
13
14
  import { BaseScreenProps } from '../navigation/types';
@@ -16,6 +17,11 @@ import { SecureClientSession } from '../../models/secureSession';
16
17
  import { fontFamilies } from '../styles/fonts';
17
18
  import { User } from '../../models/interfaces';
18
19
 
20
+ interface SessionWithUser extends SecureClientSession {
21
+ userProfile?: User;
22
+ isLoadingProfile?: boolean;
23
+ }
24
+
19
25
  interface DeviceSession {
20
26
  sessionId: string;
21
27
  deviceId: string;
@@ -26,7 +32,7 @@ interface DeviceSession {
26
32
  isCurrent: boolean;
27
33
  }
28
34
 
29
- const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
35
+ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
30
36
  onClose,
31
37
  theme,
32
38
  navigate,
@@ -43,11 +49,16 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
43
49
  isLoading
44
50
  } = useOxy();
45
51
 
46
- const [allDeviceSessions, setAllDeviceSessions] = useState<DeviceSession[]>([]);
47
- const [loadingDeviceSessions, setLoadingDeviceSessions] = useState(false);
52
+ const [sessionsWithUsers, setSessionsWithUsers] = useState<SessionWithUser[]>([]);
48
53
  const [switchingToUserId, setSwitchingToUserId] = useState<string | null>(null);
49
54
  const [removingUserId, setRemovingUserId] = useState<string | null>(null);
50
- const [showRemoteDevices, setShowRemoteDevices] = useState(false);
55
+
56
+ // Device session management state
57
+ const [showDeviceManagement, setShowDeviceManagement] = useState(false);
58
+ const [deviceSessions, setDeviceSessions] = useState<DeviceSession[]>([]);
59
+ const [loadingDeviceSessions, setLoadingDeviceSessions] = useState(false);
60
+ const [remotingLogoutSessionId, setRemoteLogoutSessionId] = useState<string | null>(null);
61
+ const [loggingOutAllDevices, setLoggingOutAllDevices] = useState(false);
51
62
 
52
63
  const screenWidth = Dimensions.get('window').width;
53
64
  const isDarkTheme = theme === 'dark';
@@ -67,37 +78,46 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
67
78
  shadow: isDarkTheme ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.1)',
68
79
  };
69
80
 
70
- // Load all device sessions for remote management
71
- const loadAllDeviceSessions = async () => {
72
- if (!activeSessionId || !oxyServices) return;
73
-
74
- setLoadingDeviceSessions(true);
75
- try {
76
- const allSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
77
- const deviceSessions: DeviceSession[] = allSessions.map((session: any) => ({
78
- sessionId: session.sessionId,
79
- deviceId: session.deviceId,
80
- deviceName: session.deviceName || 'Unknown Device',
81
- isActive: session.isActive,
82
- lastActive: session.lastActive || new Date().toISOString(),
83
- expiresAt: session.expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
84
- isCurrent: session.sessionId === activeSessionId
81
+ // Load user profiles for sessions
82
+ useEffect(() => {
83
+ const loadUserProfiles = async () => {
84
+ if (!sessions.length || !oxyServices) return;
85
+
86
+ const updatedSessions: SessionWithUser[] = sessions.map(session => ({
87
+ ...session,
88
+ isLoadingProfile: true,
85
89
  }));
86
- setAllDeviceSessions(deviceSessions);
87
- } catch (error) {
88
- console.error('Failed to load device sessions:', error);
89
- Alert.alert('Error', 'Failed to load device sessions');
90
- } finally {
91
- setLoadingDeviceSessions(false);
92
- }
93
- };
90
+ setSessionsWithUsers(updatedSessions);
94
91
 
95
- // Load device sessions when showing remote devices
96
- useEffect(() => {
97
- if (showRemoteDevices) {
98
- loadAllDeviceSessions();
99
- }
100
- }, [showRemoteDevices, activeSessionId, oxyServices]);
92
+ // Load profiles for each session
93
+ for (let i = 0; i < sessions.length; i++) {
94
+ const session = sessions[i];
95
+ try {
96
+ // Try to get user profile using the session
97
+ const userProfile = await oxyServices.getUserBySession(session.sessionId);
98
+
99
+ setSessionsWithUsers(prev =>
100
+ prev.map(s =>
101
+ s.sessionId === session.sessionId
102
+ ? { ...s, userProfile, isLoadingProfile: false }
103
+ : s
104
+ )
105
+ );
106
+ } catch (error) {
107
+ console.error(`Failed to load profile for session ${session.sessionId}:`, error);
108
+ setSessionsWithUsers(prev =>
109
+ prev.map(s =>
110
+ s.sessionId === session.sessionId
111
+ ? { ...s, isLoadingProfile: false }
112
+ : s
113
+ )
114
+ );
115
+ }
116
+ }
117
+ };
118
+
119
+ loadUserProfiles();
120
+ }, [sessions, oxyServices]);
101
121
 
102
122
  const handleSwitchSession = async (sessionId: string) => {
103
123
  if (sessionId === user?.sessionId) return; // Already active session
@@ -118,12 +138,16 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
118
138
  };
119
139
 
120
140
  const handleRemoveSession = async (sessionId: string) => {
121
- const sessionToRemove = sessions.find(s => s.sessionId === sessionId);
141
+ const sessionToRemove = sessionsWithUsers.find(s => s.sessionId === sessionId);
122
142
  if (!sessionToRemove) return;
123
143
 
144
+ const displayName = typeof sessionToRemove.userProfile?.name === 'object'
145
+ ? sessionToRemove.userProfile.name.full || sessionToRemove.userProfile.name.first || sessionToRemove.userProfile.username
146
+ : sessionToRemove.userProfile?.name || sessionToRemove.userProfile?.username || 'this account';
147
+
124
148
  Alert.alert(
125
149
  'Remove Account',
126
- `Are you sure you want to remove this session from this device? You'll need to sign in again to access this account.`,
150
+ `Are you sure you want to remove ${displayName} from this device? You'll need to sign in again to access this account.`,
127
151
  [
128
152
  {
129
153
  text: 'Cancel',
@@ -180,27 +204,47 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
180
204
  );
181
205
  };
182
206
 
207
+ // Device session management functions
208
+ const loadAllDeviceSessions = async () => {
209
+ if (!oxyServices || !user?.sessionId) return;
210
+
211
+ setLoadingDeviceSessions(true);
212
+ try {
213
+ // This would call the API to get all device sessions for the current user
214
+ const allSessions = await oxyServices.getDeviceSessions(user.sessionId);
215
+ setDeviceSessions(allSessions || []);
216
+ } catch (error) {
217
+ console.error('Failed to load device sessions:', error);
218
+ Alert.alert('Error', 'Failed to load device sessions. Please try again.');
219
+ } finally {
220
+ setLoadingDeviceSessions(false);
221
+ }
222
+ };
223
+
183
224
  const handleRemoteSessionLogout = async (sessionId: string, deviceName: string) => {
184
225
  Alert.alert(
185
- 'Logout Remote Session',
186
- `Are you sure you want to logout from "${deviceName}"?`,
226
+ 'Remove Device Session',
227
+ `Are you sure you want to sign out from "${deviceName}"? This will end the session on that device.`,
187
228
  [
188
229
  {
189
230
  text: 'Cancel',
190
231
  style: 'cancel',
191
232
  },
192
233
  {
193
- text: 'Logout',
234
+ text: 'Sign Out',
194
235
  style: 'destructive',
195
236
  onPress: async () => {
237
+ setRemoteLogoutSessionId(sessionId);
196
238
  try {
197
- await oxyServices.logoutSecureSession(activeSessionId!, sessionId);
198
- Alert.alert('Success', 'Remote session logged out successfully!');
199
- // Refresh the device sessions list
239
+ await oxyServices?.logoutSecureSession(user?.sessionId || '', sessionId);
240
+ // Refresh device sessions list
200
241
  await loadAllDeviceSessions();
242
+ Alert.alert('Success', `Signed out from ${deviceName} successfully!`);
201
243
  } catch (error) {
202
244
  console.error('Remote logout failed:', error);
203
- Alert.alert('Logout Failed', 'There was a problem logging out the remote session. Please try again.');
245
+ Alert.alert('Logout Failed', 'There was a problem signing out from the device. Please try again.');
246
+ } finally {
247
+ setRemoteLogoutSessionId(null);
204
248
  }
205
249
  },
206
250
  },
@@ -210,26 +254,36 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
210
254
  };
211
255
 
212
256
  const handleLogoutAllDevices = async () => {
257
+ const otherDevicesCount = deviceSessions.filter(session => !session.isCurrent).length;
258
+
259
+ if (otherDevicesCount === 0) {
260
+ Alert.alert('No Other Devices', 'No other device sessions found to sign out from.');
261
+ return;
262
+ }
263
+
213
264
  Alert.alert(
214
- 'Logout All Devices',
215
- 'Are you sure you want to logout from all other devices? This will keep your current session active.',
265
+ 'Sign Out All Other Devices',
266
+ `Are you sure you want to sign out from all ${otherDevicesCount} other device(s)? This will end sessions on all other devices except this one.`,
216
267
  [
217
268
  {
218
269
  text: 'Cancel',
219
270
  style: 'cancel',
220
271
  },
221
272
  {
222
- text: 'Logout All Others',
273
+ text: 'Sign Out All',
223
274
  style: 'destructive',
224
275
  onPress: async () => {
276
+ setLoggingOutAllDevices(true);
225
277
  try {
226
- await oxyServices.logoutAllDeviceSessions(activeSessionId!, undefined, true);
227
- Alert.alert('Success', 'All other device sessions logged out successfully!');
228
- // Refresh the device sessions list
278
+ await oxyServices?.logoutAllDeviceSessions(user?.sessionId || '', undefined, true);
279
+ // Refresh device sessions list
229
280
  await loadAllDeviceSessions();
281
+ Alert.alert('Success', `Signed out from all other devices successfully!`);
230
282
  } catch (error) {
231
283
  console.error('Logout all devices failed:', error);
232
- Alert.alert('Logout Failed', 'There was a problem logging out other devices. Please try again.');
284
+ Alert.alert('Logout Failed', 'There was a problem signing out from other devices. Please try again.');
285
+ } finally {
286
+ setLoggingOutAllDevices(false);
233
287
  }
234
288
  },
235
289
  },
@@ -238,108 +292,168 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
238
292
  );
239
293
  };
240
294
 
241
- const renderSessionItem = (session: SecureClientSession) => {
242
- const isActive = session.sessionId === activeSessionId;
243
- const isSwitching = switchingToUserId === session.sessionId;
244
- const isRemoving = removingUserId === session.sessionId;
245
-
295
+ const renderDeviceSessionItem = (deviceSession: DeviceSession) => {
296
+ const isLoggingOut = remotingLogoutSessionId === deviceSession.sessionId;
297
+
246
298
  return (
247
299
  <View
248
- key={session.sessionId}
300
+ key={deviceSession.sessionId}
249
301
  style={[
250
- styles.userItem,
302
+ styles.sessionCard,
251
303
  {
252
- backgroundColor: isActive ? colors.activeCard : colors.surface,
253
- borderColor: isActive ? colors.accent : colors.border,
304
+ backgroundColor: deviceSession.isCurrent ? colors.activeCard : colors.card,
305
+ borderColor: deviceSession.isCurrent ? colors.accent : colors.border,
306
+ borderWidth: deviceSession.isCurrent ? 2 : 1,
254
307
  },
255
308
  ]}
256
309
  >
257
- <View style={styles.userInfo}>
258
- <Text style={[styles.username, { color: colors.text }]}>
259
- {isActive ? user?.username || 'Current Account' : 'Account Session'}
260
- </Text>
261
- <Text style={[styles.email, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
262
- Last active: {new Date(session.lastActive).toLocaleDateString()}
263
- </Text>
264
- {isActive && (
265
- <View style={[styles.activeBadge, { backgroundColor: colors.success }]}>
266
- <Text style={styles.activeBadgeText}>Active</Text>
267
- </View>
268
- )}
269
- </View>
270
-
271
- <View style={styles.userActions}>
272
- {!isActive && (
310
+ <View style={styles.sessionHeader}>
311
+ <View style={styles.userInfo}>
312
+ <Text style={[styles.displayName, { color: colors.text }]} numberOfLines={1}>
313
+ {deviceSession.deviceName}
314
+ {deviceSession.isCurrent && (
315
+ <Text style={[styles.username, { color: colors.accent }]}>
316
+ {' (This Device)'}
317
+ </Text>
318
+ )}
319
+ </Text>
320
+ <Text style={[styles.username, { color: colors.secondaryText }]} numberOfLines={1}>
321
+ ID: ...{deviceSession.deviceId.slice(-8)}
322
+ </Text>
323
+ <Text style={[styles.lastActive, { color: colors.secondaryText }]} numberOfLines={1}>
324
+ Last active: {new Date(deviceSession.lastActive).toLocaleDateString()}
325
+ </Text>
326
+ </View>
327
+
328
+ {!deviceSession.isCurrent && (
273
329
  <TouchableOpacity
274
- style={[styles.switchButton, { borderColor: colors.accent }]}
275
- onPress={() => handleSwitchSession(session.sessionId)}
276
- disabled={isSwitching || isRemoving}
330
+ style={[styles.removeButton, {
331
+ borderColor: colors.destructive,
332
+ backgroundColor: colors.background,
333
+ }]}
334
+ onPress={() => handleRemoteSessionLogout(deviceSession.sessionId, deviceSession.deviceName)}
335
+ disabled={isLoggingOut}
277
336
  >
278
- {isSwitching ? (
279
- <ActivityIndicator color={colors.accent} size="small" />
337
+ {isLoggingOut ? (
338
+ <ActivityIndicator color={colors.destructive} size="small" />
280
339
  ) : (
281
- <Text style={[styles.switchButtonText, { color: colors.accent }]}>Switch</Text>
340
+ <Text style={[styles.removeButtonText, { color: colors.destructive }]}>
341
+ Sign Out
342
+ </Text>
282
343
  )}
283
344
  </TouchableOpacity>
284
345
  )}
285
-
286
- <TouchableOpacity
287
- style={[styles.removeButton, { borderColor: colors.destructive }]}
288
- onPress={() => handleRemoveSession(session.sessionId)}
289
- disabled={isSwitching || isRemoving || sessions.length === 1}
290
- >
291
- {isRemoving ? (
292
- <ActivityIndicator color={colors.destructive} size="small" />
293
- ) : (
294
- <Text style={[styles.removeButtonText, { color: colors.destructive }]}>Remove</Text>
295
- )}
296
- </TouchableOpacity>
297
346
  </View>
298
347
  </View>
299
348
  );
300
349
  };
301
350
 
302
- const renderDeviceSessionItem = (deviceSession: DeviceSession) => {
303
- const isCurrentSession = deviceSession.isCurrent;
304
-
351
+ // Load device sessions when device management is shown
352
+ useEffect(() => {
353
+ if (showDeviceManagement && deviceSessions.length === 0) {
354
+ loadAllDeviceSessions();
355
+ }
356
+ }, [showDeviceManagement]);
357
+
358
+ const renderSessionItem = (sessionWithUser: SessionWithUser) => {
359
+ const isActive = sessionWithUser.sessionId === activeSessionId;
360
+ const isSwitching = switchingToUserId === sessionWithUser.sessionId;
361
+ const isRemoving = removingUserId === sessionWithUser.sessionId;
362
+ const { userProfile, isLoadingProfile } = sessionWithUser;
363
+
364
+ const displayName = typeof userProfile?.name === 'object'
365
+ ? userProfile.name.full || userProfile.name.first || userProfile.username
366
+ : userProfile?.name || userProfile?.username || 'Unknown User';
367
+ const username = userProfile?.username || 'unknown';
368
+ const avatarUrl = userProfile?.avatar?.url;
369
+
305
370
  return (
306
371
  <View
307
- key={deviceSession.sessionId}
372
+ key={sessionWithUser.sessionId}
308
373
  style={[
309
- styles.userItem,
374
+ styles.sessionCard,
310
375
  {
311
- backgroundColor: isCurrentSession ? colors.activeCard : colors.surface,
312
- borderColor: isCurrentSession ? colors.accent : colors.border,
376
+ backgroundColor: isActive ? colors.activeCard : colors.card,
377
+ borderColor: isActive ? colors.accent : colors.border,
378
+ borderWidth: isActive ? 2 : 1,
379
+ shadowColor: colors.shadow,
313
380
  },
314
381
  ]}
315
382
  >
316
- <View style={styles.userInfo}>
317
- <Text style={[styles.username, { color: colors.text }]}>
318
- {deviceSession.deviceName}
319
- {isCurrentSession && ' (This Device)'}
320
- </Text>
321
- <Text style={[styles.email, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
322
- Last active: {new Date(deviceSession.lastActive).toLocaleDateString()}
323
- </Text>
324
- <Text style={[styles.email, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
325
- Device ID: {deviceSession.deviceId.substring(0, 8)}...
326
- </Text>
327
- {isCurrentSession && (
328
- <View style={[styles.activeBadge, { backgroundColor: colors.success }]}>
329
- <Text style={styles.activeBadgeText}>Current</Text>
330
- </View>
331
- )}
383
+ <View style={styles.sessionHeader}>
384
+ <View style={styles.avatarContainer}>
385
+ {isLoadingProfile ? (
386
+ <View style={[styles.avatarPlaceholder, { backgroundColor: colors.surface }]}>
387
+ <ActivityIndicator size="small" color={colors.accent} />
388
+ </View>
389
+ ) : avatarUrl ? (
390
+ <Image
391
+ source={{ uri: avatarUrl }}
392
+ style={styles.avatar}
393
+ />
394
+ ) : (
395
+ <View style={[styles.avatarPlaceholder, { backgroundColor: colors.surface }]}>
396
+ <Text style={[styles.avatarText, { color: colors.accent }]}>
397
+ {displayName.charAt(0).toUpperCase()}
398
+ </Text>
399
+ </View>
400
+ )}
401
+ {isActive && (
402
+ <View style={[styles.activeBadge, { backgroundColor: colors.success }]}>
403
+ <Text style={styles.activeBadgeText}>✓</Text>
404
+ </View>
405
+ )}
406
+ </View>
407
+
408
+ <View style={styles.userInfo}>
409
+ <Text style={[styles.displayName, { color: colors.text }]} numberOfLines={1}>
410
+ {displayName}
411
+ </Text>
412
+ <Text style={[styles.username, { color: colors.secondaryText }]} numberOfLines={1}>
413
+ @{username}
414
+ </Text>
415
+ <Text style={[styles.lastActive, { color: colors.secondaryText }]} numberOfLines={1}>
416
+ Last active: {new Date(sessionWithUser.lastActive).toLocaleDateString()}
417
+ </Text>
418
+ </View>
332
419
  </View>
333
420
 
334
- <View style={styles.userActions}>
335
- {!isCurrentSession && (
421
+ <View style={styles.sessionActions}>
422
+ {!isActive && (
336
423
  <TouchableOpacity
337
- style={[styles.removeButton, { borderColor: colors.destructive }]}
338
- onPress={() => handleRemoteSessionLogout(deviceSession.sessionId, deviceSession.deviceName)}
424
+ style={[styles.switchButton, {
425
+ borderColor: colors.accent,
426
+ backgroundColor: colors.background,
427
+ }]}
428
+ onPress={() => handleSwitchSession(sessionWithUser.sessionId)}
429
+ disabled={isSwitching || isRemoving}
339
430
  >
340
- <Text style={[styles.removeButtonText, { color: colors.destructive }]}>Logout</Text>
431
+ {isSwitching ? (
432
+ <ActivityIndicator color={colors.accent} size="small" />
433
+ ) : (
434
+ <Text style={[styles.switchButtonText, { color: colors.accent }]}>
435
+ Switch
436
+ </Text>
437
+ )}
341
438
  </TouchableOpacity>
342
439
  )}
440
+
441
+ <TouchableOpacity
442
+ style={[styles.removeButton, {
443
+ borderColor: colors.destructive,
444
+ backgroundColor: colors.background,
445
+ }]}
446
+ onPress={() => handleRemoveSession(sessionWithUser.sessionId)}
447
+ disabled={isSwitching || isRemoving}
448
+ >
449
+ {isRemoving ? (
450
+ <ActivityIndicator color={colors.destructive} size="small" />
451
+ ) : (
452
+ <Text style={[styles.removeButtonText, { color: colors.destructive }]}>
453
+ Remove
454
+ </Text>
455
+ )}
456
+ </TouchableOpacity>
343
457
  </View>
344
458
  </View>
345
459
  );
@@ -351,103 +465,160 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
351
465
  <TouchableOpacity style={styles.backButton} onPress={goBack}>
352
466
  <Text style={[styles.backButtonText, { color: colors.accent }]}>‹ Back</Text>
353
467
  </TouchableOpacity>
354
- <Text style={[styles.title, { color: colors.text }]}>Account Switcher</Text>
355
- <View style={styles.placeholder} />
468
+ <Text style={[styles.title, { color: colors.text }]}>Accounts</Text>
469
+ <View style={styles.headerSpacer} />
356
470
  </View>
357
471
 
358
- <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContainer}>
359
- <View style={styles.description}>
360
- <Text style={[styles.descriptionText, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
361
- Manage multiple accounts on this device. Switch between accounts or remove them from this device.
362
- </Text>
363
- </View>
364
-
365
- <View style={styles.usersContainer}>
366
- <Text style={[styles.sectionTitle, { color: colors.text }]}>
367
- Local Sessions ({sessions.length})
368
- </Text>
369
-
370
- {sessions.map(renderSessionItem)}
371
- </View>
372
-
373
- {/* Remote Device Management Section */}
374
- <View style={styles.usersContainer}>
375
- <View style={styles.sectionHeader}>
376
- <Text style={[styles.sectionTitle, { color: colors.text }]}>
377
- All Devices & Sessions
472
+ <ScrollView
473
+ style={styles.content}
474
+ showsVerticalScrollIndicator={false}
475
+ contentContainerStyle={styles.scrollContent}
476
+ >
477
+ {isLoading ? (
478
+ <View style={styles.loadingContainer}>
479
+ <ActivityIndicator size="large" color={colors.accent} />
480
+ <Text style={[styles.loadingText, { color: colors.secondaryText }]}>
481
+ Loading accounts...
378
482
  </Text>
379
- <TouchableOpacity
380
- style={[styles.toggleButton, { borderColor: colors.accent }]}
381
- onPress={() => setShowRemoteDevices(!showRemoteDevices)}
382
- >
383
- <Text style={[styles.toggleButtonText, { color: colors.accent }]}>
384
- {showRemoteDevices ? 'Hide' : 'Show'}
385
- </Text>
386
- </TouchableOpacity>
387
483
  </View>
388
-
389
- {showRemoteDevices && (
390
- <>
391
- {loadingDeviceSessions ? (
392
- <View style={styles.loadingContainer}>
393
- <ActivityIndicator color={colors.accent} size="small" />
394
- <Text style={[styles.loadingText, { color: colors.secondaryText }]}>
395
- Loading device sessions...
484
+ ) : (
485
+ <>
486
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>
487
+ Saved Accounts ({sessionsWithUsers.length})
488
+ </Text>
489
+
490
+ {sessionsWithUsers.length === 0 ? (
491
+ <View style={styles.emptyState}>
492
+ <Text style={[styles.emptyText, { color: colors.secondaryText }]}>
493
+ No saved accounts found
494
+ </Text>
495
+ </View>
496
+ ) : (
497
+ sessionsWithUsers.map(renderSessionItem)
498
+ )}
499
+
500
+ <View style={styles.actionsSection}>
501
+ <TouchableOpacity
502
+ style={[styles.actionButton, {
503
+ borderColor: colors.border,
504
+ backgroundColor: colors.card,
505
+ }]}
506
+ onPress={() => navigate?.('SignIn')}
507
+ >
508
+ <Text style={[styles.actionButtonText, { color: colors.text }]}>
509
+ + Add Another Account
510
+ </Text>
511
+ </TouchableOpacity>
512
+
513
+ {sessionsWithUsers.length > 0 && (
514
+ <TouchableOpacity
515
+ style={[styles.actionButton, styles.dangerButton, {
516
+ borderColor: colors.destructive,
517
+ backgroundColor: colors.background,
518
+ }]}
519
+ onPress={handleLogoutAll}
520
+ >
521
+ <Text style={[styles.dangerButtonText, { color: colors.destructive }]}>
522
+ Sign Out All Accounts
396
523
  </Text>
397
- </View>
398
- ) : (
399
- <>
400
- {allDeviceSessions.length > 0 ? (
401
- <>
402
- {allDeviceSessions.map(renderDeviceSessionItem)}
403
- {allDeviceSessions.filter(s => !s.isCurrent).length > 0 && (
404
- <TouchableOpacity
405
- style={[styles.actionButton, { borderColor: colors.destructive, marginTop: 12 }]}
406
- onPress={handleLogoutAllDevices}
407
- >
408
- <Text style={[styles.actionButtonText, { color: colors.destructive }]}>
409
- Logout All Other Devices
410
- </Text>
411
- </TouchableOpacity>
412
- )}
413
- </>
414
- ) : (
415
- <Text style={[styles.emptyText, { color: colors.secondaryText }]}>
416
- No other device sessions found
417
- </Text>
418
- )}
419
- </>
524
+ </TouchableOpacity>
420
525
  )}
421
- </>
422
- )}
423
- </View>
526
+ </View>
424
527
 
425
- <View style={styles.actionsContainer}>
426
- <TouchableOpacity
427
- style={[styles.actionButton, { borderColor: colors.border }]}
428
- onPress={() => navigate('SignIn')}
429
- >
430
- <Text style={[styles.actionButtonText, { color: colors.text }]}>
431
- Add Another Account
432
- </Text>
433
- </TouchableOpacity>
528
+ {/* Device Management Section */}
529
+ <View style={styles.actionsSection}>
530
+ <TouchableOpacity
531
+ style={[styles.actionButton, {
532
+ borderColor: colors.border,
533
+ backgroundColor: colors.card,
534
+ }]}
535
+ onPress={() => setShowDeviceManagement(!showDeviceManagement)}
536
+ >
537
+ <Text style={[styles.actionButtonText, { color: colors.text }]}>
538
+ {showDeviceManagement ? '− Hide Device Management' : '+ Manage Device Sessions'}
539
+ </Text>
540
+ </TouchableOpacity>
541
+ </View>
434
542
 
435
- {sessions.length > 1 && (
436
- <TouchableOpacity
437
- style={[styles.dangerButton, { backgroundColor: isDarkTheme ? '#300000' : '#FFEBEE' }]}
438
- onPress={handleLogoutAll}
439
- disabled={isLoading}
440
- >
441
- {isLoading ? (
442
- <ActivityIndicator color={colors.destructive} size="small" />
443
- ) : (
444
- <Text style={[styles.dangerButtonText, { color: colors.destructive }]}>
445
- Sign Out All Sessions
543
+ {showDeviceManagement && (
544
+ <View style={[styles.deviceManagementSection, {
545
+ backgroundColor: colors.card,
546
+ borderColor: colors.border,
547
+ }]}>
548
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>
549
+ Device Sessions
550
+ </Text>
551
+
552
+ {loadingDeviceSessions ? (
553
+ <View style={styles.loadingContainer}>
554
+ <ActivityIndicator size="large" color={colors.accent} />
555
+ <Text style={[styles.loadingText, { color: colors.secondaryText }]}>
556
+ Loading device sessions...
557
+ </Text>
558
+ </View>
559
+ ) : deviceSessions.length === 0 ? (
560
+ <View style={styles.emptyState}>
561
+ <Text style={[styles.emptyText, { color: colors.secondaryText }]}>
562
+ No device sessions found
563
+ </Text>
564
+ </View>
565
+ ) : (
566
+ <>
567
+ {deviceSessions.map(renderDeviceSessionItem)}
568
+
569
+ {deviceSessions.filter(session => !session.isCurrent).length > 0 && (
570
+ <TouchableOpacity
571
+ style={[styles.actionButton, styles.dangerButton, {
572
+ borderColor: colors.destructive,
573
+ backgroundColor: colors.background,
574
+ marginTop: 20,
575
+ }]}
576
+ onPress={handleLogoutAllDevices}
577
+ disabled={loggingOutAllDevices}
578
+ >
579
+ {loggingOutAllDevices ? (
580
+ <ActivityIndicator color={colors.destructive} size="small" />
581
+ ) : (
582
+ <Text style={[styles.dangerButtonText, { color: colors.destructive }]}>
583
+ Sign Out All Other Devices
584
+ </Text>
585
+ )}
586
+ </TouchableOpacity>
587
+ )}
588
+ </>
589
+ )}
590
+ </View>
591
+ )}
592
+
593
+ <View style={styles.actionsSection}>
594
+ <TouchableOpacity
595
+ style={[styles.actionButton, {
596
+ borderColor: colors.border,
597
+ backgroundColor: colors.card,
598
+ }]}
599
+ onPress={() => navigate?.('SignIn')}
600
+ >
601
+ <Text style={[styles.actionButtonText, { color: colors.text }]}>
602
+ + Add Another Account
446
603
  </Text>
604
+ </TouchableOpacity>
605
+
606
+ {sessionsWithUsers.length > 0 && (
607
+ <TouchableOpacity
608
+ style={[styles.actionButton, styles.dangerButton, {
609
+ borderColor: colors.destructive,
610
+ backgroundColor: colors.background,
611
+ }]}
612
+ onPress={handleLogoutAll}
613
+ >
614
+ <Text style={[styles.dangerButtonText, { color: colors.destructive }]}>
615
+ Sign Out All Accounts
616
+ </Text>
617
+ </TouchableOpacity>
447
618
  )}
448
- </TouchableOpacity>
449
- )}
450
- </View>
619
+ </View>
620
+ </>
621
+ )}
451
622
  </ScrollView>
452
623
  </View>
453
624
  );
@@ -462,168 +633,187 @@ const styles = StyleSheet.create({
462
633
  alignItems: 'center',
463
634
  justifyContent: 'space-between',
464
635
  paddingHorizontal: 20,
465
- paddingTop: Platform.OS === 'ios' ? 50 : 20,
466
- paddingBottom: 20,
467
- borderBottomWidth: 1,
468
- borderBottomColor: 'rgba(0, 0, 0, 0.1)',
636
+ paddingTop: 20,
637
+ paddingBottom: 10,
469
638
  },
470
639
  backButton: {
471
- paddingVertical: 8,
472
- paddingHorizontal: 4,
640
+ padding: 8,
473
641
  },
474
642
  backButtonText: {
475
643
  fontSize: 18,
476
- fontFamily: fontFamilies.phudu,
644
+ fontFamily: fontFamilies.phuduMedium,
477
645
  },
478
646
  title: {
479
- fontSize: 20,
480
- fontFamily: fontFamilies.phuduSemiBold,
647
+ fontSize: 24,
648
+ fontFamily: fontFamilies.phuduBold,
481
649
  textAlign: 'center',
482
650
  },
483
- placeholder: {
484
- width: 60, // Same as back button to center title
651
+ headerSpacer: {
652
+ width: 40,
485
653
  },
486
- scrollView: {
654
+ content: {
487
655
  flex: 1,
656
+ paddingHorizontal: 20,
488
657
  },
489
- scrollContainer: {
490
- padding: 20,
658
+ scrollContent: {
659
+ paddingBottom: 40,
491
660
  },
492
- description: {
493
- marginBottom: 24,
661
+ loadingContainer: {
662
+ flex: 1,
663
+ justifyContent: 'center',
664
+ alignItems: 'center',
665
+ paddingTop: 60,
494
666
  },
495
- descriptionText: {
496
- fontSize: 14,
667
+ loadingText: {
668
+ marginTop: 16,
669
+ fontSize: 16,
497
670
  fontFamily: fontFamilies.phudu,
498
- lineHeight: 20,
499
- },
500
- usersContainer: {
501
- marginBottom: 32,
502
671
  },
503
672
  sectionTitle: {
504
- fontSize: 16,
673
+ fontSize: 20,
505
674
  fontFamily: fontFamilies.phuduSemiBold,
506
- marginBottom: 16,
675
+ marginBottom: 20,
676
+ marginTop: 10,
677
+ },
678
+ emptyState: {
679
+ alignItems: 'center',
680
+ paddingVertical: 40,
507
681
  },
508
- userItem: {
682
+ emptyText: {
683
+ fontSize: 16,
684
+ fontFamily: fontFamilies.phudu,
685
+ textAlign: 'center',
686
+ },
687
+ sessionCard: {
688
+ borderRadius: 16,
689
+ marginBottom: 16,
690
+ padding: 20,
691
+ shadowOffset: {
692
+ width: 0,
693
+ height: 2,
694
+ },
695
+ shadowOpacity: 0.1,
696
+ shadowRadius: 8,
697
+ elevation: 3,
698
+ },
699
+ sessionHeader: {
509
700
  flexDirection: 'row',
510
701
  alignItems: 'center',
511
- justifyContent: 'space-between',
512
- padding: 16,
513
- borderRadius: 12,
514
- borderWidth: 1,
515
- marginBottom: 12,
702
+ marginBottom: 16,
703
+ },
704
+ avatarContainer: {
705
+ position: 'relative',
706
+ marginRight: 16,
707
+ },
708
+ avatar: {
709
+ width: 60,
710
+ height: 60,
711
+ borderRadius: 30,
712
+ },
713
+ avatarPlaceholder: {
714
+ width: 60,
715
+ height: 60,
716
+ borderRadius: 30,
717
+ justifyContent: 'center',
718
+ alignItems: 'center',
719
+ },
720
+ avatarText: {
721
+ fontSize: 24,
722
+ fontFamily: fontFamilies.phuduBold,
723
+ },
724
+ activeBadge: {
725
+ position: 'absolute',
726
+ bottom: -2,
727
+ right: -2,
728
+ width: 20,
729
+ height: 20,
730
+ borderRadius: 10,
731
+ justifyContent: 'center',
732
+ alignItems: 'center',
733
+ },
734
+ activeBadgeText: {
735
+ color: 'white',
736
+ fontSize: 12,
737
+ fontFamily: fontFamilies.phuduBold,
516
738
  },
517
739
  userInfo: {
518
740
  flex: 1,
741
+ justifyContent: 'center',
519
742
  },
520
- username: {
521
- fontSize: 16,
743
+ displayName: {
744
+ fontSize: 18,
522
745
  fontFamily: fontFamilies.phuduSemiBold,
523
746
  marginBottom: 4,
524
747
  },
525
- email: {
748
+ username: {
526
749
  fontSize: 14,
527
750
  fontFamily: fontFamilies.phudu,
528
- marginBottom: 8,
529
- },
530
- activeBadge: {
531
- paddingHorizontal: 8,
532
- paddingVertical: 4,
533
- borderRadius: 6,
534
- alignSelf: 'flex-start',
751
+ marginBottom: 4,
535
752
  },
536
- activeBadgeText: {
537
- color: '#FFFFFF',
753
+ lastActive: {
538
754
  fontSize: 12,
539
- fontFamily: fontFamilies.phuduMedium,
755
+ fontFamily: fontFamilies.phudu,
540
756
  },
541
- userActions: {
757
+ sessionActions: {
542
758
  flexDirection: 'row',
543
- gap: 8,
759
+ justifyContent: 'space-between',
760
+ gap: 12,
544
761
  },
545
762
  switchButton: {
546
- paddingHorizontal: 16,
547
- paddingVertical: 8,
548
- borderRadius: 8,
763
+ flex: 1,
764
+ paddingVertical: 12,
765
+ paddingHorizontal: 20,
549
766
  borderWidth: 1,
550
- minWidth: 70,
767
+ borderRadius: 8,
551
768
  alignItems: 'center',
769
+ justifyContent: 'center',
552
770
  },
553
771
  switchButtonText: {
554
772
  fontSize: 14,
555
- fontFamily: fontFamilies.phuduMedium,
773
+ fontFamily: fontFamilies.phuduSemiBold,
556
774
  },
557
775
  removeButton: {
558
- paddingHorizontal: 16,
559
- paddingVertical: 8,
560
- borderRadius: 8,
776
+ flex: 1,
777
+ paddingVertical: 12,
778
+ paddingHorizontal: 20,
561
779
  borderWidth: 1,
562
- minWidth: 70,
780
+ borderRadius: 8,
563
781
  alignItems: 'center',
782
+ justifyContent: 'center',
564
783
  },
565
784
  removeButtonText: {
566
785
  fontSize: 14,
567
- fontFamily: fontFamilies.phuduMedium,
786
+ fontFamily: fontFamilies.phuduSemiBold,
568
787
  },
569
- actionsContainer: {
788
+ actionsSection: {
789
+ marginTop: 40,
570
790
  gap: 16,
571
791
  },
572
792
  actionButton: {
573
793
  paddingVertical: 16,
574
794
  paddingHorizontal: 20,
575
- borderRadius: 12,
576
795
  borderWidth: 1,
796
+ borderRadius: 12,
577
797
  alignItems: 'center',
798
+ justifyContent: 'center',
578
799
  },
579
800
  actionButtonText: {
580
801
  fontSize: 16,
581
- fontFamily: fontFamilies.phuduMedium,
802
+ fontFamily: fontFamilies.phuduSemiBold,
582
803
  },
583
804
  dangerButton: {
584
- paddingVertical: 16,
585
- paddingHorizontal: 20,
586
- borderRadius: 12,
587
- alignItems: 'center',
805
+ // Additional styles for danger buttons if needed
588
806
  },
589
807
  dangerButtonText: {
590
808
  fontSize: 16,
591
- fontFamily: fontFamilies.phuduMedium,
592
- },
593
- sectionHeader: {
594
- flexDirection: 'row',
595
- alignItems: 'center',
596
- justifyContent: 'space-between',
597
- marginBottom: 16,
809
+ fontFamily: fontFamilies.phuduSemiBold,
598
810
  },
599
- toggleButton: {
600
- paddingHorizontal: 12,
601
- paddingVertical: 6,
602
- borderRadius: 6,
811
+ deviceManagementSection: {
812
+ marginTop: 20,
813
+ padding: 16,
814
+ borderRadius: 12,
603
815
  borderWidth: 1,
604
816
  },
605
- toggleButtonText: {
606
- fontSize: 14,
607
- fontFamily: fontFamilies.phuduMedium,
608
- },
609
- loadingContainer: {
610
- flexDirection: 'row',
611
- alignItems: 'center',
612
- justifyContent: 'center',
613
- paddingVertical: 20,
614
- gap: 8,
615
- },
616
- loadingText: {
617
- fontSize: 14,
618
- fontFamily: fontFamilies.phudu,
619
- },
620
- emptyText: {
621
- fontSize: 14,
622
- fontFamily: fontFamilies.phudu,
623
- textAlign: 'center',
624
- paddingVertical: 20,
625
- fontStyle: 'italic',
626
- },
627
817
  });
628
818
 
629
- export default AccountSwitcherScreen;
819
+ export default ModernAccountSwitcherScreen;