@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.
Files changed (50) hide show
  1. package/lib/commonjs/crypto/keyManager.js +87 -22
  2. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  3. package/lib/commonjs/ui/components/TextField/TextFieldFlat.js +3 -1
  4. package/lib/commonjs/ui/components/TextField/TextFieldFlat.js.map +1 -1
  5. package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js +3 -1
  6. package/lib/commonjs/ui/components/TextField/TextFieldOutlined.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +32 -42
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useSessionManagement.js +1 -1
  10. package/lib/commonjs/ui/context/utils/errorHandlers.js +10 -0
  11. package/lib/commonjs/ui/context/utils/errorHandlers.js.map +1 -1
  12. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +8 -10
  13. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  14. package/lib/commonjs/ui/utils/avatarUtils.js +81 -5
  15. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  16. package/lib/module/crypto/keyManager.js +87 -22
  17. package/lib/module/crypto/keyManager.js.map +1 -1
  18. package/lib/module/ui/components/TextField/TextFieldFlat.js +3 -1
  19. package/lib/module/ui/components/TextField/TextFieldFlat.js.map +1 -1
  20. package/lib/module/ui/components/TextField/TextFieldOutlined.js +3 -1
  21. package/lib/module/ui/components/TextField/TextFieldOutlined.js.map +1 -1
  22. package/lib/module/ui/context/OxyContext.js +28 -37
  23. package/lib/module/ui/context/OxyContext.js.map +1 -1
  24. package/lib/module/ui/context/hooks/useSessionManagement.js +1 -1
  25. package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
  26. package/lib/module/ui/context/utils/errorHandlers.js +10 -0
  27. package/lib/module/ui/context/utils/errorHandlers.js.map +1 -1
  28. package/lib/module/ui/hooks/mutations/useAccountMutations.js +8 -10
  29. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  30. package/lib/module/ui/utils/avatarUtils.js +80 -5
  31. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  32. package/lib/typescript/crypto/keyManager.d.ts +9 -2
  33. package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
  34. package/lib/typescript/models/interfaces.d.ts +0 -4
  35. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  36. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  37. package/lib/typescript/ui/context/utils/errorHandlers.d.ts.map +1 -1
  38. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  39. package/lib/typescript/ui/utils/avatarUtils.d.ts +14 -0
  40. package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
  41. package/package.json +5 -4
  42. package/src/crypto/keyManager.ts +92 -16
  43. package/src/models/interfaces.ts +1 -5
  44. package/src/ui/components/TextField/TextFieldFlat.tsx +1 -1
  45. package/src/ui/components/TextField/TextFieldOutlined.tsx +1 -1
  46. package/src/ui/context/OxyContext.tsx +36 -34
  47. package/src/ui/context/hooks/useSessionManagement.ts +1 -1
  48. package/src/ui/context/utils/errorHandlers.ts +10 -0
  49. package/src/ui/hooks/mutations/useAccountMutations.ts +8 -6
  50. 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 { useUpdateProfile } from '../hooks/mutations/useAccountMutations';
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 (restored) {
214
- if (__DEV__) {
215
- logger('Identity restored from backup successfully');
216
- }
217
- } else {
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(async () => {
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
- if (logger) {
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
- if (logger) {
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
- if (__DEV__ && logger) {
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 isSynced = await storage.getItem('oxy_identity_synced');
431
- if (isSynced === 'false') {
432
- await syncIdentity();
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
- if (__DEV__ && logger) {
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
- logger('Session validation failed during init', validationError);
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 using mutation (handles query invalidation and accountStore update)
642
- await updateProfileMutation.mutateAsync({ avatar: file.id });
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, updateProfileMutation]);
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.js';
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
- console.log(`[${contextName}] Avatar visibility updated to public`);
28
+ // Visibility update is logged by the API
26
29
  } catch (visError: any) {
27
- // Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
28
- if (visError?.response?.status !== 404) {
29
- console.warn(`[${contextName}] Failed to update avatar visibility, continuing anyway:`, visError);
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
+