@oxyhq/services 5.6.0 → 5.6.2
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/ui/context/OxyContext.js +124 -53
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +84 -25
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/authStore.js +36 -1
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/commonjs/utils/deviceManager.js +7 -3
- package/lib/commonjs/utils/deviceManager.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +125 -54
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +84 -25
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/stores/authStore.js +36 -1
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/module/utils/deviceManager.js +7 -3
- package/lib/module/utils/deviceManager.js.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +6 -0
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/stores/authStore.d.ts +2 -0
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/lib/typescript/utils/deviceManager.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/models/interfaces.ts +7 -1
- package/src/ui/context/OxyContext.tsx +80 -56
- package/src/ui/screens/AccountSettingsScreen.tsx +68 -24
- package/src/ui/stores/authStore.ts +21 -0
- package/src/utils/deviceManager.ts +12 -4
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { createContext, useContext,
|
|
1
|
+
import React, { createContext, useContext, useEffect, useCallback, ReactNode, useMemo } from 'react';
|
|
2
2
|
import { OxyServices } from '../../core';
|
|
3
3
|
import { User } from '../../models/interfaces';
|
|
4
4
|
import { SecureLoginResponse, SecureClientSession, MinimalUserData } from '../../models/secureSession';
|
|
5
5
|
import { DeviceManager } from '../../utils/deviceManager';
|
|
6
6
|
import { useSessionSocket } from '../hooks/useSessionSocket';
|
|
7
7
|
import { toast } from '../../lib/sonner';
|
|
8
|
+
import { useAuthStore } from '../stores/authStore';
|
|
8
9
|
|
|
9
10
|
// Define the context shape
|
|
10
11
|
export interface OxyContextState {
|
|
@@ -95,7 +96,7 @@ const getStorage = async (): Promise<StorageInterface> => {
|
|
|
95
96
|
if (!AsyncStorage) {
|
|
96
97
|
try {
|
|
97
98
|
const asyncStorageModule = await import('@react-native-async-storage/async-storage');
|
|
98
|
-
AsyncStorage = asyncStorageModule.default;
|
|
99
|
+
AsyncStorage = (asyncStorageModule.default as unknown) as StorageInterface;
|
|
99
100
|
} catch (error) {
|
|
100
101
|
console.error('Failed to import AsyncStorage:', error);
|
|
101
102
|
throw new Error('AsyncStorage is required in React Native environment');
|
|
@@ -120,14 +121,22 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
120
121
|
onAuthStateChange,
|
|
121
122
|
bottomSheetRef,
|
|
122
123
|
}) => {
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
124
|
+
// Zustand state
|
|
125
|
+
const user = useAuthStore((state) => state.user);
|
|
126
|
+
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
|
127
|
+
const isLoading = useAuthStore((state) => state.isLoading);
|
|
128
|
+
const error = useAuthStore((state) => state.error);
|
|
129
|
+
const loginSuccess = useAuthStore((state) => state.loginSuccess);
|
|
130
|
+
const loginFailure = useAuthStore((state) => state.loginFailure);
|
|
131
|
+
const logoutStore = useAuthStore((state) => state.logout);
|
|
132
|
+
|
|
133
|
+
// Local state for non-auth fields
|
|
134
|
+
const [minimalUser, setMinimalUser] = React.useState<MinimalUserData | null>(null);
|
|
135
|
+
const [sessions, setSessions] = React.useState<SecureClientSession[]>([]);
|
|
136
|
+
const [activeSessionId, setActiveSessionId] = React.useState<string | null>(null);
|
|
137
|
+
const [storage, setStorage] = React.useState<StorageInterface | null>(null);
|
|
138
|
+
// Add a new state to track token restoration
|
|
139
|
+
const [tokenReady, setTokenReady] = React.useState(false);
|
|
131
140
|
|
|
132
141
|
// Storage keys (memoized to prevent infinite loops)
|
|
133
142
|
const keys = useMemo(() => getSecureStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
|
|
@@ -140,7 +149,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
140
149
|
setStorage(platformStorage);
|
|
141
150
|
} catch (error) {
|
|
142
151
|
console.error('Failed to initialize storage:', error);
|
|
143
|
-
|
|
152
|
+
useAuthStore.setState({ error: 'Failed to initialize storage' });
|
|
144
153
|
}
|
|
145
154
|
};
|
|
146
155
|
|
|
@@ -152,7 +161,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
152
161
|
const initAuth = async () => {
|
|
153
162
|
if (!storage) return;
|
|
154
163
|
|
|
155
|
-
|
|
164
|
+
useAuthStore.setState({ isLoading: true });
|
|
156
165
|
try {
|
|
157
166
|
// Load stored sessions
|
|
158
167
|
const sessionsData = await storage.getItem(keys.sessions);
|
|
@@ -216,7 +225,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
216
225
|
|
|
217
226
|
// Load full user data
|
|
218
227
|
const fullUser = await oxyServices.getUserBySession(activeSession.sessionId);
|
|
219
|
-
|
|
228
|
+
loginSuccess(fullUser);
|
|
220
229
|
setMinimalUser({
|
|
221
230
|
id: fullUser.id,
|
|
222
231
|
username: fullUser.username,
|
|
@@ -241,7 +250,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
241
250
|
console.error('Secure auth initialization error:', err);
|
|
242
251
|
await clearAllStorage();
|
|
243
252
|
} finally {
|
|
244
|
-
|
|
253
|
+
useAuthStore.setState({ isLoading: false });
|
|
245
254
|
}
|
|
246
255
|
};
|
|
247
256
|
|
|
@@ -250,6 +259,26 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
250
259
|
}
|
|
251
260
|
}, [storage, oxyServices, keys, onAuthStateChange]);
|
|
252
261
|
|
|
262
|
+
// Effect to restore token on app load or session switch
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const restoreToken = async () => {
|
|
265
|
+
if (activeSessionId && oxyServices) {
|
|
266
|
+
try {
|
|
267
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
268
|
+
setTokenReady(true);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
// If token restoration fails, force logout
|
|
271
|
+
await logout();
|
|
272
|
+
setTokenReady(false);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
setTokenReady(true); // No session, so token is not needed
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
restoreToken();
|
|
279
|
+
// Only run when activeSessionId or oxyServices changes
|
|
280
|
+
}, [activeSessionId, oxyServices]);
|
|
281
|
+
|
|
253
282
|
// Remove invalid session
|
|
254
283
|
const removeInvalidSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
255
284
|
const filteredSessions = sessions.filter(s => s.sessionId !== sessionId);
|
|
@@ -262,7 +291,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
262
291
|
} else {
|
|
263
292
|
// No valid sessions left
|
|
264
293
|
setActiveSessionId(null);
|
|
265
|
-
|
|
294
|
+
logoutStore();
|
|
266
295
|
setMinimalUser(null);
|
|
267
296
|
await storage?.removeItem(keys.activeSessionId);
|
|
268
297
|
|
|
@@ -270,7 +299,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
270
299
|
onAuthStateChange(null);
|
|
271
300
|
}
|
|
272
301
|
}
|
|
273
|
-
}, [sessions, storage, keys, onAuthStateChange]);
|
|
302
|
+
}, [sessions, storage, keys, onAuthStateChange, logoutStore]);
|
|
274
303
|
|
|
275
304
|
// Save sessions to storage
|
|
276
305
|
const saveSessionsToStorage = useCallback(async (sessionsList: SecureClientSession[]): Promise<void> => {
|
|
@@ -298,7 +327,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
298
327
|
// Switch to a different session
|
|
299
328
|
const switchToSession = useCallback(async (sessionId: string): Promise<void> => {
|
|
300
329
|
try {
|
|
301
|
-
|
|
330
|
+
useAuthStore.setState({ isLoading: true });
|
|
302
331
|
|
|
303
332
|
// Get access token for this session
|
|
304
333
|
await oxyServices.getTokenBySession(sessionId);
|
|
@@ -307,7 +336,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
307
336
|
const fullUser = await oxyServices.getUserBySession(sessionId);
|
|
308
337
|
|
|
309
338
|
setActiveSessionId(sessionId);
|
|
310
|
-
|
|
339
|
+
loginSuccess(fullUser);
|
|
311
340
|
setMinimalUser({
|
|
312
341
|
id: fullUser.id,
|
|
313
342
|
username: fullUser.username,
|
|
@@ -321,18 +350,16 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
321
350
|
}
|
|
322
351
|
} catch (error) {
|
|
323
352
|
console.error('Switch session error:', error);
|
|
324
|
-
|
|
353
|
+
useAuthStore.setState({ error: 'Failed to switch session' });
|
|
325
354
|
} finally {
|
|
326
|
-
|
|
355
|
+
useAuthStore.setState({ isLoading: false });
|
|
327
356
|
}
|
|
328
|
-
}, [oxyServices, onAuthStateChange, saveActiveSessionId]);
|
|
357
|
+
}, [oxyServices, onAuthStateChange, loginSuccess, saveActiveSessionId]);
|
|
329
358
|
|
|
330
359
|
// Secure login method
|
|
331
360
|
const login = async (username: string, password: string, deviceName?: string): Promise<User> => {
|
|
332
361
|
if (!storage) throw new Error('Storage not initialized');
|
|
333
|
-
|
|
334
|
-
setIsLoading(true);
|
|
335
|
-
setError(null);
|
|
362
|
+
useAuthStore.setState({ isLoading: true, error: null });
|
|
336
363
|
|
|
337
364
|
try {
|
|
338
365
|
// Get device fingerprint for enhanced device identification
|
|
@@ -398,7 +425,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
398
425
|
|
|
399
426
|
// Load full user data
|
|
400
427
|
const fullUser = await oxyServices.getUserBySession(response.sessionId);
|
|
401
|
-
|
|
428
|
+
loginSuccess(fullUser);
|
|
402
429
|
setMinimalUser(response.user);
|
|
403
430
|
|
|
404
431
|
if (onAuthStateChange) {
|
|
@@ -407,10 +434,10 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
407
434
|
|
|
408
435
|
return fullUser;
|
|
409
436
|
} catch (error: any) {
|
|
410
|
-
|
|
437
|
+
loginFailure(error.message || 'Login failed');
|
|
411
438
|
throw error;
|
|
412
439
|
} finally {
|
|
413
|
-
|
|
440
|
+
useAuthStore.setState({ isLoading: false });
|
|
414
441
|
}
|
|
415
442
|
};
|
|
416
443
|
|
|
@@ -435,7 +462,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
435
462
|
} else {
|
|
436
463
|
// No sessions left
|
|
437
464
|
setActiveSessionId(null);
|
|
438
|
-
|
|
465
|
+
logoutStore();
|
|
439
466
|
setMinimalUser(null);
|
|
440
467
|
await storage?.removeItem(keys.activeSessionId);
|
|
441
468
|
|
|
@@ -446,7 +473,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
446
473
|
}
|
|
447
474
|
} catch (error) {
|
|
448
475
|
console.error('Logout error:', error);
|
|
449
|
-
|
|
476
|
+
useAuthStore.setState({ error: 'Logout failed' });
|
|
450
477
|
}
|
|
451
478
|
};
|
|
452
479
|
|
|
@@ -456,13 +483,13 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
456
483
|
|
|
457
484
|
if (!activeSessionId) {
|
|
458
485
|
console.error('No active session ID found, cannot logout all');
|
|
459
|
-
|
|
486
|
+
useAuthStore.setState({ error: 'No active session found' });
|
|
460
487
|
throw new Error('No active session found');
|
|
461
488
|
}
|
|
462
489
|
|
|
463
490
|
if (!oxyServices) {
|
|
464
491
|
console.error('OxyServices not initialized');
|
|
465
|
-
|
|
492
|
+
useAuthStore.setState({ error: 'Service not available' });
|
|
466
493
|
throw new Error('Service not available');
|
|
467
494
|
}
|
|
468
495
|
|
|
@@ -474,7 +501,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
474
501
|
// Clear all local data
|
|
475
502
|
setSessions([]);
|
|
476
503
|
setActiveSessionId(null);
|
|
477
|
-
|
|
504
|
+
logoutStore();
|
|
478
505
|
setMinimalUser(null);
|
|
479
506
|
await clearAllStorage();
|
|
480
507
|
console.log('Local storage cleared');
|
|
@@ -485,7 +512,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
485
512
|
}
|
|
486
513
|
} catch (error) {
|
|
487
514
|
console.error('Logout all error:', error);
|
|
488
|
-
|
|
515
|
+
useAuthStore.setState({ error: `Logout all failed: ${error instanceof Error ? error.message : 'Unknown error'}` });
|
|
489
516
|
throw error;
|
|
490
517
|
}
|
|
491
518
|
};
|
|
@@ -494,8 +521,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
494
521
|
const signUp = async (username: string, email: string, password: string): Promise<User> => {
|
|
495
522
|
if (!storage) throw new Error('Storage not initialized');
|
|
496
523
|
|
|
497
|
-
|
|
498
|
-
setError(null);
|
|
524
|
+
useAuthStore.setState({ isLoading: true, error: null });
|
|
499
525
|
|
|
500
526
|
try {
|
|
501
527
|
// Create new account using the OxyServices signUp method
|
|
@@ -509,10 +535,10 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
509
535
|
|
|
510
536
|
return user;
|
|
511
537
|
} catch (error: any) {
|
|
512
|
-
|
|
538
|
+
loginFailure(error.message || 'Sign up failed');
|
|
513
539
|
throw error;
|
|
514
540
|
} finally {
|
|
515
|
-
|
|
541
|
+
useAuthStore.setState({ isLoading: false });
|
|
516
542
|
}
|
|
517
543
|
};
|
|
518
544
|
|
|
@@ -569,7 +595,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
569
595
|
// Clear all local sessions since we logged out from all devices
|
|
570
596
|
setSessions([]);
|
|
571
597
|
setActiveSessionId(null);
|
|
572
|
-
|
|
598
|
+
logoutStore();
|
|
573
599
|
setMinimalUser(null);
|
|
574
600
|
await clearAllStorage();
|
|
575
601
|
|
|
@@ -640,16 +666,6 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
640
666
|
}
|
|
641
667
|
}, [bottomSheetRef]);
|
|
642
668
|
|
|
643
|
-
// Compute comprehensive authentication status
|
|
644
|
-
// This is the single source of truth for authentication across the entire app
|
|
645
|
-
const isAuthenticated = useMemo(() => {
|
|
646
|
-
// User is authenticated if:
|
|
647
|
-
// 1. We have a full user object loaded, OR
|
|
648
|
-
// 2. We have an active session with a valid token
|
|
649
|
-
// This covers both the loaded state and the loading-but-authenticated state
|
|
650
|
-
return !!user || (!!activeSessionId && !!oxyServices?.getCurrentUserId());
|
|
651
|
-
}, [user, activeSessionId, oxyServices]);
|
|
652
|
-
|
|
653
669
|
// Integrate socket for real-time session updates
|
|
654
670
|
console.log('OxyContextProvider: userId', user?.id, 'baseURL', oxyServices.getBaseURL());
|
|
655
671
|
useSessionSocket({
|
|
@@ -675,20 +691,28 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
675
691
|
error,
|
|
676
692
|
login,
|
|
677
693
|
logout,
|
|
678
|
-
logoutAll,
|
|
679
|
-
signUp,
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
694
|
+
logoutAll: async () => { await logout(); },
|
|
695
|
+
signUp: async (username, email, password) => {
|
|
696
|
+
await signUp(username, email, password);
|
|
697
|
+
return user as User; // Return the latest user from Zustand
|
|
698
|
+
},
|
|
699
|
+
switchSession: async (sessionId) => { await switchToSession(sessionId); },
|
|
700
|
+
removeSession: async (sessionId) => { await removeSession(sessionId); },
|
|
701
|
+
refreshSessions: async () => { await refreshSessions(); },
|
|
702
|
+
getDeviceSessions: async () => { return await getDeviceSessions(); },
|
|
703
|
+
logoutAllDeviceSessions: async () => { await logoutAllDeviceSessions(); },
|
|
704
|
+
updateDeviceName: async (deviceName) => { await updateDeviceName(deviceName); },
|
|
686
705
|
oxyServices,
|
|
687
706
|
bottomSheetRef,
|
|
688
707
|
showBottomSheet,
|
|
689
708
|
hideBottomSheet,
|
|
690
709
|
};
|
|
691
710
|
|
|
711
|
+
// Wrap children rendering to block until token is ready
|
|
712
|
+
if (!tokenReady) {
|
|
713
|
+
return <div>Loading authentication...</div>;
|
|
714
|
+
}
|
|
715
|
+
|
|
692
716
|
return (
|
|
693
717
|
<OxyContext.Provider value={contextValue}>
|
|
694
718
|
{children}
|
|
@@ -19,6 +19,7 @@ import { Ionicons } from '@expo/vector-icons';
|
|
|
19
19
|
import { toast } from '../../lib/sonner';
|
|
20
20
|
import { fontFamilies } from '../styles/fonts';
|
|
21
21
|
import { confirmAction } from '../utils/confirmAction';
|
|
22
|
+
import { useAuthStore } from '../stores/authStore';
|
|
22
23
|
|
|
23
24
|
const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
24
25
|
onClose,
|
|
@@ -27,6 +28,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
27
28
|
navigate,
|
|
28
29
|
}) => {
|
|
29
30
|
const { user, oxyServices, isLoading: authLoading, isAuthenticated } = useOxy();
|
|
31
|
+
const updateUser = useAuthStore((state) => state.updateUser);
|
|
30
32
|
const [isLoading, setIsLoading] = useState(false);
|
|
31
33
|
const [isSaving, setIsSaving] = useState(false);
|
|
32
34
|
|
|
@@ -35,11 +37,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
35
37
|
|
|
36
38
|
// Form state
|
|
37
39
|
const [displayName, setDisplayName] = useState('');
|
|
40
|
+
const [lastName, setLastName] = useState('');
|
|
38
41
|
const [username, setUsername] = useState('');
|
|
39
42
|
const [email, setEmail] = useState('');
|
|
40
43
|
const [bio, setBio] = useState('');
|
|
41
44
|
const [location, setLocation] = useState('');
|
|
42
|
-
const [
|
|
45
|
+
const [links, setLinks] = useState<string[]>([]);
|
|
43
46
|
const [avatarUrl, setAvatarUrl] = useState('');
|
|
44
47
|
|
|
45
48
|
// Editing states
|
|
@@ -47,11 +50,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
47
50
|
|
|
48
51
|
// Temporary input states for inline editing
|
|
49
52
|
const [tempDisplayName, setTempDisplayName] = useState('');
|
|
53
|
+
const [tempLastName, setTempLastName] = useState('');
|
|
50
54
|
const [tempUsername, setTempUsername] = useState('');
|
|
51
55
|
const [tempEmail, setTempEmail] = useState('');
|
|
52
56
|
const [tempBio, setTempBio] = useState('');
|
|
53
57
|
const [tempLocation, setTempLocation] = useState('');
|
|
54
|
-
const [
|
|
58
|
+
const [tempLinks, setTempLinks] = useState<string[]>([]);
|
|
55
59
|
|
|
56
60
|
// Memoize theme-related calculations to prevent unnecessary recalculations
|
|
57
61
|
const themeStyles = useMemo(() => {
|
|
@@ -78,14 +82,19 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
78
82
|
if (user) {
|
|
79
83
|
const userDisplayName = typeof user.name === 'string'
|
|
80
84
|
? user.name
|
|
81
|
-
: user.name?.
|
|
82
|
-
|
|
85
|
+
: user.name?.first || user.name?.full || '';
|
|
86
|
+
const userLastName = typeof user.name === 'object' ? user.name?.last || '' : '';
|
|
83
87
|
setDisplayName(userDisplayName);
|
|
88
|
+
setLastName(userLastName);
|
|
84
89
|
setUsername(user.username || '');
|
|
85
90
|
setEmail(user.email || '');
|
|
86
91
|
setBio(user.bio || '');
|
|
87
92
|
setLocation(user.location || '');
|
|
88
|
-
|
|
93
|
+
setLinks(
|
|
94
|
+
Array.isArray(user.links)
|
|
95
|
+
? user.links.map(l => typeof l === 'string' ? l : l.link).filter(Boolean)
|
|
96
|
+
: user.website ? [user.website] : []
|
|
97
|
+
);
|
|
89
98
|
setAvatarUrl(user.avatar?.url || '');
|
|
90
99
|
}
|
|
91
100
|
}, [user]);
|
|
@@ -102,12 +111,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
102
111
|
email,
|
|
103
112
|
bio,
|
|
104
113
|
location,
|
|
105
|
-
|
|
114
|
+
links,
|
|
106
115
|
};
|
|
107
116
|
|
|
108
117
|
// Handle name field
|
|
109
|
-
if (displayName) {
|
|
110
|
-
updates.name = displayName;
|
|
118
|
+
if (displayName || lastName) {
|
|
119
|
+
updates.name = { first: displayName, last: lastName };
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
// Handle avatar
|
|
@@ -115,7 +124,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
115
124
|
updates.avatar = { url: avatarUrl };
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
await
|
|
127
|
+
await updateUser(updates, oxyServices);
|
|
119
128
|
toast.success('Profile updated successfully');
|
|
120
129
|
|
|
121
130
|
animateSaveButton(1); // Scale back to normal
|
|
@@ -144,7 +153,8 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
144
153
|
const startEditing = (type: string, currentValue: string) => {
|
|
145
154
|
switch (type) {
|
|
146
155
|
case 'displayName':
|
|
147
|
-
setTempDisplayName(
|
|
156
|
+
setTempDisplayName(displayName);
|
|
157
|
+
setTempLastName(lastName);
|
|
148
158
|
break;
|
|
149
159
|
case 'username':
|
|
150
160
|
setTempUsername(currentValue);
|
|
@@ -158,8 +168,8 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
158
168
|
case 'location':
|
|
159
169
|
setTempLocation(currentValue);
|
|
160
170
|
break;
|
|
161
|
-
case '
|
|
162
|
-
|
|
171
|
+
case 'links':
|
|
172
|
+
setTempLinks([...links]);
|
|
163
173
|
break;
|
|
164
174
|
}
|
|
165
175
|
setEditingField(type);
|
|
@@ -171,6 +181,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
171
181
|
switch (type) {
|
|
172
182
|
case 'displayName':
|
|
173
183
|
setDisplayName(tempDisplayName);
|
|
184
|
+
setLastName(tempLastName);
|
|
174
185
|
break;
|
|
175
186
|
case 'username':
|
|
176
187
|
setUsername(tempUsername);
|
|
@@ -184,8 +195,8 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
184
195
|
case 'location':
|
|
185
196
|
setLocation(tempLocation);
|
|
186
197
|
break;
|
|
187
|
-
case '
|
|
188
|
-
|
|
198
|
+
case 'links':
|
|
199
|
+
setLinks(tempLinks);
|
|
189
200
|
break;
|
|
190
201
|
}
|
|
191
202
|
|
|
@@ -207,7 +218,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
207
218
|
email: 'Email',
|
|
208
219
|
bio: 'Bio',
|
|
209
220
|
location: 'Location',
|
|
210
|
-
|
|
221
|
+
links: 'Links'
|
|
211
222
|
};
|
|
212
223
|
return labels[type as keyof typeof labels] || 'Field';
|
|
213
224
|
};
|
|
@@ -219,19 +230,52 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
219
230
|
email: { name: 'mail', color: '#FF9500' },
|
|
220
231
|
bio: { name: 'document-text', color: '#34C759' },
|
|
221
232
|
location: { name: 'location', color: '#FF3B30' },
|
|
222
|
-
|
|
233
|
+
links: { name: 'link', color: '#32D74B' }
|
|
223
234
|
};
|
|
224
235
|
return icons[type as keyof typeof icons] || { name: 'person', color: '#007AFF' };
|
|
225
236
|
};
|
|
226
237
|
|
|
227
238
|
const renderEditingField = (type: string) => {
|
|
239
|
+
if (type === 'displayName') {
|
|
240
|
+
return (
|
|
241
|
+
<View style={styles.editingFieldContainer}>
|
|
242
|
+
<View style={styles.editingFieldContent}>
|
|
243
|
+
<View style={[styles.newValueSection, { flexDirection: 'row', gap: 12 }]}>
|
|
244
|
+
<View style={{ flex: 1 }}>
|
|
245
|
+
<Text style={styles.editingFieldLabel}>First Name</Text>
|
|
246
|
+
<TextInput
|
|
247
|
+
style={styles.editingFieldInput}
|
|
248
|
+
value={tempDisplayName}
|
|
249
|
+
onChangeText={setTempDisplayName}
|
|
250
|
+
placeholder="Enter your first name"
|
|
251
|
+
placeholderTextColor={themeStyles.isDarkTheme ? '#aaa' : '#999'}
|
|
252
|
+
autoFocus
|
|
253
|
+
selectionColor={themeStyles.primaryColor}
|
|
254
|
+
/>
|
|
255
|
+
</View>
|
|
256
|
+
<View style={{ flex: 1 }}>
|
|
257
|
+
<Text style={styles.editingFieldLabel}>Last Name</Text>
|
|
258
|
+
<TextInput
|
|
259
|
+
style={styles.editingFieldInput}
|
|
260
|
+
value={tempLastName}
|
|
261
|
+
onChangeText={setTempLastName}
|
|
262
|
+
placeholder="Enter your last name"
|
|
263
|
+
placeholderTextColor={themeStyles.isDarkTheme ? '#aaa' : '#999'}
|
|
264
|
+
selectionColor={themeStyles.primaryColor}
|
|
265
|
+
/>
|
|
266
|
+
</View>
|
|
267
|
+
</View>
|
|
268
|
+
</View>
|
|
269
|
+
</View>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
228
272
|
const fieldConfig = {
|
|
229
273
|
displayName: { label: 'Display Name', value: displayName, placeholder: 'Enter your display name', icon: 'person', color: '#007AFF', multiline: false, keyboardType: 'default' as const },
|
|
230
274
|
username: { label: 'Username', value: username, placeholder: 'Choose a username', icon: 'at', color: '#5856D6', multiline: false, keyboardType: 'default' as const },
|
|
231
275
|
email: { label: 'Email', value: email, placeholder: 'Enter your email address', icon: 'mail', color: '#FF9500', multiline: false, keyboardType: 'email-address' as const },
|
|
232
276
|
bio: { label: 'Bio', value: bio, placeholder: 'Tell people about yourself...', icon: 'document-text', color: '#34C759', multiline: true, keyboardType: 'default' as const },
|
|
233
277
|
location: { label: 'Location', value: location, placeholder: 'Enter your location', icon: 'location', color: '#FF3B30', multiline: false, keyboardType: 'default' as const },
|
|
234
|
-
|
|
278
|
+
links: { label: 'Links', value: links.join(', '), placeholder: 'Enter your links (comma separated)', icon: 'link', color: '#32D74B', multiline: false, keyboardType: 'url' as const }
|
|
235
279
|
};
|
|
236
280
|
|
|
237
281
|
const config = fieldConfig[type as keyof typeof fieldConfig];
|
|
@@ -244,7 +288,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
244
288
|
case 'email': return tempEmail;
|
|
245
289
|
case 'bio': return tempBio;
|
|
246
290
|
case 'location': return tempLocation;
|
|
247
|
-
case '
|
|
291
|
+
case 'links': return tempLinks.join(', ');
|
|
248
292
|
default: return '';
|
|
249
293
|
}
|
|
250
294
|
})();
|
|
@@ -256,7 +300,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
256
300
|
case 'email': setTempEmail(text); break;
|
|
257
301
|
case 'bio': setTempBio(text); break;
|
|
258
302
|
case 'location': setTempLocation(text); break;
|
|
259
|
-
case '
|
|
303
|
+
case 'links': setTempLinks(text.split(',').map(s => s.trim()).filter(Boolean)); break;
|
|
260
304
|
}
|
|
261
305
|
};
|
|
262
306
|
|
|
@@ -446,7 +490,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
446
490
|
{renderField(
|
|
447
491
|
'displayName',
|
|
448
492
|
'Display Name',
|
|
449
|
-
displayName,
|
|
493
|
+
[displayName, lastName].filter(Boolean).join(' '), // Show full name
|
|
450
494
|
'Add your display name',
|
|
451
495
|
'person',
|
|
452
496
|
'#007AFF',
|
|
@@ -514,10 +558,10 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
514
558
|
)}
|
|
515
559
|
|
|
516
560
|
{renderField(
|
|
517
|
-
'
|
|
518
|
-
'
|
|
519
|
-
|
|
520
|
-
'Add your
|
|
561
|
+
'links',
|
|
562
|
+
'Links',
|
|
563
|
+
links.join(', '),
|
|
564
|
+
'Add your links',
|
|
521
565
|
'link',
|
|
522
566
|
'#32D74B',
|
|
523
567
|
false,
|
|
@@ -10,6 +10,8 @@ interface AuthState {
|
|
|
10
10
|
loginSuccess: (user: User) => void;
|
|
11
11
|
loginFailure: (error: string) => void;
|
|
12
12
|
logout: () => void;
|
|
13
|
+
fetchUser: (oxyServices: any) => Promise<void>;
|
|
14
|
+
updateUser: (updates: Partial<User>, oxyServices: any) => Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>) => void) => ({
|
|
@@ -21,4 +23,23 @@ export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>)
|
|
|
21
23
|
loginSuccess: (user: User) => set({ isLoading: false, isAuthenticated: true, user }),
|
|
22
24
|
loginFailure: (error: string) => set({ isLoading: false, error }),
|
|
23
25
|
logout: () => set({ user: null, isAuthenticated: false }),
|
|
26
|
+
fetchUser: async (oxyServices) => {
|
|
27
|
+
set({ isLoading: true, error: null });
|
|
28
|
+
try {
|
|
29
|
+
const user = await oxyServices.getCurrentUser();
|
|
30
|
+
set({ user, isLoading: false, isAuthenticated: true });
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
set({ error: error.message || 'Failed to fetch user', isLoading: false });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
updateUser: async (updates, oxyServices) => {
|
|
36
|
+
set({ isLoading: true, error: null });
|
|
37
|
+
try {
|
|
38
|
+
await oxyServices.updateProfile(updates);
|
|
39
|
+
// Immediately fetch the latest user data after update
|
|
40
|
+
await useAuthStore.getState().fetchUser(oxyServices);
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
set({ error: error.message || 'Failed to update user', isLoading: false });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
24
45
|
}));
|
|
@@ -35,12 +35,20 @@ export class DeviceManager {
|
|
|
35
35
|
/**
|
|
36
36
|
* Get appropriate storage for the platform
|
|
37
37
|
*/
|
|
38
|
-
private static async getStorage() {
|
|
38
|
+
private static async getStorage(): Promise<{
|
|
39
|
+
getItem: (key: string) => Promise<string | null>;
|
|
40
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
41
|
+
removeItem: (key: string) => Promise<void>;
|
|
42
|
+
}> {
|
|
39
43
|
if (this.isReactNative()) {
|
|
40
|
-
// Try to import AsyncStorage for React Native
|
|
41
44
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
45
|
+
const asyncStorageModule = await import('@react-native-async-storage/async-storage');
|
|
46
|
+
const storage = (asyncStorageModule.default as unknown) as import('@react-native-async-storage/async-storage').AsyncStorageStatic;
|
|
47
|
+
return {
|
|
48
|
+
getItem: storage.getItem.bind(storage),
|
|
49
|
+
setItem: storage.setItem.bind(storage),
|
|
50
|
+
removeItem: storage.removeItem.bind(storage),
|
|
51
|
+
};
|
|
44
52
|
} catch (error) {
|
|
45
53
|
console.error('AsyncStorage not available in React Native:', error);
|
|
46
54
|
throw new Error('AsyncStorage is required in React Native environment');
|