@oxyhq/services 5.1.33 → 5.1.34

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 (56) hide show
  1. package/README.md +67 -2
  2. package/lib/commonjs/core/index.js +56 -1
  3. package/lib/commonjs/core/index.js.map +1 -1
  4. package/lib/commonjs/index.js +7 -0
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/ui/context/OxyContext.js +351 -41
  7. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  8. package/lib/commonjs/ui/index.js +8 -0
  9. package/lib/commonjs/ui/index.js.map +1 -1
  10. package/lib/commonjs/ui/navigation/OxyRouter.js +10 -0
  11. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  12. package/lib/commonjs/ui/screens/AccountCenterScreen.js +25 -2
  13. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  14. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +382 -0
  15. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -0
  16. package/lib/commonjs/ui/screens/SessionManagementScreen.js +448 -0
  17. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -0
  18. package/lib/module/core/index.js +56 -1
  19. package/lib/module/core/index.js.map +1 -1
  20. package/lib/module/index.js +2 -2
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/ui/context/OxyContext.js +351 -40
  23. package/lib/module/ui/context/OxyContext.js.map +1 -1
  24. package/lib/module/ui/index.js +1 -0
  25. package/lib/module/ui/index.js.map +1 -1
  26. package/lib/module/ui/navigation/OxyRouter.js +10 -0
  27. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  28. package/lib/module/ui/screens/AccountCenterScreen.js +25 -2
  29. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  30. package/lib/module/ui/screens/AccountSwitcherScreen.js +377 -0
  31. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -0
  32. package/lib/module/ui/screens/SessionManagementScreen.js +443 -0
  33. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -0
  34. package/lib/typescript/core/index.d.ts +31 -1
  35. package/lib/typescript/core/index.d.ts.map +1 -1
  36. package/lib/typescript/index.d.ts +2 -2
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/ui/context/OxyContext.d.ts +12 -1
  39. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  40. package/lib/typescript/ui/index.d.ts +1 -0
  41. package/lib/typescript/ui/index.d.ts.map +1 -1
  42. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  43. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  44. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts +5 -0
  45. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  46. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts +5 -0
  47. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -0
  48. package/package.json +1 -1
  49. package/src/core/index.ts +56 -1
  50. package/src/index.ts +2 -0
  51. package/src/ui/context/OxyContext.tsx +397 -46
  52. package/src/ui/index.ts +1 -0
  53. package/src/ui/navigation/OxyRouter.tsx +10 -0
  54. package/src/ui/screens/AccountCenterScreen.tsx +20 -1
  55. package/src/ui/screens/AccountSwitcherScreen.tsx +380 -0
  56. package/src/ui/screens/SessionManagementScreen.tsx +479 -0
@@ -6,6 +6,8 @@ import { OxyServices } from '../../core';
6
6
  import SignInScreen from '../screens/SignInScreen';
7
7
  import SignUpScreen from '../screens/SignUpScreen';
8
8
  import AccountCenterScreen from '../screens/AccountCenterScreen';
9
+ import AccountSwitcherScreen from '../screens/AccountSwitcherScreen';
10
+ import SessionManagementScreen from '../screens/SessionManagementScreen';
9
11
  import AccountOverviewScreen from '../screens/AccountOverviewScreen';
10
12
  import AccountSettingsScreen from '../screens/AccountSettingsScreen';
11
13
  import KarmaCenterScreen from '../screens/karma/KarmaCenterScreen';
@@ -33,6 +35,14 @@ const routes: Record<string, RouteConfig> = {
33
35
  component: AccountCenterScreen,
34
36
  snapPoints: ['60%', '100%'],
35
37
  },
