@oxyhq/auth 1.0.0

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 (119) hide show
  1. package/README.md +56 -0
  2. package/dist/cjs/WebOxyProvider.js +287 -0
  3. package/dist/cjs/hooks/mutations/index.js +23 -0
  4. package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
  5. package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
  6. package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
  7. package/dist/cjs/hooks/queries/index.js +35 -0
  8. package/dist/cjs/hooks/queries/queryKeys.js +82 -0
  9. package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
  10. package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
  11. package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
  12. package/dist/cjs/hooks/queryClient.js +110 -0
  13. package/dist/cjs/hooks/useAssets.js +225 -0
  14. package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
  15. package/dist/cjs/hooks/useFileFiltering.js +81 -0
  16. package/dist/cjs/hooks/useFollow.js +159 -0
  17. package/dist/cjs/hooks/useFollow.types.js +4 -0
  18. package/dist/cjs/hooks/useQueryClient.js +16 -0
  19. package/dist/cjs/hooks/useSessionSocket.js +215 -0
  20. package/dist/cjs/hooks/useWebSSO.js +146 -0
  21. package/dist/cjs/index.js +115 -0
  22. package/dist/cjs/stores/accountStore.js +226 -0
  23. package/dist/cjs/stores/assetStore.js +192 -0
  24. package/dist/cjs/stores/authStore.js +47 -0
  25. package/dist/cjs/stores/followStore.js +154 -0
  26. package/dist/cjs/utils/authHelpers.js +154 -0
  27. package/dist/cjs/utils/avatarUtils.js +77 -0
  28. package/dist/cjs/utils/errorHandlers.js +128 -0
  29. package/dist/cjs/utils/sessionHelpers.js +90 -0
  30. package/dist/cjs/utils/storageHelpers.js +147 -0
  31. package/dist/esm/WebOxyProvider.js +282 -0
  32. package/dist/esm/hooks/mutations/index.js +10 -0
  33. package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
  34. package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
  35. package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
  36. package/dist/esm/hooks/queries/index.js +14 -0
  37. package/dist/esm/hooks/queries/queryKeys.js +76 -0
  38. package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
  39. package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
  40. package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
  41. package/dist/esm/hooks/queryClient.js +104 -0
  42. package/dist/esm/hooks/useAssets.js +220 -0
  43. package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
  44. package/dist/esm/hooks/useFileFiltering.js +78 -0
  45. package/dist/esm/hooks/useFollow.js +154 -0
  46. package/dist/esm/hooks/useFollow.types.js +3 -0
  47. package/dist/esm/hooks/useQueryClient.js +12 -0
  48. package/dist/esm/hooks/useSessionSocket.js +209 -0
  49. package/dist/esm/hooks/useWebSSO.js +143 -0
  50. package/dist/esm/index.js +48 -0
  51. package/dist/esm/stores/accountStore.js +219 -0
  52. package/dist/esm/stores/assetStore.js +180 -0
  53. package/dist/esm/stores/authStore.js +44 -0
  54. package/dist/esm/stores/followStore.js +151 -0
  55. package/dist/esm/utils/authHelpers.js +145 -0
  56. package/dist/esm/utils/avatarUtils.js +72 -0
  57. package/dist/esm/utils/errorHandlers.js +121 -0
  58. package/dist/esm/utils/sessionHelpers.js +84 -0
  59. package/dist/esm/utils/storageHelpers.js +108 -0
  60. package/dist/types/WebOxyProvider.d.ts +97 -0
  61. package/dist/types/hooks/mutations/index.d.ts +8 -0
  62. package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
  63. package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
  64. package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
  65. package/dist/types/hooks/queries/index.d.ts +10 -0
  66. package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
  67. package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
  68. package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
  69. package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
  70. package/dist/types/hooks/queryClient.d.ts +18 -0
  71. package/dist/types/hooks/useAssets.d.ts +34 -0
  72. package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
  73. package/dist/types/hooks/useFileFiltering.d.ts +28 -0
  74. package/dist/types/hooks/useFollow.d.ts +61 -0
  75. package/dist/types/hooks/useFollow.types.d.ts +32 -0
  76. package/dist/types/hooks/useQueryClient.d.ts +6 -0
  77. package/dist/types/hooks/useSessionSocket.d.ts +13 -0
  78. package/dist/types/hooks/useWebSSO.d.ts +57 -0
  79. package/dist/types/index.d.ts +46 -0
  80. package/dist/types/stores/accountStore.d.ts +33 -0
  81. package/dist/types/stores/assetStore.d.ts +53 -0
  82. package/dist/types/stores/authStore.d.ts +16 -0
  83. package/dist/types/stores/followStore.d.ts +24 -0
  84. package/dist/types/utils/authHelpers.d.ts +98 -0
  85. package/dist/types/utils/avatarUtils.d.ts +33 -0
  86. package/dist/types/utils/errorHandlers.d.ts +34 -0
  87. package/dist/types/utils/sessionHelpers.d.ts +63 -0
  88. package/dist/types/utils/storageHelpers.d.ts +27 -0
  89. package/package.json +71 -0
  90. package/src/WebOxyProvider.tsx +372 -0
  91. package/src/global.d.ts +1 -0
  92. package/src/hooks/mutations/index.ts +25 -0
  93. package/src/hooks/mutations/mutationFactory.ts +215 -0
  94. package/src/hooks/mutations/useAccountMutations.ts +344 -0
  95. package/src/hooks/mutations/useServicesMutations.ts +164 -0
  96. package/src/hooks/queries/index.ts +36 -0
  97. package/src/hooks/queries/queryKeys.ts +88 -0
  98. package/src/hooks/queries/useAccountQueries.ts +152 -0
  99. package/src/hooks/queries/useSecurityQueries.ts +64 -0
  100. package/src/hooks/queries/useServicesQueries.ts +126 -0
  101. package/src/hooks/queryClient.ts +112 -0
  102. package/src/hooks/useAssets.ts +291 -0
  103. package/src/hooks/useFileDownloadUrl.ts +118 -0
  104. package/src/hooks/useFileFiltering.ts +115 -0
  105. package/src/hooks/useFollow.ts +175 -0
  106. package/src/hooks/useFollow.types.ts +33 -0
  107. package/src/hooks/useQueryClient.ts +17 -0
  108. package/src/hooks/useSessionSocket.ts +233 -0
  109. package/src/hooks/useWebSSO.ts +187 -0
  110. package/src/index.ts +144 -0
  111. package/src/stores/accountStore.ts +296 -0
  112. package/src/stores/assetStore.ts +281 -0
  113. package/src/stores/authStore.ts +63 -0
  114. package/src/stores/followStore.ts +181 -0
  115. package/src/utils/authHelpers.ts +183 -0
  116. package/src/utils/avatarUtils.ts +103 -0
  117. package/src/utils/errorHandlers.ts +194 -0
  118. package/src/utils/sessionHelpers.ts +151 -0
  119. package/src/utils/storageHelpers.ts +130 -0
