@oxyhq/services 5.16.11 → 5.16.13
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/crypto/keyManager.js +87 -22
- package/lib/commonjs/crypto/keyManager.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/TextFieldFlat.js +3 -1
- package/lib/commonjs/ui/components/TextField/TextFieldFlat.js.map +1 -1
- package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js +3 -1
- package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +32 -42
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useSessionManagement.js +1 -1
- package/lib/commonjs/ui/context/utils/errorHandlers.js +10 -0
- package/lib/commonjs/ui/context/utils/errorHandlers.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +8 -10
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +81 -5
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/crypto/keyManager.js +87 -22
- package/lib/module/crypto/keyManager.js.map +1 -1
- package/lib/module/ui/components/TextField/TextFieldFlat.js +3 -1
- package/lib/module/ui/components/TextField/TextFieldFlat.js.map +1 -1
- package/lib/module/ui/components/TextField/TextFieldOutlined.js +3 -1
- package/lib/module/ui/components/TextField/TextFieldOutlined.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +28 -37
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useSessionManagement.js +1 -1
- package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/context/utils/errorHandlers.js +10 -0
- package/lib/module/ui/context/utils/errorHandlers.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +8 -10
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +80 -5
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/crypto/keyManager.d.ts +9 -2
- package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +0 -4
- 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/context/utils/errorHandlers.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/utils/avatarUtils.d.ts +14 -0
- package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/crypto/keyManager.ts +92 -16
- package/src/models/interfaces.ts +1 -5
- package/src/ui/components/TextField/TextFieldFlat.tsx +1 -1
- package/src/ui/components/TextField/TextFieldOutlined.tsx +1 -1
- package/src/ui/context/OxyContext.tsx +36 -34
- package/src/ui/context/hooks/useSessionManagement.ts +1 -1
- package/src/ui/context/utils/errorHandlers.ts +10 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +8 -6
- package/src/ui/utils/avatarUtils.ts +91 -5
|
@@ -30,8 +30,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|
|
30
30
|
import { clearQueryCache } from '../hooks/queryClient';
|
|
31
31
|
import { KeyManager } from '../../crypto/keyManager';
|
|
32
32
|
import { translate } from '../../i18n';
|
|
33
|
-
import {
|
|
34
|
-
import { updateAvatarVisibility } from '../utils/avatarUtils';
|
|
33
|
+
import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
|
|
35
34
|
import { useAccountStore } from '../stores/accountStore';
|
|
36
35
|
|
|
37
36
|
export interface OxyContextState {
|
|
@@ -202,7 +201,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
202
201
|
|
|
203
202
|
const checkAndRestoreIdentity = async () => {
|
|
204
203
|
try {
|
|
205
|
-
const { KeyManager } = await import('../../crypto/index.js');
|
|
206
204
|
// Check if identity exists and verify integrity
|
|
207
205
|
const hasIdentity = await KeyManager.hasIdentity();
|
|
208
206
|
if (hasIdentity) {
|
|
@@ -210,14 +208,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
210
208
|
if (!isValid) {
|
|
211
209
|
// Try to restore from backup
|
|
212
210
|
const restored = await KeyManager.restoreIdentityFromBackup();
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (__DEV__) {
|
|
219
|
-
logger('Identity integrity check failed - user may need to restore from recovery phrase');
|
|
220
|
-
}
|
|
211
|
+
if (__DEV__) {
|
|
212
|
+
logger(restored
|
|
213
|
+
? 'Identity restored from backup successfully'
|
|
214
|
+
: 'Identity integrity check failed - user may need to restore from recovery phrase'
|
|
215
|
+
);
|
|
221
216
|
}
|
|
222
217
|
} else {
|
|
223
218
|
// Identity is valid - ensure backup is up to date
|
|
@@ -259,7 +254,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
259
254
|
});
|
|
260
255
|
|
|
261
256
|
const queryClient = useQueryClient();
|
|
262
|
-
const updateProfileMutation = useUpdateProfile();
|
|
263
257
|
|
|
264
258
|
const {
|
|
265
259
|
sessions,
|
|
@@ -319,9 +313,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
319
313
|
});
|
|
320
314
|
|
|
321
315
|
// syncIdentity - TanStack Query handles offline mutations automatically
|
|
322
|
-
const syncIdentity = useCallback(
|
|
323
|
-
return await syncIdentityBase();
|
|
324
|
-
}, [syncIdentityBase]);
|
|
316
|
+
const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
|
|
325
317
|
|
|
326
318
|
// Clear all account data when identity is lost (for accounts app)
|
|
327
319
|
// In accounts app, identity = account, so losing identity means losing everything
|
|
@@ -334,9 +326,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
334
326
|
try {
|
|
335
327
|
await clearQueryCache(storage);
|
|
336
328
|
} catch (error) {
|
|
337
|
-
|
|
338
|
-
logger('Failed to clear persisted query cache', error);
|
|
339
|
-
}
|
|
329
|
+
logger('Failed to clear persisted query cache', error);
|
|
340
330
|
}
|
|
341
331
|
}
|
|
342
332
|
|
|
@@ -348,9 +338,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
348
338
|
try {
|
|
349
339
|
await storage.removeItem('oxy_identity_synced');
|
|
350
340
|
} catch (error) {
|
|
351
|
-
|
|
352
|
-
logger('Failed to clear identity sync state', error);
|
|
353
|
-
}
|
|
341
|
+
logger('Failed to clear identity sync state', error);
|
|
354
342
|
}
|
|
355
343
|
}
|
|
356
344
|
|
|
@@ -421,20 +409,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
421
409
|
|
|
422
410
|
// If we were offline and now we're online, sync identity if needed
|
|
423
411
|
if (wasOffline) {
|
|
424
|
-
|
|
425
|
-
logger('Network reconnected, checking identity sync...');
|
|
426
|
-
}
|
|
412
|
+
logger('Network reconnected, checking identity sync...');
|
|
427
413
|
|
|
428
414
|
// Sync identity first (if not synced)
|
|
429
415
|
try {
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
|
|
416
|
+
const hasIdentityValue = await hasIdentity();
|
|
417
|
+
if (hasIdentityValue) {
|
|
418
|
+
// Check sync status directly - sync if not explicitly 'true'
|
|
419
|
+
// undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
|
|
420
|
+
const syncStatus = await storage.getItem('oxy_identity_synced');
|
|
421
|
+
if (syncStatus !== 'true') {
|
|
422
|
+
await syncIdentity();
|
|
423
|
+
}
|
|
433
424
|
}
|
|
434
425
|
} catch (syncError) {
|
|
435
|
-
|
|
436
|
-
logger('Error syncing identity on reconnect', syncError);
|
|
437
|
-
}
|
|
426
|
+
logger('Error syncing identity on reconnect', syncError);
|
|
438
427
|
}
|
|
439
428
|
|
|
440
429
|
// TanStack Query will automatically retry pending mutations
|
|
@@ -517,7 +506,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
517
506
|
});
|
|
518
507
|
}
|
|
519
508
|
} catch (validationError) {
|
|
520
|
-
|
|
509
|
+
// Silently handle expected 401 errors (expired/invalid sessions) during restoration
|
|
510
|
+
// Only log unexpected errors
|
|
511
|
+
if (!isInvalidSessionError(validationError)) {
|
|
512
|
+
logger('Session validation failed during init', validationError);
|
|
513
|
+
}
|
|
521
514
|
}
|
|
522
515
|
}
|
|
523
516
|
|
|
@@ -530,13 +523,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
530
523
|
try {
|
|
531
524
|
await switchSession(storedActiveSessionId);
|
|
532
525
|
} catch (switchError) {
|
|
526
|
+
// Silently handle expected 401 errors (expired/invalid active session)
|
|
533
527
|
if (isInvalidSessionError(switchError)) {
|
|
534
528
|
await storage.removeItem(storageKeys.activeSessionId);
|
|
535
529
|
updateSessions(
|
|
536
530
|
validSessions.filter((session) => session.sessionId !== storedActiveSessionId),
|
|
537
531
|
{ merge: false },
|
|
538
532
|
);
|
|
533
|
+
// Don't log expected session errors during restoration
|
|
539
534
|
} else {
|
|
535
|
+
// Only log unexpected errors
|
|
540
536
|
logger('Active session validation error', switchError);
|
|
541
537
|
}
|
|
542
538
|
}
|
|
@@ -638,8 +634,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
638
634
|
// Update file visibility to public for avatar
|
|
639
635
|
await updateAvatarVisibility(file.id, oxyServices, 'OxyContext');
|
|
640
636
|
|
|
641
|
-
// Update user profile
|
|
642
|
-
await
|
|
637
|
+
// Update user profile (handles query invalidation and accountStore update)
|
|
638
|
+
await updateProfileWithAvatar(
|
|
639
|
+
{ avatar: file.id },
|
|
640
|
+
oxyServices,
|
|
641
|
+
activeSessionId,
|
|
642
|
+
queryClient,
|
|
643
|
+
syncIdentity
|
|
644
|
+
);
|
|
643
645
|
|
|
644
646
|
toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
|
|
645
647
|
} catch (e: any) {
|
|
@@ -648,7 +650,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
648
650
|
},
|
|
649
651
|
},
|
|
650
652
|
});
|
|
651
|
-
}, [oxyServices, currentLanguage, showBottomSheetForContext,
|
|
653
|
+
}, [oxyServices, currentLanguage, showBottomSheetForContext, activeSessionId, queryClient, syncIdentity]);
|
|
652
654
|
|
|
653
655
|
const contextValue: OxyContextState = useMemo(() => ({
|
|
654
656
|
user,
|
|
@@ -7,7 +7,7 @@ import { getStorageKeys, type StorageInterface } from '../utils/storageHelpers';
|
|
|
7
7
|
import { handleAuthError, isInvalidSessionError } from '../utils/errorHandlers';
|
|
8
8
|
import type { OxyServices } from '../../../core';
|
|
9
9
|
import type { QueryClient } from '@tanstack/react-query';
|
|
10
|
-
import { clearQueryCache } from '../../hooks/queryClient
|
|
10
|
+
import { clearQueryCache } from '../../hooks/queryClient';
|
|
11
11
|
|
|
12
12
|
export interface UseSessionManagementOptions {
|
|
13
13
|
oxyServices: OxyServices;
|
|
@@ -53,11 +53,21 @@ export const isInvalidSessionError = (error: unknown): boolean => {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Check error.status directly (HttpService sets this)
|
|
57
|
+
if ((error as any).status === 401) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
const normalizedMessage = extractErrorMessage(error)?.toLowerCase();
|
|
57
62
|
if (!normalizedMessage) {
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
// Check for HTTP 401 in message (HttpService creates errors with "HTTP 401:" format)
|
|
67
|
+
if (normalizedMessage.includes('http 401') || normalizedMessage.includes('401')) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
return DEFAULT_INVALID_SESSION_MESSAGES.some((msg) =>
|
|
62
72
|
normalizedMessage.includes(msg.toLowerCase()),
|
|
63
73
|
);
|
|
@@ -219,15 +219,17 @@ export const useUploadAvatar = () => {
|
|
|
219
219
|
if (activeSessionId) {
|
|
220
220
|
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
// Refresh accountStore with cache-busted URL if avatar was updated
|
|
224
|
+
if (data?.avatar && activeSessionId && oxyServices) {
|
|
225
|
+
refreshAvatarInStore(activeSessionId, data.avatar, oxyServices);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Invalidate all related queries to refresh everywhere
|
|
222
229
|
invalidateUserQueries(queryClient);
|
|
230
|
+
invalidateAccountQueries(queryClient);
|
|
223
231
|
toast.success('Avatar updated successfully');
|
|
224
232
|
},
|
|
225
|
-
onSettled: () => {
|
|
226
|
-
queryClient.invalidateQueries({ queryKey: queryKeys.accounts.current() });
|
|
227
|
-
if (activeSessionId) {
|
|
228
|
-
queryClient.invalidateQueries({ queryKey: queryKeys.users.profile(activeSessionId) });
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
233
|
});
|
|
232
234
|
};
|
|
233
235
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { OxyServices } from '../../core';
|
|
2
|
+
import type { User } from '../../models/interfaces';
|
|
2
3
|
import { useAccountStore } from '../stores/accountStore';
|
|
4
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Updates file visibility to public for avatar use.
|
|
@@ -22,12 +25,11 @@ export async function updateAvatarVisibility(
|
|
|
22
25
|
|
|
23
26
|
try {
|
|
24
27
|
await oxyServices.assetUpdateVisibility(fileId, 'public');
|
|
25
|
-
|
|
28
|
+
// Visibility update is logged by the API
|
|
26
29
|
} catch (visError: any) {
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
30
|
+
// Silently handle errors - 404 means asset doesn't exist yet (which is OK)
|
|
31
|
+
// Other errors are logged by the API, so no need to log here
|
|
32
|
+
// Function continues gracefully regardless of visibility update success
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -51,3 +53,87 @@ export function refreshAvatarInStore(
|
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Updates user profile with avatar and handles all side effects (query invalidation, accountStore update).
|
|
58
|
+
* This function can be used from within OxyContext provider without requiring useOxy hook.
|
|
59
|
+
*
|
|
60
|
+
* @param updates - Profile updates including avatar
|
|
61
|
+
* @param oxyServices - OxyServices instance
|
|
62
|
+
* @param activeSessionId - Active session ID
|
|
63
|
+
* @param queryClient - TanStack Query client
|
|
64
|
+
* @param syncIdentity - Optional sync identity function for handling auth errors
|
|
65
|
+
* @returns Promise that resolves with updated user data
|
|
66
|
+
*/
|
|
67
|
+
export async function updateProfileWithAvatar(
|
|
68
|
+
updates: Partial<User>,
|
|
69
|
+
oxyServices: OxyServices,
|
|
70
|
+
activeSessionId: string | null,
|
|
71
|
+
queryClient: QueryClient,
|
|
72
|
+
syncIdentity?: () => Promise<User>
|
|
73
|
+
): Promise<User> {
|
|
74
|
+
// Ensure we have a valid token before making the request
|
|
75
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
76
|
+
try {
|
|
77
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
78
|
+
} catch (tokenError) {
|
|
79
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
80
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
81
|
+
if (syncIdentity) {
|
|
82
|
+
try {
|
|
83
|
+
await syncIdentity();
|
|
84
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
85
|
+
} catch (syncError) {
|
|
86
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
throw tokenError;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
throw tokenError;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const data = await oxyServices.updateProfile(updates);
|
|
99
|
+
|
|
100
|
+
// Update cache with server response
|
|
101
|
+
queryClient.setQueryData(queryKeys.accounts.current(), data);
|
|
102
|
+
if (activeSessionId) {
|
|
103
|
+
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If avatar was updated, refresh accountStore with cache-busted URL
|
|
107
|
+
if (updates.avatar && activeSessionId) {
|
|
108
|
+
refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Invalidate all related queries to refresh everywhere
|
|
112
|
+
invalidateUserQueries(queryClient);
|
|
113
|
+
invalidateAccountQueries(queryClient);
|
|
114
|
+
|
|
115
|
+
return data;
|
|
116
|
+
} catch (error: any) {
|
|
117
|
+
const errorMessage = error?.message || '';
|
|
118
|
+
const status = error?.status || error?.response?.status;
|
|
119
|
+
|
|
120
|
+
// Handle authentication errors
|
|
121
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
122
|
+
if (activeSessionId && syncIdentity) {
|
|
123
|
+
try {
|
|
124
|
+
await syncIdentity();
|
|
125
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
126
|
+
// Retry the update after getting token
|
|
127
|
+
return await updateProfileWithAvatar(updates, oxyServices, activeSessionId, queryClient, syncIdentity);
|
|
128
|
+
} catch (retryError) {
|
|
129
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error('No active session. Please sign in.');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|