38
+ AccountSwitcher: {
39
+ component: AccountSwitcherScreen,
40
+ snapPoints: ['70%', '100%'],
41
+ },
42
+ SessionManagement: {
43
+ component: SessionManagementScreen,
44
+ snapPoints: ['70%', '100%'],
45
+ },
36
46
  AccountOverview: {
37
47
  component: AccountOverviewScreen,
38
48
  snapPoints: ['60%', '85%'],
@@ -19,7 +19,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
19
19
  theme,
20
20
  navigate,
21
21
  }) => {
22
- const { user, logout, isLoading } = useOxy();
22
+ const { user, logout, isLoading, users } = useOxy();
23
23
 
24
24
  const isDarkTheme = theme === 'dark';
25
25
  const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
@@ -93,6 +93,18 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
93
93
  </View>
94
94
 
95
95
  <View style={styles.actionsContainer}>
96
+ {/* Show Account Switcher if multiple users exist */}
97
+ {users && users.length > 1 && (
98
+ <TouchableOpacity
99
+ style={[styles.actionButton, { borderColor }]}
100
+ onPress={() => navigate('AccountSwitcher')}
101
+ >
102
+ <Text style={[styles.actionButtonText, { color: textColor }]}>
103
+ Switch Account ({users.length} accounts)
104
+ </Text>
105
+ </TouchableOpacity>
106
+ )}
107
+
96
108
  <TouchableOpacity
97
109
  style={[styles.actionButton, { borderColor }]}
98
110
  onPress={() => navigate('AccountSettings', { activeTab: 'profile' })}
@@ -107,6 +119,13 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
107
119
  <Text style={[styles.actionButtonText, { color: textColor }]}>Account Settings</Text>
108
120
  </TouchableOpacity>
109
121
 
122
+ <TouchableOpacity
123
+ style={[styles.actionButton, { borderColor }]}
124
+ onPress={() => navigate('SessionManagement')}
125
+ >
126
+ <Text style={[styles.actionButtonText, { color: textColor }]}>Manage Sessions</Text>
127
+ </TouchableOpacity>
128
+
110
129
  {Platform.OS !== 'web' && (
111
130
  <TouchableOpacity
112
131
  style={[styles.actionButton, { borderColor }]}
@@ -0,0 +1,380 @@
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
+ } from 'react-native';
12
+ import { BaseScreenProps } from '../navigation/types';
13
+ import { useOxy, AuthenticatedUser } from '../context/OxyContext';
14
+ import { fontFamilies } from '../styles/fonts';
15
+
16
+ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
17
+ onClose,
18
+ theme,
19
+ navigate,
20
+ goBack,
21
+ }) => {
22
+ const {
23
+ user,
24
+ users,
25
+ switchUser,
26
+ removeUser,
27
+ logoutAll,
28
+ isLoading
29
+ } = useOxy();
30
+
31
+ const [switchingToUserId, setSwitchingToUserId] = useState<string | null>(null);
32
+ const [removingUserId, setRemovingUserId] = useState<string | null>(null);
33
+
34
+ const isDarkTheme = theme === 'dark';
35
+ const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
36
+ const backgroundColor = isDarkTheme ? '#121212' : '#FFFFFF';
37
+ const secondaryBackgroundColor = isDarkTheme ? '#222222' : '#F5F5F5';
38
+ const borderColor = isDarkTheme ? '#444444' : '#E0E0E0';
39
+ const primaryColor = '#0066CC';
40
+ const dangerColor = '#D32F2F';
41
+ const successColor = '#2E7D32';
42
+
43
+ const handleSwitchUser = async (userId: string) => {
44
+ if (userId === user?._id) return; // Already active user
45
+
46
+ setSwitchingToUserId(userId);
47
+ try {
48
+ await switchUser(userId);
49
+ Alert.alert('Success', 'Account switched successfully!');
50
+ if (onClose) {
51
+ onClose();
52
+ }
53
+ } catch (error) {
54
+ console.error('Switch user failed:', error);
55
+ Alert.alert('Switch Failed', 'There was a problem switching accounts. Please try again.');
56
+ } finally {
57
+ setSwitchingToUserId(null);
58
+ }
59
+ };
60
+
61
+ const handleRemoveUser = async (userId: string) => {
62
+ const userToRemove = users.find(u => u._id === userId);
63
+ if (!userToRemove) return;
64
+
65
+ Alert.alert(
66
+ 'Remove Account',
67
+ `Are you sure you want to remove ${userToRemove.username} from this device? You'll need to sign in again to access this account.`,
68
+ [
69
+ {
70
+ text: 'Cancel',
71
+ style: 'cancel',
72
+ },
73
+ {
74
+ text: 'Remove',
75
+ style: 'destructive',
76
+ onPress: async () => {
77
+ setRemovingUserId(userId);
78
+ try {
79
+ await removeUser(userId);
80
+ Alert.alert('Success', 'Account removed successfully!');
81
+ } catch (error) {
82
+ console.error('Remove user failed:', error);
83
+ Alert.alert('Remove Failed', 'There was a problem removing the account. Please try again.');
84
+ } finally {
85
+ setRemovingUserId(null);
86
+ }
87
+ },
88
+ },
89
+ ],
90
+ { cancelable: true }
91
+ );
92
+ };
93
+
94
+ const handleLogoutAll = async () => {
95
+ Alert.alert(
96
+ 'Sign Out All',
97
+ 'Are you sure you want to sign out of all accounts on this device?',
98
+ [
99
+ {
100
+ text: 'Cancel',
101
+ style: 'cancel',
102
+ },
103
+ {
104
+ text: 'Sign Out All',
105
+ style: 'destructive',
106
+ onPress: async () => {
107
+ try {
108
+ await logoutAll();
109
+ Alert.alert('Success', 'All accounts signed out successfully!');
110
+ if (onClose) {
111
+ onClose();
112
+ }
113
+ } catch (error) {
114
+ console.error('Logout all failed:', error);
115
+ Alert.alert('Logout Failed', 'There was a problem signing out. Please try again.');
116
+ }
117
+ },
118
+ },
119
+ ],
120
+ { cancelable: true }
121
+ );
122
+ };
123
+
124
+ const renderUserItem = (userItem: AuthenticatedUser) => {
125
+ const isActive = userItem._id === user?._id;
126
+ const isSwitching = switchingToUserId === userItem._id;
127
+ const isRemoving = removingUserId === userItem._id;
128
+
129
+ return (
130
+ <View
131
+ key={userItem._id}
132
+ style={[
133
+ styles.userItem,
134
+ {
135
+ backgroundColor: isActive ? primaryColor + '20' : secondaryBackgroundColor,
136
+ borderColor: isActive ? primaryColor : borderColor,
137
+ },
138
+ ]}
139
+ >
140
+ <View style={styles.userInfo}>
141
+ <Text style={[styles.username, { color: textColor }]}>{userItem.username}</Text>
142
+ {userItem.email && (
143
+ <Text style={[styles.email, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
144
+ {userItem.email}
145
+ </Text>
146
+ )}
147
+ {isActive && (
148
+ <View style={[styles.activeBadge, { backgroundColor: successColor }]}>
149
+ <Text style={styles.activeBadgeText}>Active</Text>
150
+ </View>
151
+ )}
152
+ </View>
153
+
154
+ <View style={styles.userActions}>
155
+ {!isActive && (
156
+ <TouchableOpacity
157
+ style={[styles.switchButton, { borderColor: primaryColor }]}
158
+ onPress={() => handleSwitchUser(userItem._id)}
159
+ disabled={isSwitching || isRemoving}
160
+ >
161
+ {isSwitching ? (
162
+ <ActivityIndicator color={primaryColor} size="small" />
163
+ ) : (
164
+ <Text style={[styles.switchButtonText, { color: primaryColor }]}>Switch</Text>
165
+ )}
166
+ </TouchableOpacity>
167
+ )}
168
+
169
+ <TouchableOpacity
170
+ style={[styles.removeButton, { borderColor: dangerColor }]}
171
+ onPress={() => handleRemoveUser(userItem._id)}
172
+ disabled={isSwitching || isRemoving || users.length === 1}
173
+ >
174
+ {isRemoving ? (
175
+ <ActivityIndicator color={dangerColor} size="small" />
176
+ ) : (
177
+ <Text style={[styles.removeButtonText, { color: dangerColor }]}>Remove</Text>
178
+ )}
179
+ </TouchableOpacity>
180
+ </View>
181
+ </View>
182
+ );
183
+ };
184
+
185
+ return (
186
+ <View style={[styles.container, { backgroundColor }]}>
187
+ <View style={styles.header}>
188
+ <TouchableOpacity style={styles.backButton} onPress={goBack}>
189
+ <Text style={[styles.backButtonText, { color: primaryColor }]}>‹ Back</Text>
190
+ </TouchableOpacity>
191
+ <Text style={[styles.title, { color: textColor }]}>Account Switcher</Text>
192
+ <View style={styles.placeholder} />
193
+ </View>
194
+
195
+ <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContainer}>
196
+ <View style={styles.description}>
197
+ <Text style={[styles.descriptionText, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
198
+ Manage multiple accounts on this device. Switch between accounts or remove them from this device.
199
+ </Text>
200
+ </View>
201
+
202
+ <View style={styles.usersContainer}>
203
+ <Text style={[styles.sectionTitle, { color: textColor }]}>
204
+ Accounts ({users.length})
205
+ </Text>
206
+
207
+ {users.map(renderUserItem)}
208
+ </View>
209
+
210
+ <View style={styles.actionsContainer}>
211
+ <TouchableOpacity
212
+ style={[styles.actionButton, { borderColor }]}
213
+ onPress={() => navigate('SignIn')}
214
+ >
215
+ <Text style={[styles.actionButtonText, { color: textColor }]}>
216
+ Add Another Account
217
+ </Text>
218
+ </TouchableOpacity>
219
+
220
+ {users.length > 1 && (
221
+ <TouchableOpacity
222
+ style={[styles.dangerButton, { backgroundColor: isDarkTheme ? '#300000' : '#FFEBEE' }]}
223
+ onPress={handleLogoutAll}
224
+ disabled={isLoading}
225
+ >
226
+ {isLoading ? (
227
+ <ActivityIndicator color={dangerColor} size="small" />
228
+ ) : (
229
+ <Text style={[styles.dangerButtonText, { color: dangerColor }]}>
230
+ Sign Out All Accounts
231
+ </Text>
232
+ )}
233
+ </TouchableOpacity>
234
+ )}
235
+ </View>
236
+ </ScrollView>
237
+ </View>
238
+ );
239
+ };
240
+
241
+ const styles = StyleSheet.create({
242
+ container: {
243
+ flex: 1,
244
+ },
245
+ header: {
246
+ flexDirection: 'row',
247
+ alignItems: 'center',
248
+ justifyContent: 'space-between',
249
+ paddingHorizontal: 20,
250
+ paddingTop: Platform.OS === 'ios' ? 50 : 20,
251
+ paddingBottom: 20,
252
+ borderBottomWidth: 1,
253
+ borderBottomColor: 'rgba(0, 0, 0, 0.1)',
254
+ },
255
+ backButton: {
256
+ paddingVertical: 8,
257
+ paddingHorizontal: 4,
258
+ },
259
+ backButtonText: {
260
+ fontSize: 18,
261
+ fontFamily: fontFamilies.phudu,
262
+ },
263
+ title: {
264
+ fontSize: 20,
265
+ fontFamily: fontFamilies.phuduSemiBold,
266
+ textAlign: 'center',
267
+ },
268
+ placeholder: {
269
+ width: 60, // Same as back button to center title
270
+ },
271
+ scrollView: {
272
+ flex: 1,
273
+ },
274
+ scrollContainer: {
275
+ padding: 20,
276
+ },
277
+ description: {
278
+ marginBottom: 24,
279
+ },
280
+ descriptionText: {
281
+ fontSize: 14,
282
+ fontFamily: fontFamilies.phudu,
283
+ lineHeight: 20,
284
+ },
285
+ usersContainer: {
286
+ marginBottom: 32,
287
+ },
288
+ sectionTitle: {
289
+ fontSize: 16,
290
+ fontFamily: fontFamilies.phuduSemiBold,
291
+ marginBottom: 16,
292
+ },
293
+ userItem: {
294
+ flexDirection: 'row',
295
+ alignItems: 'center',
296
+ justifyContent: 'space-between',
297
+ padding: 16,
298
+ borderRadius: 12,
299
+ borderWidth: 1,
300
+ marginBottom: 12,
301
+ },
302
+ userInfo: {
303
+ flex: 1,
304
+ },
305
+ username: {
306
+ fontSize: 16,
307
+ fontFamily: fontFamilies.phuduSemiBold,
308
+ marginBottom: 4,
309
+ },
310
+ email: {
311
+ fontSize: 14,
312
+ fontFamily: fontFamilies.phudu,
313
+ marginBottom: 8,
314
+ },
315
+ activeBadge: {
316
+ paddingHorizontal: 8,
317
+ paddingVertical: 4,
318
+ borderRadius: 6,
319
+ alignSelf: 'flex-start',
320
+ },
321
+ activeBadgeText: {
322
+ color: '#FFFFFF',
323
+ fontSize: 12,
324
+ fontFamily: fontFamilies.phuduMedium,
325
+ },
326
+ userActions: {
327
+ flexDirection: 'row',
328
+ gap: 8,
329
+ },
330
+ switchButton: {
331
+ paddingHorizontal: 16,
332
+ paddingVertical: 8,
333
+ borderRadius: 8,
334
+ borderWidth: 1,
335
+ minWidth: 70,
336
+ alignItems: 'center',
337
+ },
338
+ switchButtonText: {
339
+ fontSize: 14,
340
+ fontFamily: fontFamilies.phuduMedium,
341
+ },
342
+ removeButton: {
343
+ paddingHorizontal: 16,
344
+ paddingVertical: 8,
345
+ borderRadius: 8,
346
+ borderWidth: 1,
347
+ minWidth: 70,
348
+ alignItems: 'center',
349
+ },
350
+ removeButtonText: {
351
+ fontSize: 14,
352
+ fontFamily: fontFamilies.phuduMedium,
353
+ },
354
+ actionsContainer: {
355
+ gap: 16,
356
+ },
357
+ actionButton: {
358
+ paddingVertical: 16,
359
+ paddingHorizontal: 20,
360
+ borderRadius: 12,
361
+ borderWidth: 1,
362
+ alignItems: 'center',
363
+ },
364
+ actionButtonText: {
365
+ fontSize: 16,
366
+ fontFamily: fontFamilies.phuduMedium,
367
+ },
368
+ dangerButton: {
369
+ paddingVertical: 16,
370
+ paddingHorizontal: 20,
371
+ borderRadius: 12,
372
+ alignItems: 'center',
373
+ },
374
+ dangerButtonText: {
375
+ fontSize: 16,
376
+ fontFamily: fontFamilies.phuduMedium,
377
+ },
378
+ });
379
+
380
+ export default AccountSwitcherScreen;