@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.
@@ -1,552 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import {
3
- View,
4
- Text,
5
- TouchableOpacity,
6
- StyleSheet,
7
- ActivityIndicator,
8
- ScrollView,
9
- Alert,
10
- Platform,
11
- Image,
12
- Dimensions,
13
- } from 'react-native';
14
- import { BaseScreenProps } from '../navigation/types';
15
- import { useOxy } from '../context/OxyContext';
16
- import { SecureClientSession } from '../../models/secureSession';
17
- import { fontFamilies } from '../styles/fonts';
18
- import { User } from '../../models/interfaces';
19
-
20
- interface SessionWithUser extends SecureClientSession {
21
- userProfile?: User;
22
- isLoadingProfile?: boolean;
23
- }
24
-
25
- const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
26
- onClose,
27
- theme,
28
- navigate,
29
- goBack,
30
- oxyServices,
31
- }) => {
32
- const {
33
- user,
34
- sessions,
35
- activeSessionId,
36
- switchSession,
37
- removeSession,
38
- logoutAll,
39
- isLoading
40
- } = useOxy();
41
-
42
- const [sessionsWithUsers, setSessionsWithUsers] = useState<SessionWithUser[]>([]);
43
- const [switchingToUserId, setSwitchingToUserId] = useState<string | null>(null);
44
- const [removingUserId, setRemovingUserId] = useState<string | null>(null);
45
-
46
- const screenWidth = Dimensions.get('window').width;
47
- const isDarkTheme = theme === 'dark';
48
-
49
- // Modern color scheme
50
- const colors = {
51
- background: isDarkTheme ? '#000000' : '#FFFFFF',
52
- surface: isDarkTheme ? '#1C1C1E' : '#F2F2F7',
53
- card: isDarkTheme ? '#2C2C2E' : '#FFFFFF',
54
- text: isDarkTheme ? '#FFFFFF' : '#000000',
55
- secondaryText: isDarkTheme ? '#8E8E93' : '#6D6D70',
56
- accent: '#007AFF',
57
- destructive: '#FF3B30',
58
- success: '#34C759',
59
- border: isDarkTheme ? '#38383A' : '#C6C6C8',
60
- activeCard: isDarkTheme ? '#0A84FF20' : '#007AFF15',
61
- shadow: isDarkTheme ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.1)',
62
- };
63
-
64
- // Load user profiles for sessions
65
- useEffect(() => {
66
- const loadUserProfiles = async () => {
67
- if (!sessions.length || !oxyServices) return;
68
-
69
- const updatedSessions: SessionWithUser[] = sessions.map(session => ({
70
- ...session,
71
- isLoadingProfile: true,
72
- }));
73
- setSessionsWithUsers(updatedSessions);
74
-
75
- // Load profiles for each session
76
- for (let i = 0; i < sessions.length; i++) {
77
- const session = sessions[i];
78
- try {
79
- // Try to get user profile using the session
80
- const userProfile = await oxyServices.getUserBySession(session.sessionId);
81
-
82
- setSessionsWithUsers(prev =>
83
- prev.map(s =>
84
- s.sessionId === session.sessionId
85
- ? { ...s, userProfile, isLoadingProfile: false }
86
- : s
87
- )
88
- );
89
- } catch (error) {
90
- console.error(`Failed to load profile for session ${session.sessionId}:`, error);
91
- setSessionsWithUsers(prev =>
92
- prev.map(s =>
93
- s.sessionId === session.sessionId
94
- ? { ...s, isLoadingProfile: false }
95
- : s
96
- )
97
- );
98
- }
99
- }
100
- };
101
-
102
- loadUserProfiles();
103
- }, [sessions, oxyServices]);
104
-
105
- const handleSwitchSession = async (sessionId: string) => {
106
- if (sessionId === user?.sessionId) return; // Already active session
107
-
108
- setSwitchingToUserId(sessionId);
109
- try {
110
- await switchSession(sessionId);
111
- Alert.alert('Success', 'Account switched successfully!');
112
- if (onClose) {
113
- onClose();
114
- }
115
- } catch (error) {
116
- console.error('Switch session failed:', error);
117
- Alert.alert('Switch Failed', 'There was a problem switching accounts. Please try again.');
118
- } finally {
119
- setSwitchingToUserId(null);
120
- }
121
- };
122
-
123
- const handleRemoveSession = async (sessionId: string) => {
124
- const sessionToRemove = sessionsWithUsers.find(s => s.sessionId === sessionId);
125
- if (!sessionToRemove) return;
126
-
127
- const displayName = typeof sessionToRemove.userProfile?.name === 'object'
128
- ? sessionToRemove.userProfile.name.full || sessionToRemove.userProfile.name.first || sessionToRemove.userProfile.username
129
- : sessionToRemove.userProfile?.name || sessionToRemove.userProfile?.username || 'this account';
130
-
131
- Alert.alert(
132
- 'Remove Account',
133
- `Are you sure you want to remove ${displayName} from this device? You'll need to sign in again to access this account.`,
134
- [
135
- {
136
- text: 'Cancel',
137
- style: 'cancel',
138
- },
139
- {
140
- text: 'Remove',
141
- style: 'destructive',
142
- onPress: async () => {
143
- setRemovingUserId(sessionId);
144
- try {
145
- await removeSession(sessionId);
146
- Alert.alert('Success', 'Account removed successfully!');
147
- } catch (error) {
148
- console.error('Remove session failed:', error);
149
- Alert.alert('Remove Failed', 'There was a problem removing the account. Please try again.');
150
- } finally {
151
- setRemovingUserId(null);
152
- }
153
- },
154
- },
155
- ],
156
- { cancelable: true }
157
- );
158
- };
159
-
160
- const handleLogoutAll = async () => {
161
- Alert.alert(
162
- 'Sign Out All',
163
- 'Are you sure you want to sign out of all accounts on this device?',
164
- [
165
- {
166
- text: 'Cancel',
167
- style: 'cancel',
168
- },
169
- {
170
- text: 'Sign Out All',
171
- style: 'destructive',
172
- onPress: async () => {
173
- try {
174
- await logoutAll();
175
- Alert.alert('Success', 'All accounts signed out successfully!');
176
- if (onClose) {
177
- onClose();
178
- }
179
- } catch (error) {
180
- console.error('Logout all failed:', error);
181
- Alert.alert('Logout Failed', 'There was a problem signing out. Please try again.');
182
- }
183
- },
184
- },
185
- ],
186
- { cancelable: true }
187
- );
188
- };
189
-
190
- const renderSessionItem = (sessionWithUser: SessionWithUser) => {
191
- const isActive = sessionWithUser.sessionId === activeSessionId;
192
- const isSwitching = switchingToUserId === sessionWithUser.sessionId;
193
- const isRemoving = removingUserId === sessionWithUser.sessionId;
194
- const { userProfile, isLoadingProfile } = sessionWithUser;
195
-
196
- const displayName = typeof userProfile?.name === 'object'
197
- ? userProfile.name.full || userProfile.name.first || userProfile.username
198
- : userProfile?.name || userProfile?.username || 'Unknown User';
199
- const username = userProfile?.username || 'unknown';
200
- const avatarUrl = userProfile?.avatar?.url;
201
-
202
- return (
203
- <View
204
- key={sessionWithUser.sessionId}
205
- style={[
206
- styles.sessionCard,
207
- {
208
- backgroundColor: isActive ? colors.activeCard : colors.card,
209
- borderColor: isActive ? colors.accent : colors.border,
210
- borderWidth: isActive ? 2 : 1,
211
- shadowColor: colors.shadow,
212
- },
213
- ]}
214
- >
215
- <View style={styles.sessionHeader}>
216
- <View style={styles.avatarContainer}>
217
- {isLoadingProfile ? (
218
- <View style={[styles.avatarPlaceholder, { backgroundColor: colors.surface }]}>
219
- <ActivityIndicator size="small" color={colors.accent} />
220
- </View>
221
- ) : avatarUrl ? (
222
- <Image
223
- source={{ uri: avatarUrl }}
224
- style={styles.avatar}
225
- />
226
- ) : (
227
- <View style={[styles.avatarPlaceholder, { backgroundColor: colors.surface }]}>
228
- <Text style={[styles.avatarText, { color: colors.accent }]}>
229
- {displayName.charAt(0).toUpperCase()}
230
- </Text>
231
- </View>
232
- )}
233
- {isActive && (
234
- <View style={[styles.activeBadge, { backgroundColor: colors.success }]}>
235
- <Text style={styles.activeBadgeText}>✓</Text>
236
- </View>
237
- )}
238
- </View>
239
-
240
- <View style={styles.userInfo}>
241
- <Text style={[styles.displayName, { color: colors.text }]} numberOfLines={1}>
242
- {displayName}
243
- </Text>
244
- <Text style={[styles.username, { color: colors.secondaryText }]} numberOfLines={1}>
245
- @{username}
246
- </Text>
247
- <Text style={[styles.lastActive, { color: colors.secondaryText }]} numberOfLines={1}>
248
- Last active: {new Date(sessionWithUser.lastActive).toLocaleDateString()}
249
- </Text>
250
- </View>
251
- </View>
252
-
253
- <View style={styles.sessionActions}>
254
- {!isActive && (
255
- <TouchableOpacity
256
- style={[styles.switchButton, {
257
- borderColor: colors.accent,
258
- backgroundColor: colors.background,
259
- }]}
260
- onPress={() => handleSwitchSession(sessionWithUser.sessionId)}
261
- disabled={isSwitching || isRemoving}
262
- >
263
- {isSwitching ? (
264
- <ActivityIndicator color={colors.accent} size="small" />
265
- ) : (
266
- <Text style={[styles.switchButtonText, { color: colors.accent }]}>
267
- Switch
268
- </Text>
269
- )}
270
- </TouchableOpacity>
271
- )}
272
-
273
- <TouchableOpacity
274
- style={[styles.removeButton, {
275
- borderColor: colors.destructive,
276
- backgroundColor: colors.background,
277
- }]}
278
- onPress={() => handleRemoveSession(sessionWithUser.sessionId)}
279
- disabled={isSwitching || isRemoving}
280
- >
281
- {isRemoving ? (
282
- <ActivityIndicator color={colors.destructive} size="small" />
283
- ) : (
284
- <Text style={[styles.removeButtonText, { color: colors.destructive }]}>
285
- Remove
286
- </Text>
287
- )}
288
- </TouchableOpacity>
289
- </View>
290
- </View>
291
- );
292
- };
293
-
294
- return (
295
- <View style={[styles.container, { backgroundColor: colors.background }]}>
296
- <View style={styles.header}>
297
- <TouchableOpacity style={styles.backButton} onPress={goBack}>
298
- <Text style={[styles.backButtonText, { color: colors.accent }]}>‹ Back</Text>
299
- </TouchableOpacity>
300
- <Text style={[styles.title, { color: colors.text }]}>Accounts</Text>
301
- <View style={styles.headerSpacer} />
302
- </View>
303
-
304
- <ScrollView
305
- style={styles.content}
306
- showsVerticalScrollIndicator={false}
307
- contentContainerStyle={styles.scrollContent}
308
- >
309
- {isLoading ? (
310
- <View style={styles.loadingContainer}>
311
- <ActivityIndicator size="large" color={colors.accent} />
312
- <Text style={[styles.loadingText, { color: colors.secondaryText }]}>
313
- Loading accounts...
314
- </Text>
315
- </View>
316
- ) : (
317
- <>
318
- <Text style={[styles.sectionTitle, { color: colors.text }]}>
319
- Saved Accounts ({sessionsWithUsers.length})
320
- </Text>
321
-
322
- {sessionsWithUsers.length === 0 ? (
323
- <View style={styles.emptyState}>
324
- <Text style={[styles.emptyText, { color: colors.secondaryText }]}>
325
- No saved accounts found
326
- </Text>
327
- </View>
328
- ) : (
329
- sessionsWithUsers.map(renderSessionItem)
330
- )}
331
-
332
- <View style={styles.actionsSection}>
333
- <TouchableOpacity
334
- style={[styles.actionButton, {
335
- borderColor: colors.border,
336
- backgroundColor: colors.card,
337
- }]}
338
- onPress={() => navigate?.('SignIn')}
339
- >
340
- <Text style={[styles.actionButtonText, { color: colors.text }]}>
341
- + Add Another Account
342
- </Text>
343
- </TouchableOpacity>
344
-
345
- {sessionsWithUsers.length > 0 && (
346
- <TouchableOpacity
347
- style={[styles.actionButton, styles.dangerButton, {
348
- borderColor: colors.destructive,
349
- backgroundColor: colors.background,
350
- }]}
351
- onPress={handleLogoutAll}
352
- >
353
- <Text style={[styles.dangerButtonText, { color: colors.destructive }]}>
354
- Sign Out All Accounts
355
- </Text>
356
- </TouchableOpacity>
357
- )}
358
- </View>
359
- </>
360
- )}
361
- </ScrollView>
362
- </View>
363
- );
364
- };
365
-
366
- const styles = StyleSheet.create({
367
- container: {
368
- flex: 1,
369
- },
370
- header: {
371
- flexDirection: 'row',
372
- alignItems: 'center',
373
- justifyContent: 'space-between',
374
- paddingHorizontal: 20,
375
- paddingTop: 20,
376
- paddingBottom: 10,
377
- },
378
- backButton: {
379
- padding: 8,
380
- },
381
- backButtonText: {
382
- fontSize: 18,
383
- fontFamily: fontFamilies.phuduMedium,
384
- },
385
- title: {
386
- fontSize: 24,
387
- fontFamily: fontFamilies.phuduBold,
388
- textAlign: 'center',
389
- },
390
- headerSpacer: {
391
- width: 40,
392
- },
393
- content: {
394
- flex: 1,
395
- paddingHorizontal: 20,
396
- },
397
- scrollContent: {
398
- paddingBottom: 40,
399
- },
400
- loadingContainer: {
401
- flex: 1,
402
- justifyContent: 'center',
403
- alignItems: 'center',
404
- paddingTop: 60,
405
- },
406
- loadingText: {
407
- marginTop: 16,
408
- fontSize: 16,
409
- fontFamily: fontFamilies.phudu,
410
- },
411
- sectionTitle: {
412
- fontSize: 20,
413
- fontFamily: fontFamilies.phuduSemiBold,
414
- marginBottom: 20,
415
- marginTop: 10,
416
- },
417
- emptyState: {
418
- alignItems: 'center',
419
- paddingVertical: 40,
420
- },
421
- emptyText: {
422
- fontSize: 16,
423
- fontFamily: fontFamilies.phudu,
424
- textAlign: 'center',
425
- },
426
- sessionCard: {
427
- borderRadius: 16,
428
- marginBottom: 16,
429
- padding: 20,
430
- shadowOffset: {
431
- width: 0,
432
- height: 2,
433
- },
434
- shadowOpacity: 0.1,
435
- shadowRadius: 8,
436
- elevation: 3,
437
- },
438
- sessionHeader: {
439
- flexDirection: 'row',
440
- alignItems: 'center',
441
- marginBottom: 16,
442
- },
443
- avatarContainer: {
444
- position: 'relative',
445
- marginRight: 16,
446
- },
447
- avatar: {
448
- width: 60,
449
- height: 60,
450
- borderRadius: 30,
451
- },
452
- avatarPlaceholder: {
453
- width: 60,
454
- height: 60,
455
- borderRadius: 30,
456
- justifyContent: 'center',
457
- alignItems: 'center',
458
- },
459
- avatarText: {
460
- fontSize: 24,
461
- fontFamily: fontFamilies.phuduBold,
462
- },
463
- activeBadge: {
464
- position: 'absolute',
465
- bottom: -2,
466
- right: -2,
467
- width: 20,
468
- height: 20,
469
- borderRadius: 10,
470
- justifyContent: 'center',
471
- alignItems: 'center',
472
- },
473
- activeBadgeText: {
474
- color: 'white',
475
- fontSize: 12,
476
- fontFamily: fontFamilies.phuduBold,
477
- },
478
- userInfo: {
479
- flex: 1,
480
- justifyContent: 'center',
481
- },
482
- displayName: {
483
- fontSize: 18,
484
- fontFamily: fontFamilies.phuduSemiBold,
485
- marginBottom: 4,
486
- },
487
- username: {
488
- fontSize: 14,
489
- fontFamily: fontFamilies.phudu,
490
- marginBottom: 4,
491
- },
492
- lastActive: {
493
- fontSize: 12,
494
- fontFamily: fontFamilies.phudu,
495
- },
496
- sessionActions: {
497
- flexDirection: 'row',
498
- justifyContent: 'space-between',
499
- gap: 12,
500
- },
501
- switchButton: {
502
- flex: 1,
503
- paddingVertical: 12,
504
- paddingHorizontal: 20,
505
- borderWidth: 1,
506
- borderRadius: 8,
507
- alignItems: 'center',
508
- justifyContent: 'center',
509
- },
510
- switchButtonText: {
511
- fontSize: 14,
512
- fontFamily: fontFamilies.phuduSemiBold,
513
- },
514
- removeButton: {
515
- flex: 1,
516
- paddingVertical: 12,
517
- paddingHorizontal: 20,
518
- borderWidth: 1,
519
- borderRadius: 8,
520
- alignItems: 'center',
521
- justifyContent: 'center',
522
- },
523
- removeButtonText: {
524
- fontSize: 14,
525
- fontFamily: fontFamilies.phuduSemiBold,
526
- },
527
- actionsSection: {
528
- marginTop: 40,
529
- gap: 16,
530
- },
531
- actionButton: {
532
- paddingVertical: 16,
533
- paddingHorizontal: 20,
534
- borderWidth: 1,
535
- borderRadius: 12,
536
- alignItems: 'center',
537
- justifyContent: 'center',
538
- },
539
- actionButtonText: {
540
- fontSize: 16,
541
- fontFamily: fontFamilies.phuduSemiBold,
542
- },
543
- dangerButton: {
544
- // Additional styles for danger buttons if needed
545
- },
546
- dangerButtonText: {
547
- fontSize: 16,
548
- fontFamily: fontFamilies.phuduSemiBold,
549
- },
550
- });
551
-
552
- export default ModernAccountSwitcherScreen;