@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionManagementScreen.d.ts","sourceRoot":"","sources":["../../../../src/ui/screens/SessionManagementScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAYnD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAiBtD,QAAA,MAAM,uBAAuB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAgUtD,CAAC;AAiIF,eAAe,uBAAuB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "5.1.33",
3
+ "version": "5.1.34",
4
4
  "description": "Reusable OxyHQ module to handle authentication, user management, karma system and more 🚀",
5
5
  "main": "lib/commonjs/node/index.js",
6
6
  "module": "lib/module/node/index.js",
package/src/core/index.ts CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  } from '../models/interfaces';
42
42
 
43
43
  /**
44
- * Default cloud URL for Oxy services
44
+ * Default cloud URL for Oxy services, cloud is where the user files are. (e.g. images, videos, etc.). Not the API.
45
45
  */
46
46
  export const OXY_CLOUD_URL = 'https://cloud.oxy.so';
47
47
 
@@ -277,6 +277,61 @@ export class OxyServices {
277
277
  }
278
278
  }
279
279
 
280
+ /* Session Management Methods */
281
+
282
+ /**
283
+ * Get active sessions for the authenticated user
284
+ * @returns Array of active session objects
285
+ */
286
+ async getUserSessions(): Promise<any[]> {
287
+ try {
288
+ const res = await this.client.get('/sessions');
289
+ return res.data;
290
+ } catch (error) {
291
+ throw this.handleError(error);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Logout from a specific session
297
+ * @param sessionId - The session ID to logout from
298
+ * @returns Success status
299
+ */
300
+ async logoutSession(sessionId: string): Promise<{ success: boolean; message: string }> {
301
+ try {
302
+ const res = await this.client.delete(`/sessions/${sessionId}`);
303
+ return res.data;
304
+ } catch (error) {
305
+ throw this.handleError(error);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Logout from all other sessions (keep current session active)
311
+ * @returns Success status
312
+ */
313
+ async logoutOtherSessions(): Promise<{ success: boolean; message: string }> {
314
+ try {
315
+ const res = await this.client.post('/sessions/logout-others');
316
+ return res.data;
317
+ } catch (error) {
318
+ throw this.handleError(error);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Logout from all sessions
324
+ * @returns Success status
325
+ */
326
+ async logoutAllSessions(): Promise<{ success: boolean; message: string }> {
327
+ try {
328
+ const res = await this.client.post('/sessions/logout-all');
329
+ return res.data;
330
+ } catch (error) {
331
+ throw this.handleError(error);
332
+ }
333
+ }
334
+
280
335
  /* Profile Methods */
281
336
 
282
337
  /**
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  SignInScreen,
18
18
  SignUpScreen,
19
19
  AccountCenterScreen,
20
+ SessionManagementScreen,
20
21
 
21
22
  // Components
22
23
  OxySignInButton,
@@ -50,6 +51,7 @@ export {
50
51
  SignInScreen,
51
52
  SignUpScreen,
52
53
  AccountCenterScreen,
54
+ SessionManagementScreen,
53
55
 
54
56
  // Components
55
57
  OxySignInButton,
@@ -2,19 +2,34 @@ import React, { createContext, useContext, useState, useEffect, useCallback, Rea
2
2
  import { OxyServices } from '../../core';
3
3
  import { User, LoginResponse } from '../../models/interfaces';
4
4
 
5
+ // Define authenticated user with tokens
6
+ export interface AuthenticatedUser extends User {
7
+ accessToken: string;
8
+ refreshToken?: string;
9
+ sessionId?: string;
10
+ }
11
+
5
12
  // Define the context shape
6
13
  export interface OxyContextState {
7
- // Authentication state
8
- user: User | null;
14
+ // Multi-user authentication state
15
+ user: User | null; // Current active user
16
+ users: AuthenticatedUser[]; // All authenticated users
9
17
  isAuthenticated: boolean;
10
18
  isLoading: boolean;
11
19
  error: string | null;
12
20
 
13
21
  // Auth methods
14
22
  login: (username: string, password: string) => Promise<User>;
15
- logout: () => Promise<void>;
23
+ logout: (userId?: string) => Promise<void>; // Optional userId for multi-user logout
24
+ logoutAll: () => Promise<void>; // Logout all users
16
25
  signUp: (username: string, email: string, password: string) => Promise<User>;
17
26
 
27
+ // Multi-user methods
28
+ switchUser: (userId: string) => Promise<void>;
29
+ removeUser: (userId: string) => Promise<void>;
30
+ getUserSessions: (userId?: string) => Promise<any[]>; // Get sessions for user
31
+ logoutSession: (sessionId: string, userId?: string) => Promise<void>; // Logout specific session
32
+
18
33
  // Access to services
19
34
  oxyServices: OxyServices;
20
35
  bottomSheetRef?: React.RefObject<any>;
@@ -94,6 +109,9 @@ const getStorage = async (): Promise<StorageInterface> => {
94
109
 
95
110
  // Storage keys
96
111
  const getStorageKeys = (prefix = 'oxy') => ({
112
+ users: `${prefix}_users`, // Array of authenticated users with tokens
113
+ activeUserId: `${prefix}_active_user_id`, // ID of currently active user
114
+ // Legacy keys for migration
97
115
  accessToken: `${prefix}_access_token`,
98
116
  refreshToken: `${prefix}_refresh_token`,
99
117
  user: `${prefix}_user`,
@@ -108,6 +126,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
108
126
  }) => {
109
127
  // Authentication state
110
128
  const [user, setUser] = useState<User | null>(null);
129
+ const [users, setUsers] = useState<AuthenticatedUser[]>([]);
111
130
  const [isLoading, setIsLoading] = useState(true);
112
131
  const [error, setError] = useState<string | null>(null);
113
132
  const [storage, setStorage] = useState<StorageInterface | null>(null);
@@ -137,41 +156,41 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
137
156
 
138
157
  setIsLoading(true);
139
158
  try {
140
- // Try to load tokens from storage
141
- const accessToken = await storage.getItem(keys.accessToken);
142
- const refreshToken = await storage.getItem(keys.refreshToken);
143
- const storedUser = await storage.getItem(keys.user);
144
-
145
- if (accessToken) {
146
- // Set tokens in OxyServices
147
- if (refreshToken) {
148
- oxyServices.setTokens(accessToken, refreshToken);
149
- } else {
150
- // Use setTokens with the same token if setToken is not available
151
- oxyServices.setTokens(accessToken, accessToken);
152
- }
153
-
154
- // Validate the tokens
155
- const isValid = await oxyServices.validate();
156
-
157
- if (isValid && storedUser) {
158
- // Set user state
159
- const parsedUser = JSON.parse(storedUser);
160
- setUser(parsedUser);
161
-
162
- // Notify about auth state change
163
- if (onAuthStateChange) {
164
- onAuthStateChange(parsedUser);
159
+ // Check for multi-user data first
160
+ const usersData = await storage.getItem(keys.users);
161
+ const activeUserId = await storage.getItem(keys.activeUserId);
162
+
163
+ if (usersData) {
164
+ // Multi-user setup exists
165
+ const parsedUsers: AuthenticatedUser[] = JSON.parse(usersData);
166
+ setUsers(parsedUsers);
167
+
168
+ if (activeUserId && parsedUsers.length > 0) {
169
+ const activeUser = parsedUsers.find(u => u._id === activeUserId);
170
+ if (activeUser) {
171
+ setUser(activeUser);
172
+ oxyServices.setTokens(activeUser.accessToken, activeUser.refreshToken || activeUser.accessToken);
173
+
174
+ // Validate the tokens
175
+ const isValid = await oxyServices.validate();
176
+ if (!isValid) {
177
+ // Remove invalid user
178
+ await removeUser(activeUser._id);
179
+ } else {
180
+ // Notify about auth state change
181
+ if (onAuthStateChange) {
182
+ onAuthStateChange(activeUser);
183
+ }
184
+ }
165
185
  }
166
- } else {
167
- // Tokens are invalid, clear everything
168
- await clearStorage();
169
- oxyServices.clearTokens();
170
186
  }
187
+ } else {
188
+ // Check for legacy single-user data and migrate
189
+ await migrateLegacyAuth();
171
190
  }
172
191
  } catch (err) {
173
192
  console.error('Auth initialization error:', err);
174
- await clearStorage();
193
+ await clearAllStorage();
175
194
  oxyServices.clearTokens();
176
195
  } finally {
177
196
  setIsLoading(false);
@@ -181,9 +200,57 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
181
200
  if (storage) {
182
201
  initAuth();
183
202
  }
184
- }, [storage, oxyServices, keys.accessToken, keys.refreshToken, keys.user, onAuthStateChange]);
203
+ }, [storage, oxyServices, keys.users, keys.activeUserId, onAuthStateChange]);
204
+
205
+ // Migrate legacy single-user authentication to multi-user
206
+ const migrateLegacyAuth = async (): Promise<void> => {
207
+ if (!storage) return;
208
+
209
+ try {
210
+ const accessToken = await storage.getItem(keys.accessToken);
211
+ const refreshToken = await storage.getItem(keys.refreshToken);
212
+ const storedUser = await storage.getItem(keys.user);
213
+
214
+ if (accessToken && storedUser) {
215
+ // Set tokens in OxyServices
216
+ oxyServices.setTokens(accessToken, refreshToken || accessToken);
217
+
218
+ // Validate the tokens
219
+ const isValid = await oxyServices.validate();
220
+
221
+ if (isValid) {
222
+ const parsedUser = JSON.parse(storedUser);
223
+ const authenticatedUser: AuthenticatedUser = {
224
+ ...parsedUser,
225
+ accessToken,
226
+ refreshToken: refreshToken || undefined,
227
+ };
228
+
229
+ // Store in new multi-user format
230
+ await storage.setItem(keys.users, JSON.stringify([authenticatedUser]));
231
+ await storage.setItem(keys.activeUserId, authenticatedUser._id);
232
+
233
+ // Set state
234
+ setUsers([authenticatedUser]);
235
+ setUser(authenticatedUser);
236
+
237
+ // Notify about auth state change
238
+ if (onAuthStateChange) {
239
+ onAuthStateChange(authenticatedUser);
240
+ }
241
+ }
185
242
 
186
- // Helper to clear storage
243
+ // Clear legacy storage
244
+ await storage.removeItem(keys.accessToken);
245
+ await storage.removeItem(keys.refreshToken);
246
+ await storage.removeItem(keys.user);
247
+ }
248
+ } catch (err) {
249
+ console.error('Migration error:', err);
250
+ }
251
+ };
252
+
253
+ // Helper to clear legacy storage
187
254
  const clearStorage = async (): Promise<void> => {
188
255
  if (!storage) return;
189
256
 
@@ -196,6 +263,32 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
196
263
  }
197
264
  };
198
265
 
266
+ // Helper to clear all storage (multi-user)
267
+ const clearAllStorage = async (): Promise<void> => {
268
+ if (!storage) return;
269
+
270
+ try {
271
+ await storage.removeItem(keys.users);
272
+ await storage.removeItem(keys.activeUserId);
273
+ // Also clear legacy keys
274
+ await clearStorage();
275
+ } catch (err) {
276
+ console.error('Clear all storage error:', err);
277
+ }
278
+ };
279
+
280
+ // Save users to storage
281
+ const saveUsersToStorage = async (usersList: AuthenticatedUser[]): Promise<void> => {
282
+ if (!storage) return;
283
+ await storage.setItem(keys.users, JSON.stringify(usersList));
284
+ };
285
+
286
+ // Save active user ID to storage
287
+ const saveActiveUserId = async (userId: string): Promise<void> => {
288
+ if (!storage) return;
289
+ await storage.setItem(keys.activeUserId, userId);
290
+ };
291
+
199
292
  // Utility function to handle different token response formats
200
293
  const storeTokens = async (response: any) => {
201
294
  // Store token and user data
@@ -211,7 +304,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
211
304
  await storage?.setItem(keys.user, JSON.stringify(response.user));
212
305
  };
213
306
 
214
- // Login method
307
+ // Login method (updated for multi-user)
215
308
  const login = async (username: string, password: string): Promise<User> => {
216
309
  if (!storage) throw new Error('Storage not initialized');
217
310
 
@@ -220,17 +313,46 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
220
313
 
221
314
  try {
222
315
  const response = await oxyServices.login(username, password);
223
- setUser(response.user);
316
+ const accessToken = response.accessToken || (response as any).token;
317
+
318
+ if (!accessToken) {
319
+ throw new Error('No access token received from login');
320
+ }
224
321
 
225
- // Store tokens
226
- await storeTokens(response);
322
+ const newUser: AuthenticatedUser = {
323
+ ...response.user,
324
+ accessToken,
325
+ refreshToken: response.refreshToken,
326
+ // sessionId will be set by backend, but we don't get it in response yet
327
+ };
328
+
329
+ // Check if user already exists
330
+ const existingUserIndex = users.findIndex(u => u._id === newUser._id);
331
+ let updatedUsers: AuthenticatedUser[];
332
+
333
+ if (existingUserIndex >= 0) {
334
+ // Update existing user
335
+ updatedUsers = [...users];
336
+ updatedUsers[existingUserIndex] = newUser;
337
+ } else {
338
+ // Add new user
339
+ updatedUsers = [...users, newUser];
340
+ }
341
+
342
+ // Update state
343
+ setUsers(updatedUsers);
344
+ setUser(newUser);
345
+
346
+ // Save to storage
347
+ await saveUsersToStorage(updatedUsers);
348
+ await saveActiveUserId(newUser._id);
227
349
 
228
350
  // Notify about auth state change
229
351
  if (onAuthStateChange) {
230
- onAuthStateChange(response.user);
352
+ onAuthStateChange(newUser);
231
353
  }
232
354
 
233
- return response.user;
355
+ return newUser;
234
356
  } catch (err: any) {
235
357
  setError(err.message || 'Login failed');
236
358
  throw err;
@@ -239,30 +361,242 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
239
361
  }
240
362
  };
241
363
 
242
- // Logout method
243
- const logout = async (): Promise<void> => {
364
+ // Logout method (supports multi-user)
365
+ const logout = async (userId?: string): Promise<void> => {
244
366
  if (!storage) throw new Error('Storage not initialized');
245
367
 
246
368
  setIsLoading(true);
247
369
  setError(null);
248
370
 
249
371
  try {
250
- await oxyServices.logout();
251
- await clearStorage();
372
+ const targetUserId = userId || user?._id;
373
+ if (!targetUserId) return;
374
+
375
+ const targetUser = users.find(u => u._id === targetUserId);
376
+ if (targetUser) {
377
+ // Set the target user's tokens to logout
378
+ oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
379
+
380
+ try {
381
+ await oxyServices.logout();
382
+ } catch (logoutError) {
383
+ console.warn('Logout API call failed:', logoutError);
384
+ }
385
+
386
+ // Remove user from list
387
+ const updatedUsers = users.filter(u => u._id !== targetUserId);
388
+ setUsers(updatedUsers);
389
+
390
+ // If logging out current user, switch to another user or clear
391
+ if (targetUserId === user?._id) {
392
+ if (updatedUsers.length > 0) {
393
+ // Switch to first available user
394
+ const nextUser = updatedUsers[0];
395
+ setUser(nextUser);
396
+ oxyServices.setTokens(nextUser.accessToken, nextUser.refreshToken || nextUser.accessToken);
397
+ await saveActiveUserId(nextUser._id);
398
+
399
+ if (onAuthStateChange) {
400
+ onAuthStateChange(nextUser);
401
+ }
402
+ } else {
403
+ // No users left
404
+ setUser(null);
405
+ oxyServices.clearTokens();
406
+ await storage.removeItem(keys.activeUserId);
407
+
408
+ if (onAuthStateChange) {
409
+ onAuthStateChange(null);
410
+ }
411
+ }
412
+ }
413
+
414
+ // Save updated users list
415
+ await saveUsersToStorage(updatedUsers);
416
+ }
417
+ } catch (err: any) {
418
+ setError(err.message || 'Logout failed');
419
+ throw err;
420
+ } finally {
421
+ setIsLoading(false);
422
+ }
423
+ };
424
+
425
+ // Logout all users
426
+ const logoutAll = async (): Promise<void> => {
427
+ if (!storage) throw new Error('Storage not initialized');
428
+
429
+ setIsLoading(true);
430
+ setError(null);
431
+
432
+ try {
433
+ // Logout each user
434
+ for (const userItem of users) {
435
+ try {
436
+ oxyServices.setTokens(userItem.accessToken, userItem.refreshToken || userItem.accessToken);
437
+ await oxyServices.logout();
438
+ } catch (logoutError) {
439
+ console.warn(`Logout failed for user ${userItem._id}:`, logoutError);
440
+ }
441
+ }
442
+
443
+ // Clear all state and storage
444
+ setUsers([]);
252
445
  setUser(null);
446
+ oxyServices.clearTokens();
447
+ await clearAllStorage();
253
448
 
254
449
  // Notify about auth state change
255
450
  if (onAuthStateChange) {
256
451
  onAuthStateChange(null);
257
452
  }
258
453
  } catch (err: any) {
259
- setError(err.message || 'Logout failed');
454
+ setError(err.message || 'Logout all failed');
260
455
  throw err;
261
456
  } finally {
262
457
  setIsLoading(false);
263
458
  }
264
459
  };
265
460
 
461
+ // Switch user
462
+ const switchUser = async (userId: string): Promise<void> => {
463
+ if (!storage) throw new Error('Storage not initialized');
464
+
465
+ setError(null);
466
+
467
+ try {
468
+ const targetUser = users.find(u => u._id === userId);
469
+ if (!targetUser) {
470
+ throw new Error('User not found');
471
+ }
472
+
473
+ // Validate tokens before switching
474
+ oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
475
+ const isValid = await oxyServices.validate();
476
+
477
+ if (!isValid) {
478
+ // Remove invalid user
479
+ await removeUser(userId);
480
+ throw new Error('User session is invalid');
481
+ }
482
+
483
+ // Switch to the user
484
+ setUser(targetUser);
485
+ await saveActiveUserId(userId);
486
+
487
+ // Notify about auth state change
488
+ if (onAuthStateChange) {
489
+ onAuthStateChange(targetUser);
490
+ }
491
+ } catch (err: any) {
492
+ setError(err.message || 'Switch user failed');
493
+ throw err;
494
+ }
495
+ };
496
+
497
+ // Remove user
498
+ const removeUser = async (userId: string): Promise<void> => {
499
+ if (!storage) throw new Error('Storage not initialized');
500
+
501
+ try {
502
+ const updatedUsers = users.filter(u => u._id !== userId);
503
+ setUsers(updatedUsers);
504
+
505
+ // If removing current user, switch to another or clear
506
+ if (userId === user?._id) {
507
+ if (updatedUsers.length > 0) {
508
+ await switchUser(updatedUsers[0]._id);
509
+ } else {
510
+ setUser(null);
511
+ oxyServices.clearTokens();
512
+ await storage.removeItem(keys.activeUserId);
513
+
514
+ if (onAuthStateChange) {
515
+ onAuthStateChange(null);
516
+ }
517
+ }
518
+ }
519
+
520
+ // Save updated users list
521
+ await saveUsersToStorage(updatedUsers);
522
+ } catch (err: any) {
523
+ setError(err.message || 'Remove user failed');
524
+ throw err;
525
+ }
526
+ };
527
+
528
+ // Get user sessions
529
+ const getUserSessions = async (userId?: string): Promise<any[]> => {
530
+ try {
531
+ const targetUserId = userId || user?._id;
532
+ if (!targetUserId) return [];
533
+
534
+ const targetUser = users.find(u => u._id === targetUserId);
535
+ if (!targetUser) return [];
536
+
537
+ // Store current tokens to restore later
538
+ const currentUser = user;
539
+ const wasCurrentUser = targetUserId === user?._id;
540
+
541
+ if (!wasCurrentUser) {
542
+ // Temporarily switch to target user's tokens
543
+ oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
544
+ }
545
+
546
+ try {
547
+ // Use the new OxyServices method
548
+ const sessions = await oxyServices.getUserSessions();
549
+ return sessions;
550
+ } finally {
551
+ if (!wasCurrentUser && currentUser) {
552
+ // Restore original tokens
553
+ oxyServices.setTokens(currentUser.accessToken, currentUser.refreshToken || currentUser.accessToken);
554
+ }
555
+ }
556
+ } catch (err: any) {
557
+ console.error('Get user sessions failed:', err);
558
+ return [];
559
+ }
560
+ };
561
+
562
+ // Logout specific session
563
+ const logoutSession = async (sessionId: string, userId?: string): Promise<void> => {
564
+ try {
565
+ const targetUserId = userId || user?._id;
566
+ if (!targetUserId) return;
567
+
568
+ const targetUser = users.find(u => u._id === targetUserId);
569
+ if (!targetUser) return;
570
+
571
+ // Store current tokens to restore later
572
+ const currentUser = user;
573
+ const wasCurrentUser = targetUserId === user?._id;
574
+
575
+ if (!wasCurrentUser) {
576
+ // Temporarily switch to target user's tokens
577
+ oxyServices.setTokens(targetUser.accessToken, targetUser.refreshToken || targetUser.accessToken);
578
+ }
579
+
580
+ try {
581
+ // Use the new OxyServices method
582
+ await oxyServices.logoutSession(sessionId);
583
+
584
+ // If this is the current user's session, remove them from local state
585
+ if (wasCurrentUser && sessionId === targetUser.sessionId) {
586
+ await removeUser(targetUserId);
587
+ }
588
+ } finally {
589
+ if (!wasCurrentUser && currentUser) {
590
+ // Restore original tokens
591
+ oxyServices.setTokens(currentUser.accessToken, currentUser.refreshToken || currentUser.accessToken);
592
+ }
593
+ }
594
+ } catch (err: any) {
595
+ console.error('Logout session failed:', err);
596
+ throw err;
597
+ }
598
+ };
599
+
266
600
  // Sign up method
267
601
  const signUp = async (username: string, email: string, password: string): Promise<User> => {
268
602
  if (!storage) throw new Error('Storage not initialized');
@@ -322,14 +656,31 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
322
656
 
323
657
  // Build context value
324
658
  const contextValue: OxyContextState = {
659
+ // Single user state (current active user)
325
660
  user,
326
661
  isAuthenticated: !!user,
327
662
  isLoading,
328
663
  error,
664
+
665
+ // Multi-user state
666
+ users,
667
+
668
+ // Auth methods
329
669
  login,
330
670
  logout,
671
+ logoutAll,
331
672
  signUp,
673
+
674
+ // Multi-user methods
675
+ switchUser,
676
+ removeUser,
677
+ getUserSessions,
678
+ logoutSession,
679
+
680
+ // OxyServices instance
332
681
  oxyServices,
682
+
683
+ // Bottom sheet methods
333
684
  bottomSheetRef,
334
685
  showBottomSheet,
335
686
  hideBottomSheet,
package/src/ui/index.ts CHANGED
@@ -23,6 +23,7 @@ export { fontFamilies, fontStyles } from './styles/fonts';
23
23
  export { default as SignInScreen } from './screens/SignInScreen';
24
24
  export { default as SignUpScreen } from './screens/SignUpScreen';
25
25
  export { default as AccountCenterScreen } from './screens/AccountCenterScreen';
26
+ export { default as SessionManagementScreen } from './screens/SessionManagementScreen';
26
27
  export { default as AccountOverviewScreen } from './screens/AccountOverviewScreen';
27
28
  export { default as AccountSettingsScreen } from './screens/AccountSettingsScreen';
28
29