@oxyhq/services 5.2.2 → 5.2.4
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.
- package/lib/commonjs/core/index.js +117 -0
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/models/secureSession.js +2 -0
- package/lib/commonjs/models/secureSession.js.map +1 -0
- package/lib/commonjs/ui/context/LegacyOxyContext.js +643 -0
- package/lib/commonjs/ui/context/LegacyOxyContext.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +215 -450
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/SecureOxyContext.js +408 -0
- package/lib/commonjs/ui/context/SecureOxyContext.js.map +1 -0
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +3 -3
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +31 -30
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AppInfoScreen.js +4 -4
- package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SessionManagementScreen.js +34 -34
- package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js +2 -2
- package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/core/index.js +117 -0
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/models/secureSession.js +2 -0
- package/lib/module/models/secureSession.js.map +1 -0
- package/lib/module/ui/context/LegacyOxyContext.js +639 -0
- package/lib/module/ui/context/LegacyOxyContext.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +214 -450
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/SecureOxyContext.js +403 -0
- package/lib/module/ui/context/SecureOxyContext.js.map +1 -0
- package/lib/module/ui/screens/AccountCenterScreen.js +3 -3
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +31 -30
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/module/ui/screens/AppInfoScreen.js +4 -4
- package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/module/ui/screens/SessionManagementScreen.js +34 -34
- package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInScreen.js +2 -2
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/typescript/core/index.d.ts +51 -0
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/models/secureSession.d.ts +25 -0
- package/lib/typescript/models/secureSession.d.ts.map +1 -0
- package/lib/typescript/ui/context/LegacyOxyContext.d.ts +40 -0
- package/lib/typescript/ui/context/LegacyOxyContext.d.ts.map +1 -0
- package/lib/typescript/ui/context/OxyContext.d.ts +11 -12
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/SecureOxyContext.d.ts +39 -0
- package/lib/typescript/ui/context/SecureOxyContext.d.ts.map +1 -0
- package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +117 -0
- package/src/index.ts +2 -2
- package/src/models/secureSession.ts +27 -0
- package/src/ui/context/LegacyOxyContext.tsx +735 -0
- package/src/ui/context/OxyContext.tsx +412 -674
- package/src/ui/context/SecureOxyContext.tsx +473 -0
- package/src/ui/screens/AccountCenterScreen.tsx +4 -4
- package/src/ui/screens/AccountSwitcherScreen.tsx +36 -34
- package/src/ui/screens/AppInfoScreen.tsx +3 -3
- package/src/ui/screens/SessionManagementScreen.tsx +31 -35
- package/src/ui/screens/SignInScreen.tsx +2 -2
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode, useMemo } from 'react';
|
|
2
|
+
import { OxyServices } from '../../core';
|
|
3
|
+
import { User } from '../../models/interfaces';
|
|
4
|
+
import { SecureLoginResponse, SecureClientSession, MinimalUserData } from '../../models/secureSession';
|
|
5
|
+
|
|
6
|
+
// Define the secure context shape
|
|
7
|
+
export interface SecureOxyContextState {
|
|
8
|
+
// Authentication state
|
|
9
|
+
user: User | null; // Current active user (loaded from server)
|
|
10
|
+
minimalUser: MinimalUserData | null; // Minimal user data for UI
|
|
11
|
+
sessions: SecureClientSession[]; // All active sessions
|
|
12
|
+
activeSessionId: string | null;
|
|
13
|
+
isAuthenticated: boolean;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
error: string | null;
|
|
16
|
+
|
|
17
|
+
// Auth methods
|
|
18
|
+
secureLogin: (username: string, password: string, deviceName?: string) => Promise<User>;
|
|
19
|
+
logout: (targetSessionId?: string) => Promise<void>;
|
|
20
|
+
logoutAll: () => Promise<void>;
|
|
21
|
+
signUp: (username: string, email: string, password: string) => Promise<User>;
|
|
22
|
+
|
|
23
|
+
// Multi-session methods
|
|
24
|
+
switchSession: (sessionId: string) => Promise<void>;
|
|
25
|
+
removeSession: (sessionId: string) => Promise<void>;
|
|
26
|
+
refreshSessions: () => Promise<void>;
|
|
27
|
+
|
|
28
|
+
// Access to services
|
|
29
|
+
oxyServices: OxyServices;
|
|
30
|
+
bottomSheetRef?: React.RefObject<any>;
|
|
31
|
+
|
|
32
|
+
// Methods to directly control the bottom sheet
|
|
33
|
+
showBottomSheet?: (screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => void;
|
|
34
|
+
hideBottomSheet?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create the context with default values
|
|
38
|
+
const SecureOxyContext = createContext<SecureOxyContextState | null>(null);
|
|
39
|
+
|
|
40
|
+
// Props for the SecureOxyContextProvider
|
|
41
|
+
export interface SecureOxyContextProviderProps {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
oxyServices: OxyServices;
|
|
44
|
+
storageKeyPrefix?: string;
|
|
45
|
+
onAuthStateChange?: (user: User | null) => void;
|
|
46
|
+
bottomSheetRef?: React.RefObject<any>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Platform storage implementation
|
|
50
|
+
interface StorageInterface {
|
|
51
|
+
getItem: (key: string) => Promise<string | null>;
|
|
52
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
53
|
+
removeItem: (key: string) => Promise<void>;
|
|
54
|
+
clear: () => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Web localStorage implementation
|
|
58
|
+
class WebStorage implements StorageInterface {
|
|
59
|
+
async getItem(key: string): Promise<string | null> {
|
|
60
|
+
return localStorage.getItem(key);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
64
|
+
localStorage.setItem(key, value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async removeItem(key: string): Promise<void> {
|
|
68
|
+
localStorage.removeItem(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async clear(): Promise<void> {
|
|
72
|
+
localStorage.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// React Native AsyncStorage implementation
|
|
77
|
+
let AsyncStorage: StorageInterface;
|
|
78
|
+
|
|
79
|
+
// Determine the platform and set up storage
|
|
80
|
+
const isReactNative = (): boolean => {
|
|
81
|
+
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Get appropriate storage for the platform
|
|
85
|
+
const getStorage = async (): Promise<StorageInterface> => {
|
|
86
|
+
if (isReactNative()) {
|
|
87
|
+
if (!AsyncStorage) {
|
|
88
|
+
try {
|
|
89
|
+
const asyncStorageModule = await import('@react-native-async-storage/async-storage');
|
|
90
|
+
AsyncStorage = asyncStorageModule.default;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Failed to import AsyncStorage:', error);
|
|
93
|
+
throw new Error('AsyncStorage is required in React Native environment');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return AsyncStorage;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new WebStorage();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Storage keys for secure sessions
|
|
103
|
+
const getSecureStorageKeys = (prefix = 'oxy_secure') => ({
|
|
104
|
+
sessions: `${prefix}_sessions`, // Array of SecureClientSession objects
|
|
105
|
+
activeSessionId: `${prefix}_active_session_id`, // ID of currently active session
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const SecureOxyContextProvider: React.FC<SecureOxyContextProviderProps> = ({
|
|
109
|
+
children,
|
|
110
|
+
oxyServices,
|
|
111
|
+
storageKeyPrefix = 'oxy_secure',
|
|
112
|
+
onAuthStateChange,
|
|
113
|
+
bottomSheetRef,
|
|
114
|
+
}) => {
|
|
115
|
+
// Authentication state
|
|
116
|
+
const [user, setUser] = useState<User | null>(null);
|
|
117
|
+
const [minimalUser, setMinimalUser] = useState<MinimalUserData | null>(null);
|
|
118
|
+
const [sessions, setSessions] = useState<SecureClientSession[]>([]);
|
|
119
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
120
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
121
|
+
const [error, setError] = useState<string | null>(null);
|
|
122
|
+
const [storage, setStorage] = useState<StorageInterface | null>(null);
|
|
123
|
+
|
|
124
|
+
// Storage keys (memoized to prevent infinite loops)
|
|
125
|
+
const keys = useMemo(() => getSecureStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
|
|
126
|
+
|
|
127
|
+
// Initialize storage
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const initStorage = async () => {
|
|
130
|
+
try {
|
|
131
|
+
const platformStorage = await getStorage();
|
|
132
|
+
setStorage(platformStorage);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Failed to initialize storage:', error);
|
|
135
|
+
setError('Failed to initialize storage');
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
initStorage();
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// Effect to initialize authentication state
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const initAuth = async () => {
|
|
145
|
+
if (!storage) return;
|
|
146
|
+
|
|
147
|
+
setIsLoading(true);
|
|
148
|
+
try {
|
|
149
|
+
// Load stored sessions
|
|
150
|
+
const sessionsData = await storage.getItem(keys.sessions);
|
|
151
|
+
const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
|
|
152
|
+
|
|
153
|
+
console.log('SecureAuth - sessionsData:', sessionsData);
|
|
154
|
+
console.log('SecureAuth - activeSessionId:', storedActiveSessionId);
|
|
155
|
+
|
|
156
|
+
if (sessionsData) {
|
|
157
|
+
const parsedSessions: SecureClientSession[] = JSON.parse(sessionsData);
|
|
158
|
+
setSessions(parsedSessions);
|
|
159
|
+
|
|
160
|
+
if (storedActiveSessionId && parsedSessions.length > 0) {
|
|
161
|
+
const activeSession = parsedSessions.find(s => s.sessionId === storedActiveSessionId);
|
|
162
|
+
|
|
163
|
+
if (activeSession) {
|
|
164
|
+
console.log('SecureAuth - activeSession found:', activeSession);
|
|
165
|
+
|
|
166
|
+
// Validate session
|
|
167
|
+
try {
|
|
168
|
+
const validation = await oxyServices.validateSession(activeSession.sessionId);
|
|
169
|
+
|
|
170
|
+
if (validation.valid) {
|
|
171
|
+
console.log('SecureAuth - session validated successfully');
|
|
172
|
+
setActiveSessionId(activeSession.sessionId);
|
|
173
|
+
|
|
174
|
+
// Get access token for API calls
|
|
175
|
+
await oxyServices.getTokenBySession(activeSession.sessionId);
|
|
176
|
+
|
|
177
|
+
// Load full user data
|
|
178
|
+
const fullUser = await oxyServices.getUserBySession(activeSession.sessionId);
|
|
179
|
+
setUser(fullUser);
|
|
180
|
+
setMinimalUser({
|
|
181
|
+
id: fullUser.id,
|
|
182
|
+
username: fullUser.username,
|
|
183
|
+
avatar: fullUser.avatar
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (onAuthStateChange) {
|
|
187
|
+
onAuthStateChange(fullUser);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
console.log('SecureAuth - session invalid, removing');
|
|
191
|
+
await removeInvalidSession(activeSession.sessionId);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('SecureAuth - session validation error:', error);
|
|
195
|
+
await removeInvalidSession(activeSession.sessionId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error('Secure auth initialization error:', err);
|
|
202
|
+
await clearAllStorage();
|
|
203
|
+
} finally {
|
|
204
|
+
setIsLoading(false);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (storage) {
|
|
209
|
+
initAuth();
|
|
210
|
+
}
|
|
211
|
+
}, [storage, oxyServices, keys, onAuthStateChange]);
|
|
212
|
+
|
|
213
|
+
// Remove invalid session
|
|
214
|
+
const removeInvalidSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
215
|
+
const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
|
|
216
|
+
setSessions(filteredSessions);
|
|
217
|
+
await saveSessionsToStorage(filteredSessions);
|
|
218
|
+
|
|
219
|
+
// If there are other sessions, switch to the first one
|
|
220
|
+
if (filteredSessions.length > 0) {
|
|
221
|
+
await switchToSession(filteredSessions[0].sessionId);
|
|
222
|
+
} else {
|
|
223
|
+
// No valid sessions left
|
|
224
|
+
setActiveSessionId(null);
|
|
225
|
+
setUser(null);
|
|
226
|
+
setMinimalUser(null);
|
|
227
|
+
await storage?.removeItem(keys.activeSessionId);
|
|
228
|
+
|
|
229
|
+
if (onAuthStateChange) {
|
|
230
|
+
onAuthStateChange(null);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}, [sessions, storage, keys, onAuthStateChange]);
|
|
234
|
+
|
|
235
|
+
// Save sessions to storage
|
|
236
|
+
const saveSessionsToStorage = useCallback(async (sessionsList: SecureClientSession[]): Promise<void> => {
|
|
237
|
+
if (!storage) return;
|
|
238
|
+
await storage.setItem(keys.sessions, JSON.stringify(sessionsList));
|
|
239
|
+
}, [storage, keys.sessions]);
|
|
240
|
+
|
|
241
|
+
// Save active session ID to storage
|
|
242
|
+
const saveActiveSessionId = useCallback(async (sessionId: string): Promise<void> => {
|
|
243
|
+
if (!storage) return;
|
|
244
|
+
await storage.setItem(keys.activeSessionId, sessionId);
|
|
245
|
+
}, [storage, keys.activeSessionId]);
|
|
246
|
+
|
|
247
|
+
// Clear all storage
|
|
248
|
+
const clearAllStorage = useCallback(async (): Promise<void> => {
|
|
249
|
+
if (!storage) return;
|
|
250
|
+
try {
|
|
251
|
+
await storage.removeItem(keys.sessions);
|
|
252
|
+
await storage.removeItem(keys.activeSessionId);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error('Clear secure storage error:', err);
|
|
255
|
+
}
|
|
256
|
+
}, [storage, keys]);
|
|
257
|
+
|
|
258
|
+
// Switch to a different session
|
|
259
|
+
const switchToSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
260
|
+
try {
|
|
261
|
+
setIsLoading(true);
|
|
262
|
+
|
|
263
|
+
// Get access token for this session
|
|
264
|
+
await oxyServices.getTokenBySession(sessionId);
|
|
265
|
+
|
|
266
|
+
// Load full user data
|
|
267
|
+
const fullUser = await oxyServices.getUserBySession(sessionId);
|
|
268
|
+
|
|
269
|
+
setActiveSessionId(sessionId);
|
|
270
|
+
setUser(fullUser);
|
|
271
|
+
setMinimalUser({
|
|
272
|
+
id: fullUser.id,
|
|
273
|
+
username: fullUser.username,
|
|
274
|
+
avatar: fullUser.avatar
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await saveActiveSessionId(sessionId);
|
|
278
|
+
|
|
279
|
+
if (onAuthStateChange) {
|
|
280
|
+
onAuthStateChange(fullUser);
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('Switch session error:', error);
|
|
284
|
+
setError('Failed to switch session');
|
|
285
|
+
} finally {
|
|
286
|
+
setIsLoading(false);
|
|
287
|
+
}
|
|
288
|
+
}, [oxyServices, onAuthStateChange, saveActiveSessionId]);
|
|
289
|
+
|
|
290
|
+
// Secure login method
|
|
291
|
+
const secureLogin = async (username: string, password: string, deviceName?: string): Promise<User> => {
|
|
292
|
+
if (!storage) throw new Error('Storage not initialized');
|
|
293
|
+
|
|
294
|
+
setIsLoading(true);
|
|
295
|
+
setError(null);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const response: SecureLoginResponse = await oxyServices.secureLogin(username, password, deviceName);
|
|
299
|
+
|
|
300
|
+
// Create client session object
|
|
301
|
+
const clientSession: SecureClientSession = {
|
|
302
|
+
sessionId: response.sessionId,
|
|
303
|
+
deviceId: response.deviceId,
|
|
304
|
+
expiresAt: response.expiresAt,
|
|
305
|
+
lastActive: new Date().toISOString()
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Add to sessions list
|
|
309
|
+
const updatedSessions = [...sessions, clientSession];
|
|
310
|
+
setSessions(updatedSessions);
|
|
311
|
+
await saveSessionsToStorage(updatedSessions);
|
|
312
|
+
|
|
313
|
+
// Set as active session
|
|
314
|
+
setActiveSessionId(response.sessionId);
|
|
315
|
+
await saveActiveSessionId(response.sessionId);
|
|
316
|
+
|
|
317
|
+
// Get access token for API calls
|
|
318
|
+
await oxyServices.getTokenBySession(response.sessionId);
|
|
319
|
+
|
|
320
|
+
// Load full user data
|
|
321
|
+
const fullUser = await oxyServices.getUserBySession(response.sessionId);
|
|
322
|
+
setUser(fullUser);
|
|
323
|
+
setMinimalUser(response.user);
|
|
324
|
+
|
|
325
|
+
if (onAuthStateChange) {
|
|
326
|
+
onAuthStateChange(fullUser);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return fullUser;
|
|
330
|
+
} catch (error: any) {
|
|
331
|
+
setError(error.message || 'Login failed');
|
|
332
|
+
throw error;
|
|
333
|
+
} finally {
|
|
334
|
+
setIsLoading(false);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Logout method
|
|
339
|
+
const logout = async (targetSessionId?: string): Promise<void> => {
|
|
340
|
+
if (!activeSessionId) return;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const sessionToLogout = targetSessionId || activeSessionId;
|
|
344
|
+
await oxyServices.logoutSecureSession(activeSessionId, sessionToLogout);
|
|
345
|
+
|
|
346
|
+
// Remove session from local storage
|
|
347
|
+
const filteredSessions = sessions.filter(s => s.sessionId !== sessionToLogout);
|
|
348
|
+
setSessions(filteredSessions);
|
|
349
|
+
await saveSessionsToStorage(filteredSessions);
|
|
350
|
+
|
|
351
|
+
// If logging out active session
|
|
352
|
+
if (sessionToLogout === activeSessionId) {
|
|
353
|
+
if (filteredSessions.length > 0) {
|
|
354
|
+
// Switch to another session
|
|
355
|
+
await switchToSession(filteredSessions[0].sessionId);
|
|
356
|
+
} else {
|
|
357
|
+
// No sessions left
|
|
358
|
+
setActiveSessionId(null);
|
|
359
|
+
setUser(null);
|
|
360
|
+
setMinimalUser(null);
|
|
361
|
+
await storage?.removeItem(keys.activeSessionId);
|
|
362
|
+
|
|
363
|
+
if (onAuthStateChange) {
|
|
364
|
+
onAuthStateChange(null);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('Logout error:', error);
|
|
370
|
+
setError('Logout failed');
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Logout all sessions
|
|
375
|
+
const logoutAll = async (): Promise<void> => {
|
|
376
|
+
if (!activeSessionId) return;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
await oxyServices.logoutAllSecureSessions(activeSessionId);
|
|
380
|
+
|
|
381
|
+
// Clear all local data
|
|
382
|
+
setSessions([]);
|
|
383
|
+
setActiveSessionId(null);
|
|
384
|
+
setUser(null);
|
|
385
|
+
setMinimalUser(null);
|
|
386
|
+
await clearAllStorage();
|
|
387
|
+
|
|
388
|
+
if (onAuthStateChange) {
|
|
389
|
+
onAuthStateChange(null);
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error('Logout all error:', error);
|
|
393
|
+
setError('Logout all failed');
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Sign up method (placeholder - you can implement based on your needs)
|
|
398
|
+
const signUp = async (username: string, email: string, password: string): Promise<User> => {
|
|
399
|
+
// Implement sign up logic similar to secureLogin
|
|
400
|
+
throw new Error('Sign up not implemented yet');
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Switch session method
|
|
404
|
+
const switchSession = async (sessionId: string): Promise<void> => {
|
|
405
|
+
await switchToSession(sessionId);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Remove session method
|
|
409
|
+
const removeSession = async (sessionId: string): Promise<void> => {
|
|
410
|
+
await logout(sessionId);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Refresh sessions method
|
|
414
|
+
const refreshSessions = async (): Promise<void> => {
|
|
415
|
+
if (!activeSessionId) return;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const serverSessions = await oxyServices.getSessionsBySessionId(activeSessionId);
|
|
419
|
+
|
|
420
|
+
// Update local sessions with server data
|
|
421
|
+
const updatedSessions: SecureClientSession[] = serverSessions.map(serverSession => ({
|
|
422
|
+
sessionId: serverSession.sessionId,
|
|
423
|
+
deviceId: serverSession.deviceId,
|
|
424
|
+
expiresAt: new Date().toISOString(), // You might want to get this from server
|
|
425
|
+
lastActive: new Date().toISOString()
|
|
426
|
+
}));
|
|
427
|
+
|
|
428
|
+
setSessions(updatedSessions);
|
|
429
|
+
await saveSessionsToStorage(updatedSessions);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error('Refresh sessions error:', error);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// Context value
|
|
436
|
+
const contextValue: SecureOxyContextState = {
|
|
437
|
+
user,
|
|
438
|
+
minimalUser,
|
|
439
|
+
sessions,
|
|
440
|
+
activeSessionId,
|
|
441
|
+
isAuthenticated: !!user,
|
|
442
|
+
isLoading,
|
|
443
|
+
error,
|
|
444
|
+
secureLogin,
|
|
445
|
+
logout,
|
|
446
|
+
logoutAll,
|
|
447
|
+
signUp,
|
|
448
|
+
switchSession,
|
|
449
|
+
removeSession,
|
|
450
|
+
refreshSessions,
|
|
451
|
+
oxyServices,
|
|
452
|
+
bottomSheetRef,
|
|
453
|
+
showBottomSheet: undefined, // Implement as needed
|
|
454
|
+
hideBottomSheet: undefined, // Implement as needed
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<SecureOxyContext.Provider value={contextValue}>
|
|
459
|
+
{children}
|
|
460
|
+
</SecureOxyContext.Provider>
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Hook to use the secure context
|
|
465
|
+
export const useSecureOxyContext = (): SecureOxyContextState => {
|
|
466
|
+
const context = useContext(SecureOxyContext);
|
|
467
|
+
if (!context) {
|
|
468
|
+
throw new Error('useSecureOxyContext must be used within a SecureOxyContextProvider');
|
|
469
|
+
}
|
|
470
|
+
return context;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
export default SecureOxyContext;
|
|
@@ -20,7 +20,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
|
|
|
20
20
|
theme,
|
|
21
21
|
navigate,
|
|
22
22
|
}) => {
|
|
23
|
-
const { user, logout, isLoading,
|
|
23
|
+
const { user, logout, isLoading, sessions } = useOxy();
|
|
24
24
|
|
|
25
25
|
const isDarkTheme = theme === 'dark';
|
|
26
26
|
const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
|
|
@@ -94,14 +94,14 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
|
|
|
94
94
|
</View>
|
|
95
95
|
|
|
96
96
|
<View style={styles.actionsContainer}>
|
|
97
|
-
{/* Show Account Switcher if multiple
|
|
98
|
-
{
|
|
97
|
+
{/* Show Account Switcher if multiple sessions exist */}
|
|
98
|
+
{sessions && sessions.length > 1 && (
|
|
99
99
|
<TouchableOpacity
|
|
100
100
|
style={[styles.actionButton, { borderColor }]}
|
|
101
101
|
onPress={() => navigate('AccountSwitcher')}
|
|
102
102
|
>
|
|
103
103
|
<Text style={[styles.actionButtonText, { color: textColor }]}>
|
|
104
|
-
Switch Account ({
|
|
104
|
+
Switch Account ({sessions.length} accounts)
|
|
105
105
|
</Text>
|
|
106
106
|
</TouchableOpacity>
|
|
107
107
|
)}
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
Platform,
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import { BaseScreenProps } from '../navigation/types';
|
|
13
|
-
import { useOxy
|
|
13
|
+
import { useOxy } from '../context/OxyContext';
|
|
14
|
+
import { SecureClientSession } from '../../models/secureSession';
|
|
14
15
|
import { fontFamilies } from '../styles/fonts';
|
|
15
16
|
|
|
16
17
|
const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
@@ -21,9 +22,10 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
21
22
|
}) => {
|
|
22
23
|
const {
|
|
23
24
|
user,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
sessions,
|
|
26
|
+
activeSessionId,
|
|
27
|
+
switchSession,
|
|
28
|
+
removeSession,
|
|
27
29
|
logoutAll,
|
|
28
30
|
isLoading
|
|
29
31
|
} = useOxy();
|
|
@@ -40,31 +42,31 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
40
42
|
const dangerColor = '#D32F2F';
|
|
41
43
|
const successColor = '#2E7D32';
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
+
const handleSwitchSession = async (sessionId: string) => {
|
|
46
|
+
if (sessionId === user?.sessionId) return; // Already active session
|
|
45
47
|
|
|
46
|
-
setSwitchingToUserId(
|
|
48
|
+
setSwitchingToUserId(sessionId);
|
|
47
49
|
try {
|
|
48
|
-
await
|
|
50
|
+
await switchSession(sessionId);
|
|
49
51
|
Alert.alert('Success', 'Account switched successfully!');
|
|
50
52
|
if (onClose) {
|
|
51
53
|
onClose();
|
|
52
54
|
}
|
|
53
55
|
} catch (error) {
|
|
54
|
-
console.error('Switch
|
|
56
|
+
console.error('Switch session failed:', error);
|
|
55
57
|
Alert.alert('Switch Failed', 'There was a problem switching accounts. Please try again.');
|
|
56
58
|
} finally {
|
|
57
59
|
setSwitchingToUserId(null);
|
|
58
60
|
}
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
63
|
+
const handleRemoveSession = async (sessionId: string) => {
|
|
64
|
+
const sessionToRemove = sessions.find(s => s.sessionId === sessionId);
|
|
65
|
+
if (!sessionToRemove) return;
|
|
64
66
|
|
|
65
67
|
Alert.alert(
|
|
66
68
|
'Remove Account',
|
|
67
|
-
`Are you sure you want to remove
|
|
69
|
+
`Are you sure you want to remove this session from this device? You'll need to sign in again to access this account.`,
|
|
68
70
|
[
|
|
69
71
|
{
|
|
70
72
|
text: 'Cancel',
|
|
@@ -74,12 +76,12 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
74
76
|
text: 'Remove',
|
|
75
77
|
style: 'destructive',
|
|
76
78
|
onPress: async () => {
|
|
77
|
-
setRemovingUserId(
|
|
79
|
+
setRemovingUserId(sessionId);
|
|
78
80
|
try {
|
|
79
|
-
await
|
|
81
|
+
await removeSession(sessionId);
|
|
80
82
|
Alert.alert('Success', 'Account removed successfully!');
|
|
81
83
|
} catch (error) {
|
|
82
|
-
console.error('Remove
|
|
84
|
+
console.error('Remove session failed:', error);
|
|
83
85
|
Alert.alert('Remove Failed', 'There was a problem removing the account. Please try again.');
|
|
84
86
|
} finally {
|
|
85
87
|
setRemovingUserId(null);
|
|
@@ -121,14 +123,14 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
121
123
|
);
|
|
122
124
|
};
|
|
123
125
|
|
|
124
|
-
const
|
|
125
|
-
const isActive =
|
|
126
|
-
const isSwitching = switchingToUserId ===
|
|
127
|
-
const isRemoving = removingUserId ===
|
|
126
|
+
const renderSessionItem = (session: SecureClientSession) => {
|
|
127
|
+
const isActive = session.sessionId === activeSessionId;
|
|
128
|
+
const isSwitching = switchingToUserId === session.sessionId;
|
|
129
|
+
const isRemoving = removingUserId === session.sessionId;
|
|
128
130
|
|
|
129
131
|
return (
|
|
130
132
|
<View
|
|
131
|
-
key={
|
|
133
|
+
key={session.sessionId}
|
|
132
134
|
style={[
|
|
133
135
|
styles.userItem,
|
|
134
136
|
{
|
|
@@ -138,12 +140,12 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
138
140
|
]}
|
|
139
141
|
>
|
|
140
142
|
<View style={styles.userInfo}>
|
|
141
|
-
<Text style={[styles.username, { color: textColor }]}>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
<Text style={[styles.username, { color: textColor }]}>
|
|
144
|
+
{isActive ? user?.username || 'Current Account' : 'Account Session'}
|
|
145
|
+
</Text>
|
|
146
|
+
<Text style={[styles.email, { color: isDarkTheme ? '#BBBBBB' : '#666666' }]}>
|
|
147
|
+
Last active: {new Date(session.lastActive).toLocaleDateString()}
|
|
148
|
+
</Text>
|
|
147
149
|
{isActive && (
|
|
148
150
|
<View style={[styles.activeBadge, { backgroundColor: successColor }]}>
|
|
149
151
|
<Text style={styles.activeBadgeText}>Active</Text>
|
|
@@ -155,7 +157,7 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
155
157
|
{!isActive && (
|
|
156
158
|
<TouchableOpacity
|
|
157
159
|
style={[styles.switchButton, { borderColor: primaryColor }]}
|
|
158
|
-
onPress={() =>
|
|
160
|
+
onPress={() => handleSwitchSession(session.sessionId)}
|
|
159
161
|
disabled={isSwitching || isRemoving}
|
|
160
162
|
>
|
|
161
163
|
{isSwitching ? (
|
|
@@ -168,8 +170,8 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
168
170
|
|
|
169
171
|
<TouchableOpacity
|
|
170
172
|
style={[styles.removeButton, { borderColor: dangerColor }]}
|
|
171
|
-
onPress={() =>
|
|
172
|
-
disabled={isSwitching || isRemoving ||
|
|
173
|
+
onPress={() => handleRemoveSession(session.sessionId)}
|
|
174
|
+
disabled={isSwitching || isRemoving || sessions.length === 1}
|
|
173
175
|
>
|
|
174
176
|
{isRemoving ? (
|
|
175
177
|
<ActivityIndicator color={dangerColor} size="small" />
|
|
@@ -201,10 +203,10 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
201
203
|
|
|
202
204
|
<View style={styles.usersContainer}>
|
|
203
205
|
<Text style={[styles.sectionTitle, { color: textColor }]}>
|
|
204
|
-
|
|
206
|
+
Sessions ({sessions.length})
|
|
205
207
|
</Text>
|
|
206
208
|
|
|
207
|
-
{
|
|
209
|
+
{sessions.map(renderSessionItem)}
|
|
208
210
|
</View>
|
|
209
211
|
|
|
210
212
|
<View style={styles.actionsContainer}>
|
|
@@ -217,7 +219,7 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
217
219
|
</Text>
|
|
218
220
|
</TouchableOpacity>
|
|
219
221
|
|
|
220
|
-
{
|
|
222
|
+
{sessions.length > 1 && (
|
|
221
223
|
<TouchableOpacity
|
|
222
224
|
style={[styles.dangerButton, { backgroundColor: isDarkTheme ? '#300000' : '#FFEBEE' }]}
|
|
223
225
|
onPress={handleLogoutAll}
|
|
@@ -227,7 +229,7 @@ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
227
229
|
<ActivityIndicator color={dangerColor} size="small" />
|
|
228
230
|
) : (
|
|
229
231
|
<Text style={[styles.dangerButtonText, { color: dangerColor }]}>
|
|
230
|
-
Sign Out All
|
|
232
|
+
Sign Out All Sessions
|
|
231
233
|
</Text>
|
|
232
234
|
)}
|
|
233
235
|
</TouchableOpacity>
|