@oxyhq/services 5.17.3 → 5.17.5

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.
@@ -9,77 +9,77 @@ import type { RouteName } from '../navigation/routes';
9
9
  import type { BackupData } from '../../crypto';
10
10
 
11
11
  export interface OxyContextState {
12
- user: User | null;
13
- sessions: ClientSession[];
14
- activeSessionId: string | null;
15
- currentDeviceId: string | null;
16
- isAuthenticated: boolean;
17
- isLoading: boolean;
18
- isTokenReady: boolean;
19
- isStorageReady: boolean;
20
- error: string | null;
21
- currentLanguage: string;
22
- currentLanguageMetadata: ReturnType<typeof useLanguageManagement>['metadata'];
23
- currentLanguageName: string;
24
- currentNativeLanguageName: string;
12
+ user: User | null;
13
+ sessions: ClientSession[];
14
+ activeSessionId: string | null;
15
+ currentDeviceId: string | null;
16
+ isAuthenticated: boolean;
17
+ isLoading: boolean;
18
+ isTokenReady: boolean;
19
+ isStorageReady: boolean;
20
+ error: string | null;
21
+ currentLanguage: string;
22
+ currentLanguageMetadata: ReturnType<typeof useLanguageManagement>['metadata'];
23
+ currentLanguageName: string;
24
+ currentNativeLanguageName: string;
25
25
 
26
- // Identity management (public key authentication - offline-first)
27
- createIdentity: () => Promise<{ synced: boolean }>;
28
- importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
29
- signIn: (deviceName?: string) => Promise<User>;
30
- hasIdentity: () => Promise<boolean>;
31
- getPublicKey: () => Promise<string | null>;
32
- isIdentitySynced: () => Promise<boolean>;
33
- syncIdentity: () => Promise<User>;
34
- deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
35
- storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
36
- getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
37
- clearTransferCode: (transferId: string) => Promise<void>;
38
- getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
39
- getActiveTransferId: () => string | null;
40
- updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
26
+ // Identity management (public key authentication - offline-first)
27
+ createIdentity: () => Promise<{ synced: boolean }>;
28
+ importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
29
+ signIn: (deviceName?: string) => Promise<User>;
30
+ hasIdentity: () => Promise<boolean>;
31
+ getPublicKey: () => Promise<string | null>;
32
+ isIdentitySynced: () => Promise<boolean>;
33
+ syncIdentity: () => Promise<User>;
34
+ deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
35
+ storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
36
+ getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
37
+ clearTransferCode: (transferId: string) => Promise<void>;
38
+ getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
39
+ getActiveTransferId: () => string | null;
40
+ updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
41
41
 
42
- // Identity sync state (reactive, from Zustand store)
43
- identitySyncState: {
44
- isSynced: boolean;
45
- isSyncing: boolean;
46
- };
42
+ // Identity sync state (reactive, from Zustand store)
43
+ identitySyncState: {
44
+ isSynced: boolean;
45
+ isSyncing: boolean;
46
+ };
47
47
 
48
- // Session management
49
- logout: (targetSessionId?: string) => Promise<void>;
50
- logoutAll: () => Promise<void>;
51
- switchSession: (sessionId: string) => Promise<void>;
52
- removeSession: (sessionId: string) => Promise<void>;
53
- refreshSessions: () => Promise<void>;
54
- setLanguage: (languageId: string) => Promise<void>;
55
- getDeviceSessions: () => Promise<
56
- Array<{
57
- sessionId: string;
58
- deviceId: string;
59
- deviceName?: string;
60
- lastActive?: string;
61
- expiresAt?: string;
62
- }>
63
- >;
64
- logoutAllDeviceSessions: () => Promise<void>;
65
- updateDeviceName: (deviceName: string) => Promise<void>;
66
- clearSessionState: () => Promise<void>;
67
- clearAllAccountData: () => Promise<void>;
68
- oxyServices: OxyServices;
69
- useFollow?: UseFollowHook;
70
- showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
71
- openAvatarPicker: () => void;
48
+ // Session management
49
+ logout: (targetSessionId?: string) => Promise<void>;
50
+ logoutAll: () => Promise<void>;
51
+ switchSession: (sessionId: string) => Promise<void>;
52
+ removeSession: (sessionId: string) => Promise<void>;
53
+ refreshSessions: () => Promise<void>;
54
+ setLanguage: (languageId: string) => Promise<void>;
55
+ getDeviceSessions: () => Promise<
56
+ Array<{
57
+ sessionId: string;
58
+ deviceId: string;
59
+ deviceName?: string;
60
+ lastActive?: string;
61
+ expiresAt?: string;
62
+ }>
63
+ >;
64
+ logoutAllDeviceSessions: () => Promise<void>;
65
+ updateDeviceName: (deviceName: string) => Promise<void>;
66
+ clearSessionState: () => Promise<void>;
67
+ clearAllAccountData: () => Promise<void>;
68
+ oxyServices: OxyServices;
69
+ useFollow?: UseFollowHook;
70
+ showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
71
+ openAvatarPicker: () => void;
72
72
  }
73
73
 
74
74
  export const OxyContext = createContext<OxyContextState | null>(null);