@@ -0,0 +1,181 @@
1
+ import { create } from 'zustand';
2
+ import type { OxyServices } from '@oxyhq/core';
3
+
4
+ interface FollowState {
5
+ followingUsers: Record<string, boolean>;
6
+ loadingUsers: Record<string, boolean>;
7
+ fetchingUsers: Record<string, boolean>;
8
+ errors: Record<string, string | null>;
9
+ // Follower counts for each user
10
+ followerCounts: Record<string, number>;
11
+ followingCounts: Record<string, number>;
12
+ // Loading states for counts
13
+ loadingCounts: Record<string, boolean>;
14
+ setFollowingStatus: (userId: string, isFollowing: boolean) => void;
15
+ clearFollowError: (userId: string) => void;
16
+ resetFollowState: () => void;
17
+ fetchFollowStatus: (userId: string, oxyServices: OxyServices) => Promise<void>;
18
+ toggleFollowUser: (userId: string, oxyServices: OxyServices, isCurrentlyFollowing: boolean) => Promise<void>;
19
+ // New methods for follower counts
20
+ setFollowerCount: (userId: string, count: number) => void;
21
+ setFollowingCount: (userId: string, count: number) => void;
22
+ updateCountsFromFollowAction: (targetUserId: string, action: 'follow' | 'unfollow', counts: { followers: number; following: number }, currentUserId?: string) => void;
23
+ fetchUserCounts: (userId: string, oxyServices: OxyServices) => Promise<void>;
24
+ }
25
+
26
+ export const useFollowStore = create<FollowState>((set: any, get: any) => ({
27
+ followingUsers: {},
28
+ loadingUsers: {},
29
+ fetchingUsers: {},
30
+ errors: {},
31
+ followerCounts: {},
32
+ followingCounts: {},
33
+ loadingCounts: {},
34
+ setFollowingStatus: (userId: string, isFollowing: boolean) => set((state: FollowState) => ({
35
+ followingUsers: { ...state.followingUsers, [userId]: isFollowing },
36
+ errors: { ...state.errors, [userId]: null },
37
+ })),
38
+ clearFollowError: (userId: string) => set((state: FollowState) => ({
39
+ errors: { ...state.errors, [userId]: null },
40
+ })),
41
+ resetFollowState: () => set({
42
+ followingUsers: {},
43
+ loadingUsers: {},
44
+ fetchingUsers: {},
45
+ errors: {},
46
+ followerCounts: {},
47
+ followingCounts: {},
48
+ loadingCounts: {},
49
+ }),
50
+ fetchFollowStatus: async (userId: string, oxyServices: OxyServices) => {
51
+ set((state: FollowState) => ({
52
+ fetchingUsers: { ...state.fetchingUsers, [userId]: true },
53
+ errors: { ...state.errors, [userId]: null },
54
+ }));
55
+ try {
56
+ const response = await oxyServices.getFollowStatus(userId);
57
+ set((state: FollowState) => ({
58
+ followingUsers: { ...state.followingUsers, [userId]: response.isFollowing },
59
+ fetchingUsers: { ...state.fetchingUsers, [userId]: false },
60
+ errors: { ...state.errors, [userId]: null },
61
+ }));
62
+ } catch (error: any) {
63
+ set((state: FollowState) => ({
64
+ fetchingUsers: { ...state.fetchingUsers, [userId]: false },
65
+ errors: { ...state.errors, [userId]: error?.message || 'Failed to fetch follow status' },
66
+ }));
67
+ }
68
+ },
69
+ toggleFollowUser: async (userId: string, oxyServices: OxyServices, isCurrentlyFollowing: boolean) => {
70
+ set((state: FollowState) => ({
71
+ loadingUsers: { ...state.loadingUsers, [userId]: true },
72
+ errors: { ...state.errors, [userId]: null },
73
+ }));
74
+ try {
75
+ let response: any;
76
+ let newFollowState;
77
+ if (isCurrentlyFollowing) {
78
+ response = await oxyServices.unfollowUser(userId);
79
+ newFollowState = false;
80
+ } else {
81
+ response = await oxyServices.followUser(userId);
82
+ newFollowState = true;
83
+ }
84
+
85
+ // Update follow status
86
+ set((state: FollowState) => ({
87
+ followingUsers: { ...state.followingUsers, [userId]: newFollowState },
88
+ loadingUsers: { ...state.loadingUsers, [userId]: false },
89
+ errors: { ...state.errors, [userId]: null },
90
+ }));
91
+
92
+ // Update counts if the response includes them
93
+ // The API returns counts for both users:
94
+ // - followers: target user's follower count (the user being followed)
95
+ // - following: current user's following count (the user doing the following)
96
+ if (response && response.counts) {
97
+ const { counts } = response;
98
+
99
+ // Get current user ID from oxyServices
100
+ const currentUserId = oxyServices.getCurrentUserId();
101
+
102
+ set((state: FollowState) => {
103
+ const updates: any = {};
104
+
105
+ // Update target user's follower count (the user being followed)
106
+ updates.followerCounts = {
107
+ ...state.followerCounts,
108
+ [userId]: counts.followers
109
+ };
110
+
111
+ // Update current user's following count (the user doing the following)
112
+ if (currentUserId) {
113
+ updates.followingCounts = {
114
+ ...state.followingCounts,
115
+ [currentUserId]: counts.following
116
+ };
117
+ }
118
+
119
+ return updates;
120
+ });
121
+ }
122
+ } catch (error: any) {
123
+ set((state: FollowState) => ({
124
+ loadingUsers: { ...state.loadingUsers, [userId]: false },
125
+ errors: { ...state.errors, [userId]: error?.message || 'Failed to update follow status' },
126
+ }));
127
+ }
128
+ },
129
+ setFollowerCount: (userId: string, count: number) => set((state: FollowState) => ({
130
+ followerCounts: { ...state.followerCounts, [userId]: count },
131
+ })),
132
+ setFollowingCount: (userId: string, count: number) => set((state: FollowState) => ({
133
+ followingCounts: { ...state.followingCounts, [userId]: count },
134
+ })),
135
+ updateCountsFromFollowAction: (targetUserId: string, action: 'follow' | 'unfollow', counts: { followers: number; following: number }, currentUserId?: string) => {
136
+ set((state: FollowState) => {
137
+ const updates: any = {};
138
+
139
+ // Update target user's follower count (the user being followed)
140
+ updates.followerCounts = {
141
+ ...state.followerCounts,
142
+ [targetUserId]: counts.followers
143
+ };
144
+
145
+ // Update current user's following count (the user doing the following)
146
+ if (currentUserId) {
147
+ updates.followingCounts = {
148
+ ...state.followingCounts,
149
+ [currentUserId]: counts.following
150
+ };
151
+ }
152
+
153
+ return updates;
154
+ });
155
+ },
156
+ fetchUserCounts: async (userId: string, oxyServices: OxyServices) => {
157
+ set((state: FollowState) => ({
158
+ loadingCounts: { ...state.loadingCounts, [userId]: true },
159
+ }));
160
+ try {
161
+ const user = await oxyServices.getUserById(userId);
162
+ if (user && user._count) {
163
+ set((state: FollowState) => ({
164
+ followerCounts: {
165
+ ...state.followerCounts,
166
+ [userId]: user._count?.followers || 0
167
+ },
168
+ followingCounts: {
169
+ ...state.followingCounts,
170
+ [userId]: user._count?.following || 0
171
+ },
172
+ loadingCounts: { ...state.loadingCounts, [userId]: false },
173
+ }));
174
+ }
175
+ } catch (error: unknown) {
176
+ set((state: FollowState) => ({
177
+ loadingCounts: { ...state.loadingCounts, [userId]: false },
178
+ }));
179
+ }
180
+ },
181
+ }));
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Authentication helper utilities to reduce code duplication across hooks and utilities.
3
+ * These functions handle common token validation and authentication error patterns.
4
+ */
5
+
6
+ import type { OxyServices } from '@oxyhq/core';
7
+
8
+ /**
9
+ * Error thrown when session sync is required
10
+ */
11
+ export class SessionSyncRequiredError extends Error {
12
+ constructor(message = 'Session needs to be synced. Please try again.') {
13
+ super(message);
14
+ this.name = 'SessionSyncRequiredError';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Error thrown when authentication fails
20
+ */
21
+ export class AuthenticationFailedError extends Error {
22
+ constructor(message = 'Authentication failed. Please sign in again.') {
23
+ super(message);
24
+ this.name = 'AuthenticationFailedError';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Ensures a valid token exists before making authenticated API calls.
30
+ * If no valid token exists and an active session ID is available,
31
+ * attempts to refresh the token using the session.
32
+ *
33
+ * @param oxyServices - The OxyServices instance
34
+ * @param activeSessionId - The active session ID (if available)
35
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
36
+ * @throws {Error} If token refresh fails for other reasons
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * // In a mutation or query function:
41
+ * await ensureValidToken(oxyServices, activeSessionId);
42
+ * return await oxyServices.updateProfile(updates);
43
+ * ```
44
+ */
45
+ export async function ensureValidToken(
46
+ oxyServices: OxyServices,
47
+ activeSessionId: string | null | undefined
48
+ ): Promise<void> {
49
+ if (oxyServices.hasValidToken() || !activeSessionId) {
50
+ return;
51
+ }
52
+
53
+ try {
54
+ await oxyServices.getTokenBySession(activeSessionId);
55
+ } catch (tokenError) {
56
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
57
+
58
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
59
+ throw new SessionSyncRequiredError();
60
+ }
61
+
62
+ throw tokenError;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Options for handling API authentication errors
68
+ */
69
+ export interface HandleApiErrorOptions {
70
+ /** Optional callback to attempt session sync and retry */
71
+ syncSession?: () => Promise<unknown>;
72
+ /** The active session ID for retry attempts */
73
+ activeSessionId?: string | null;
74
+ /** The OxyServices instance for retry attempts */
75
+ oxyServices?: OxyServices;
76
+ }
77
+
78
+ /**
79
+ * Checks if an error is an authentication error (401 or auth-related message)
80
+ *
81
+ * @param error - The error to check
82
+ * @returns True if the error is an authentication error
83
+ */
84
+ export function isAuthenticationError(error: unknown): boolean {
85
+ if (!error || typeof error !== 'object') {
86
+ return false;
87
+ }
88
+
89
+ const errorObj = error as { message?: string; status?: number; response?: { status?: number } };
90
+ const errorMessage = errorObj.message || '';
91
+ const status = errorObj.status || errorObj.response?.status;
92
+
93
+ return (
94
+ status === 401 ||
95
+ errorMessage.includes('Authentication required') ||
96
+ errorMessage.includes('Invalid or missing authorization header')
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Wraps an API call with authentication error handling.
102
+ * If an authentication error occurs, it can optionally attempt to sync the session and retry.
103
+ *
104
+ * @param apiCall - The API call function to execute
105
+ * @param options - Optional error handling configuration
106
+ * @returns The result of the API call
107
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
108
+ * @throws {Error} If the API call fails for non-auth reasons
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Simple usage:
113
+ * const result = await withAuthErrorHandling(
114
+ * () => oxyServices.updateProfile(updates)
115
+ * );
116
+ *
117
+ * // With retry on auth failure:
118
+ * const result = await withAuthErrorHandling(
119
+ * () => oxyServices.updateProfile(updates),
120
+ * { syncSession, activeSessionId, oxyServices }
121
+ * );
122
+ * ```
123
+ */
124
+ export async function withAuthErrorHandling<T>(
125
+ apiCall: () => Promise<T>,
126
+ options?: HandleApiErrorOptions
127
+ ): Promise<T> {
128
+ try {
129
+ return await apiCall();
130
+ } catch (error) {
131
+ if (!isAuthenticationError(error)) {
132
+ throw error;
133
+ }
134
+
135
+ // If we have sync capabilities, try to recover
136
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
137
+ try {
138
+ await options.syncSession();
139
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
140
+ // Retry the API call after refreshing token
141
+ return await apiCall();
142
+ } catch {
143
+ throw new AuthenticationFailedError();
144
+ }
145
+ }
146
+
147
+ throw new AuthenticationFailedError();
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Combines token validation and auth error handling for a complete authenticated API call.
153
+ * This is the recommended helper for most authenticated API operations.
154
+ *
155
+ * @param oxyServices - The OxyServices instance
156
+ * @param activeSessionId - The active session ID
157
+ * @param apiCall - The API call function to execute
158
+ * @param syncSession - Optional callback to sync session on auth failure
159
+ * @returns The result of the API call
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * return await authenticatedApiCall(
164
+ * oxyServices,
165
+ * activeSessionId,
166
+ * () => oxyServices.updateProfile(updates)
167
+ * );
168
+ * ```
169
+ */
170
+ export async function authenticatedApiCall<T>(
171
+ oxyServices: OxyServices,
172
+ activeSessionId: string | null | undefined,
173
+ apiCall: () => Promise<T>,
174
+ syncSession?: () => Promise<unknown>
175
+ ): Promise<T> {
176
+ await ensureValidToken(oxyServices, activeSessionId);
177
+
178
+ return withAuthErrorHandling(apiCall, {
179
+ syncSession,
180
+ activeSessionId,
181
+ oxyServices,
182
+ });
183
+ }
@@ -0,0 +1,103 @@
1
+ import type { OxyServices } from '@oxyhq/core';
2
+ import type { User } from '@oxyhq/core';
3
+ import { useAccountStore } from '../stores/accountStore';
4
+ import { useAuthStore } from '../stores/authStore';
5
+ import { QueryClient } from '@tanstack/react-query';
6
+ import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
7
+ import { authenticatedApiCall } from './authHelpers';
8
+
9
+ /**
10
+ * Updates file visibility to public for avatar use.
11
+ * Handles errors gracefully, only logging non-404 errors.
12
+ *
13
+ * @param fileId - The file ID to update visibility for
14
+ * @param oxyServices - OxyServices instance
15
+ * @param contextName - Optional context name for logging
16
+ * @returns Promise that resolves when visibility is updated (or skipped)
17
+ */
18
+ export async function updateAvatarVisibility(
19
+ fileId: string | undefined,
20
+ oxyServices: OxyServices,
21
+ contextName: string = 'AvatarUtils'
22
+ ): Promise<void> {
23
+ // Skip if temporary asset ID or no file ID
24
+ if (!fileId || fileId.startsWith('temp-')) {
25
+ return;
26
+ }
27
+
28
+ try {
29
+ await oxyServices.assetUpdateVisibility(fileId, 'public');
30
+ // Visibility update is logged by the API
31
+ } catch (visError: any) {
32
+ // Silently handle errors - 404 means asset doesn't exist yet (which is OK)
33
+ // Other errors are logged by the API, so no need to log here
34
+ // Function continues gracefully regardless of visibility update success
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Refreshes avatar in accountStore with cache-busted URL to force image reload.
40
+ *
41
+ * @param sessionId - The session ID for the account to update
42
+ * @param avatarFileId - The new avatar file ID
43
+ * @param oxyServices - OxyServices instance to generate download URL
44
+ */
45
+ export function refreshAvatarInStore(
46
+ sessionId: string,
47
+ avatarFileId: string,
48
+ oxyServices: OxyServices
49
+ ): void {
50
+ const { updateAccount } = useAccountStore.getState();
51
+ const cacheBustedUrl = oxyServices.getFileDownloadUrl(avatarFileId, 'thumb') + `?t=${Date.now()}`;
52
+ updateAccount(sessionId, {
53
+ avatar: avatarFileId,
54
+ avatarUrl: cacheBustedUrl,
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Updates user profile with avatar and handles all side effects (query invalidation, accountStore update).
60
+ * This function can be used from within OxyContext provider without requiring useOxy hook.
61
+ *
62
+ * @param updates - Profile updates including avatar
63
+ * @param oxyServices - OxyServices instance
64
+ * @param activeSessionId - Active session ID
65
+ * @param queryClient - TanStack Query client
66
+ * @param syncSession - Optional function to sync session/refresh token when auth errors occur
67
+ * @returns Promise that resolves with updated user data
68
+ */
69
+ export async function updateProfileWithAvatar(
70
+ updates: Partial<User>,
71
+ oxyServices: OxyServices,
72
+ activeSessionId: string | null,
73
+ queryClient: QueryClient,
74
+ syncSession?: () => Promise<User>
75
+ ): Promise<User> {
76
+ const data = await authenticatedApiCall<User>(
77
+ oxyServices,
78
+ activeSessionId,
79
+ () => oxyServices.updateProfile(updates),
80
+ syncSession
81
+ );
82
+
83
+ // Update cache with server response
84
+ queryClient.setQueryData(queryKeys.accounts.current(), data);
85
+ if (activeSessionId) {
86
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
87
+ }
88
+
89
+ // Update authStore so frontend components see the changes immediately
90
+ useAuthStore.getState().setUser(data);
91
+
92
+ // If avatar was updated, refresh accountStore with cache-busted URL
93
+ if (updates.avatar && activeSessionId) {
94
+ refreshAvatarInStore(activeSessionId, updates.avatar, oxyServices);
95
+ }
96
+
97
+ // Invalidate all related queries to refresh everywhere
98
+ invalidateUserQueries(queryClient);
99
+ invalidateAccountQueries(queryClient);
100
+
101
+ return data;
102
+ }
103
+
@@ -0,0 +1,194 @@
1
+ import type { ApiError } from '@oxyhq/core';
2
+
3
+ type ErrorWithMessage = {
4
+ message?: string;
5
+ };
6
+
7
+ type ErrorWithResponse = {
8
+ response?: {
9
+ status?: number;
10
+ data?: {
11
+ message?: string;
12
+ error?: string;
13
+ };
14
+ };
15
+ };
16
+
17
+ export interface HandleAuthErrorOptions {
18
+ defaultMessage: string;
19
+ code: string;
20
+ status?: number;
21
+ onError?: (error: ApiError) => void;
22
+ setAuthError?: (message: string) => void;
23
+ logger?: (message: string, error?: unknown) => void;
24
+ }
25
+
26
+ const DEFAULT_INVALID_SESSION_MESSAGES = [
27
+ 'Invalid or expired session',
28
+ 'Session is invalid',
29
+ 'Session not found',
30
+ 'Session expired',
31
+ ];
32
+
33
+ const isObject = (value: unknown): value is Record<string, unknown> =>
34
+ typeof value === 'object' && value !== null;
35
+
36
+ const getResponseStatus = (error: unknown): number | undefined => {
37
+ if (!isObject(error)) return undefined;
38
+ const response = (error as ErrorWithResponse).response;
39
+ return response?.status;
40
+ };
41
+
42
+ /**
43
+ * Determine whether the error represents an invalid session condition.
44
+ * This centralizes 401 detection across different fetch clients.
45
+ */
46
+ export const isInvalidSessionError = (error: unknown): boolean => {
47
+ const status = getResponseStatus(error);
48
+ if (status === 401) {
49
+ return true;
50
+ }
51
+
52
+ if (!isObject(error)) {
53
+ return false;
54
+ }
55
+
56
+ // Check error.status directly (HttpService sets this)
57
+ if ((error as any).status === 401) {
58
+ return true;
59
+ }
60
+
61
+ const normalizedMessage = extractErrorMessage(error)?.toLowerCase();
62
+ if (!normalizedMessage) {
63
+ return false;
64
+ }
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
+
71
+ return DEFAULT_INVALID_SESSION_MESSAGES.some((msg) =>
72
+ normalizedMessage.includes(msg.toLowerCase()),
73
+ );
74
+ };
75
+
76
+ /**
77
+ * Determine whether the error represents a timeout or network error.
78
+ * These are expected when the device is offline or has poor connectivity.
79
+ */
80
+ export const isTimeoutOrNetworkError = (error: unknown): boolean => {
81
+ if (!isObject(error) && !(error instanceof Error)) {
82
+ return false;
83
+ }
84
+
85
+ const message = extractErrorMessage(error, '').toLowerCase();
86
+ const errorCode = (error as any).code;
87
+
88
+ // Check for timeout/cancelled messages
89
+ if (
90
+ message.includes('timeout') ||
91
+ message.includes('cancelled') ||
92
+ message.includes('econnaborted') ||
93
+ message.includes('aborted') ||
94
+ message.includes('request timeout or cancelled')
95
+ ) {
96
+ return true;
97
+ }
98
+
99
+ // Check for timeout/network error codes
100
+ if (errorCode === 'TIMEOUT' || errorCode === 'NETWORK_ERROR' || errorCode === 'ECONNABORTED') {
101
+ return true;
102
+ }
103
+
104
+ // Check for AbortError
105
+ if (error instanceof Error && error.name === 'AbortError') {
106
+ return true;
107
+ }
108
+
109
+ // Check for network-related TypeErrors
110
+ if (error instanceof TypeError) {
111
+ const typeErrorMessage = error.message.toLowerCase();
112
+ if (
113
+ typeErrorMessage.includes('fetch') ||
114
+ typeErrorMessage.includes('network') ||
115
+ typeErrorMessage.includes('failed to fetch')
116
+ ) {
117
+ return true;
118
+ }
119
+ }
120
+
121
+ return false;
122
+ };
123
+
124
+ /**
125
+ * Extract a consistent error message from unknown error shapes.
126
+ *
127
+ * @param error - The unknown error payload
128
+ * @param fallbackMessage - Message to return when no concrete message is available
129
+ */
130
+ export const extractErrorMessage = (
131
+ error: unknown,
132
+ fallbackMessage = 'Unexpected error',
133
+ ): string => {
134
+ if (typeof error === 'string' && error.trim().length > 0) {
135
+ return error;
136
+ }
137
+
138
+ if (!isObject(error)) {
139
+ return fallbackMessage;
140
+ }
141
+
142
+ const withMessage = error as ErrorWithMessage;
143
+ if (withMessage.message && withMessage.message.trim().length > 0) {
144
+ return withMessage.message;
145
+ }
146
+
147
+ const withResponse = error as ErrorWithResponse;
148
+ const responseMessage =
149
+ withResponse.response?.data?.message ?? withResponse.response?.data?.error;
150
+
151
+ if (typeof responseMessage === 'string' && responseMessage.trim().length > 0) {
152
+ return responseMessage;
153
+ }
154
+
155
+ return fallbackMessage;
156
+ };
157
+
158
+ /**
159
+ * Centralized error handler for auth-related operations.
160
+ *
161
+ * @param error - Unknown error object
162
+ * @param options - Error handling configuration
163
+ * @returns Resolved error message
164
+ */
165
+ export const handleAuthError = (
166
+ error: unknown,
167
+ {
168
+ defaultMessage,
169
+ code,
170
+ status,
171
+ onError,
172
+ setAuthError,
173
+ logger,
174
+ }: HandleAuthErrorOptions,
175
+ ): string => {
176
+ const resolvedStatus = status ?? getResponseStatus(error) ?? (isInvalidSessionError(error) ? 401 : 500);
177
+ const message = extractErrorMessage(error, defaultMessage);
178
+
179
+ if (logger) {
180
+ logger(message, error);
181
+ }
182
+
183
+ setAuthError?.(message);
184
+
185
+ onError?.({
186
+ message,
187
+ code,
188
+ status: resolvedStatus,
189
+ });
190
+
191
+ return message;
192
+ };
193
+
194
+