75
75
 
76
76
  export interface OxyContextProviderProps {
77
- children: ReactNode;
78
- oxyServices?: OxyServices;
79
- baseURL?: string;
80
- storageKeyPrefix?: string;
81
- onAuthStateChange?: (user: User | null) => void;
82
- onError?: (error: ApiError) => void;
77
+ children: ReactNode;
78
+ oxyServices?: OxyServices;
79
+ baseURL?: string;
80
+ storageKeyPrefix?: string;
81
+ onAuthStateChange?: (user: User | null) => void;
82
+ onError?: (error: ApiError) => void;
83
83
  }
84
84
 
85
85
  /**
@@ -87,9 +87,9 @@ export interface OxyContextProviderProps {
87
87
  * Must be used within an OxyContextProvider.
88
88
  */
89
89
  export const useOxy = (): OxyContextState => {
90
- const context = useContext(OxyContext);
91
- if (!context) {
92
- throw new Error('useOxy must be used within an OxyContextProvider');
93
- }
94
- return context;
90
+ const context = useContext(OxyContext);
91
+ if (!context) {
92
+ throw new Error('useOxy must be used within an OxyContextProvider');
93
+ }
94
+ return context;
95
95
  };
@@ -96,18 +96,18 @@ export const useAuthOperations = ({
96
96
  const USER_ID_STORAGE_KEY = 'oxy_user_id';
97
97
 
98
98
  // Online-only sign-in: require backend availability
99
- // First, resolve userId (prefer locally stored value)
99
+ // First, look up the user by public key to get the correct userId
100
+ // This ensures we always use the userId that matches the current identity's public key
100
101
  let userId: string | null = null;
101
- if (storage) {
102
- userId = await storage.getItem(USER_ID_STORAGE_KEY);
103
- }
104
-
105
- if (!userId) {
106
- const userLookup = await oxyServices.getUserByPublicKey(publicKey);
107
- userId = userLookup.id;
108
- if (storage && userId) {
109
- await storage.setItem(USER_ID_STORAGE_KEY, userId).catch(() => {});
110
- }
102
+
103
+ // Always verify the userId matches the current public key
104
+ // This prevents auth failures when identity has changed
105
+ const userLookup = await oxyServices.getUserByPublicKey(publicKey);
106
+ userId = userLookup.id;
107
+
108
+ // Update stored userId to match current identity
109
+ if (storage && userId) {
110
+ await storage.setItem(USER_ID_STORAGE_KEY, userId).catch(() => {});
111
111
  }
112
112
 
113
113
  const challengeResponse = await oxyServices.requestChallenge(userId);
@@ -126,14 +126,30 @@ export const useAuthOperations = ({
126
126
  }
127
127
 
128
128
  // Verify and create session using userId
129
- const sessionResponse = await oxyServices.verifyChallenge(
130
- userId,
131
- challenge,
132
- signature,
133
- timestamp,
134
- deviceName,
135
- deviceFingerprint,
136
- );
129
+ let sessionResponse;
130
+ try {
131
+ sessionResponse = await oxyServices.verifyChallenge(
132
+ userId,
133
+ challenge,
134
+ signature,
135
+ timestamp,
136
+ deviceName,
137
+ deviceFingerprint,
138
+ );
139
+ } catch (verifyError) {
140
+ // Add detailed logging for 401 errors to help diagnose auth failures
141
+ if (__DEV__) {
142
+ console.error('[useAuthOperations] verifyChallenge failed:', {
143
+ error: verifyError,
144
+ userId,
145
+ challengeLength: challenge?.length,
146
+ signatureLength: signature?.length,
147
+ timestamp,
148
+ timeSinceChallenge: Date.now() - timestamp,
149
+ });
150
+ }
151
+ throw verifyError;
152
+ }
137
153
 
138
154
  // Store tokens immediately (no extra round-trip)
139
155
  oxyServices.setTokens(sessionResponse.accessToken, sessionResponse.refreshToken);
@@ -63,6 +63,21 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
63
63
  return;
64
64
  }
65
65
 
66
+ // IMPORTANT: If userId is set but we have no token, defer socket creation
67
+ // This prevents socket reconnection race condition during auth flow
68
+ // (e.g., when userId changes but tokens aren't set yet in transfer flow)
69
+ const freshToken = getAccessTokenRef.current();
70
+ if (!freshToken) {
71
+ logger.debug('Deferring socket creation - no access token available yet', { component: 'useSessionSocket', userId });
72
+ // Disconnect existing socket if it exists but we have no token
73
+ if (socketRef.current) {
74
+ socketRef.current.disconnect();
75
+ socketRef.current = null;
76
+ joinedRoomRef.current = null;
77
+ }
78
+ return;
79
+ }
80
+
66
81
  // Initialize socket with token refresh
67
82
  const initializeSocket = async () => {
68
83
  try {
@@ -96,6 +111,8 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
96
111
  };
97
112
  } else {
98
113
  logger.debug('No access token available for socket authentication', { component: 'useSessionSocket', userId });
114
+ // Defer socket creation if token is still missing
115
+ return;
99
116
  }
100
117
 
101
118
  socketRef.current = io(baseURL, socketOptions);