@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,372 @@
1
+ /**
2
+ * @oxyhq/auth — Web Authentication Provider
3
+ *
4
+ * Clean implementation with ZERO React Native dependencies.
5
+ * Provides FedCM, popup, and redirect authentication methods.
6
+ * Uses centralized AuthManager for token and session management.
7
+ */
8
+
9
+ import {
10
+ createContext,
11
+ useCallback,
12
+ useContext,
13
+ useEffect,
14
+ useMemo,
15
+ useState,
16
+ type ReactNode,
17
+ } from 'react';
18
+ import {
19
+ OxyServices,
20
+ CrossDomainAuth,
21
+ AuthManager,
22
+ createAuthManager,
23
+ } from '@oxyhq/core';
24
+ import type {
25
+ User,
26
+ SessionLoginResponse,
27
+ ClientSession,
28
+ } from '@oxyhq/core';
29
+ import { QueryClientProvider } from '@tanstack/react-query';
30
+ import { createQueryClient } from './hooks/queryClient';
31
+
32
+ export interface WebAuthState {
33
+ user: User | null;
34
+ isAuthenticated: boolean;
35
+ isLoading: boolean;
36
+ error: string | null;
37
+ activeSessionId: string | null;
38
+ sessions: ClientSession[];
39
+ }
40
+
41
+ export interface WebAuthActions {
42
+ signIn: () => Promise<void>;
43
+ signInWithFedCM: () => Promise<void>;
44
+ signInWithPopup: () => Promise<void>;
45
+ signInWithRedirect: () => void;
46
+ signOut: () => Promise<void>;
47
+ isFedCMSupported: () => boolean;
48
+ switchSession: (sessionId: string) => Promise<void>;
49
+ clearSessionState: () => Promise<void>;
50
+ }
51
+
52
+ export interface WebOxyContextValue extends WebAuthState, WebAuthActions {
53
+ oxyServices: OxyServices;
54
+ crossDomainAuth: CrossDomainAuth;
55
+ authManager: AuthManager;
56
+ }
57
+
58
+ const WebOxyContext = createContext<WebOxyContextValue | null>(null);
59
+
60
+ export interface WebOxyProviderProps {
61
+ children: ReactNode;
62
+ baseURL: string;
63
+ authWebUrl?: string;
64
+ onAuthStateChange?: (user: User | null) => void;
65
+ onError?: (error: Error) => void;
66
+ preferredAuthMethod?: 'auto' | 'fedcm' | 'popup' | 'redirect';
67
+ skipAutoCheck?: boolean;
68
+ }
69
+
70
+ /**
71
+ * Web-only Oxy Provider
72
+ *
73
+ * Provides authentication context for pure web applications (React, Next.js, Vite).
74
+ * Supports FedCM, popup, and redirect authentication methods.
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * import { WebOxyProvider, useAuth } from '@oxyhq/auth';
79
+ *
80
+ * function App() {
81
+ * return (
82
+ * <WebOxyProvider baseURL="https://api.oxy.so">
83
+ * <YourApp />
84
+ * </WebOxyProvider>
85
+ * );
86
+ * }
87
+ * ```
88
+ */
89
+ export function WebOxyProvider({
90
+ children,
91
+ baseURL,
92
+ authWebUrl,
93
+ onAuthStateChange,
94
+ onError,
95
+ preferredAuthMethod = 'auto',
96
+ skipAutoCheck = false,
97
+ }: WebOxyProviderProps) {
98
+ const [oxyServices] = useState(() => new OxyServices({ baseURL, authWebUrl }));
99
+ const [crossDomainAuth] = useState(() => new CrossDomainAuth(oxyServices));
100
+ const [authManager] = useState(() => createAuthManager(oxyServices, { autoRefresh: true }));
101
+ const [queryClient] = useState(() => createQueryClient());
102
+
103
+ // Auth state
104
+ const [user, setUser] = useState<User | null>(null);
105
+ const [isLoading, setIsLoading] = useState(!skipAutoCheck);
106
+ const [error, setError] = useState<string | null>(null);
107
+ const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
108
+ const [sessions, setSessions] = useState<ClientSession[]>([]);
109
+
110
+ const isAuthenticated = !!user;
111
+
112
+ const handleAuthSuccess = useCallback(async (
113
+ session: SessionLoginResponse,
114
+ method: 'fedcm' | 'popup' | 'redirect' | 'credentials' = 'credentials'
115
+ ) => {
116
+ await authManager.handleAuthSuccess(session, method);
117
+
118
+ // Set active session
119
+ if (session.sessionId) {
120
+ setActiveSessionId(session.sessionId);
121
+ }
122
+
123
+ // Fetch full user profile
124
+ try {
125
+ const fullUser = await oxyServices.getCurrentUser();
126
+ if (fullUser) {
127
+ setUser(fullUser);
128
+ } else {
129
+ setUser(session.user as User);
130
+ }
131
+ } catch {
132
+ setUser(session.user as User);
133
+ }
134
+
135
+ setError(null);
136
+ setIsLoading(false);
137
+ }, [authManager, oxyServices]);
138
+
139
+ const handleAuthError = useCallback((err: unknown) => {
140
+ const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
141
+ setError(errorMessage);
142
+ setIsLoading(false);
143
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
144
+ }, [onError]);
145
+
146
+ // Initialize
147
+ useEffect(() => {
148
+ if (skipAutoCheck) return;
149
+
150
+ let mounted = true;
151
+
152
+ const initAuth = async () => {
153
+ try {
154
+ const callbackSession = crossDomainAuth.handleRedirectCallback();
155
+ if (callbackSession && mounted) {
156
+ await handleAuthSuccess(callbackSession, 'redirect');
157
+ return;
158
+ }
159
+
160
+ const restoredUser = await authManager.initialize();
161
+ if (restoredUser && mounted) {
162
+ try {
163
+ const currentUser = await oxyServices.getCurrentUser();
164
+ if (mounted && currentUser) {
165
+ setUser(currentUser);
166
+ setIsLoading(false);
167
+ return;
168
+ }
169
+ } catch {
170
+ await authManager.signOut();
171
+ }
172
+ }
173
+
174
+ try {
175
+ const session = await crossDomainAuth.silentSignIn();
176
+ if (mounted && session?.user) {
177
+ await handleAuthSuccess(session, 'fedcm');
178
+ return;
179
+ }
180
+ } catch {
181
+ // Silent sign-in failed
182
+ }
183
+
184
+ if (mounted) setIsLoading(false);
185
+ } catch {
186
+ if (mounted) setIsLoading(false);
187
+ }
188
+ };
189
+
190
+ initAuth();
191
+ return () => { mounted = false; };
192
+ }, [oxyServices, crossDomainAuth, authManager, skipAutoCheck, handleAuthSuccess]);
193
+
194
+ useEffect(() => {
195
+ onAuthStateChange?.(user);
196
+ }, [user, onAuthStateChange]);
197
+
198
+ const signIn = useCallback(async () => {
199
+ setError(null);
200
+ setIsLoading(true);
201
+
202
+ let selectedMethod: 'fedcm' | 'popup' | 'redirect' = 'popup';
203
+
204
+ try {
205
+ const session = await crossDomainAuth.signIn({
206
+ method: preferredAuthMethod,
207
+ onMethodSelected: (method) => {
208
+ selectedMethod = method as 'fedcm' | 'popup' | 'redirect';
209
+ },
210
+ });
211
+
212
+ if (session) {
213
+ await handleAuthSuccess(session, selectedMethod);
214
+ } else {
215
+ setIsLoading(false);
216
+ }
217
+ } catch (err) {
218
+ handleAuthError(err);
219
+ }
220
+ }, [crossDomainAuth, preferredAuthMethod, handleAuthSuccess, handleAuthError]);
221
+
222
+ const signInWithFedCM = useCallback(async () => {
223
+ setError(null);
224
+ setIsLoading(true);
225
+ try {
226
+ const session = await crossDomainAuth.signInWithFedCM();
227
+ await handleAuthSuccess(session, 'fedcm');
228
+ } catch (err) {
229
+ handleAuthError(err);
230
+ }
231
+ }, [crossDomainAuth, handleAuthSuccess, handleAuthError]);
232
+
233
+ const signInWithPopup = useCallback(async () => {
234
+ setError(null);
235
+ setIsLoading(true);
236
+ try {
237
+ const session = await crossDomainAuth.signInWithPopup();
238
+ await handleAuthSuccess(session, 'popup');
239
+ } catch (err) {
240
+ handleAuthError(err);
241
+ }
242
+ }, [crossDomainAuth, handleAuthSuccess, handleAuthError]);
243
+
244
+ const signInWithRedirect = useCallback(() => {
245
+ setError(null);
246
+ crossDomainAuth.signInWithRedirect({
247
+ redirectUri: typeof window !== 'undefined' ? window.location.href : undefined,
248
+ });
249
+ }, [crossDomainAuth]);
250
+
251
+ const isFedCMSupported = useCallback(() => {
252
+ return crossDomainAuth.isFedCMSupported();
253
+ }, [crossDomainAuth]);
254
+
255
+ const signOut = useCallback(async () => {
256
+ setError(null);
257
+ try {
258
+ await authManager.signOut();
259
+ setUser(null);
260
+ setActiveSessionId(null);
261
+ setSessions([]);
262
+ } catch (err) {
263
+ const errorMessage = err instanceof Error ? err.message : 'Sign out failed';
264
+ setError(errorMessage);
265
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
266
+ }
267
+ }, [authManager, onError]);
268
+
269
+ const switchSession = useCallback(async (sessionId: string) => {
270
+ try {
271
+ const result = await oxyServices.getTokenBySession(sessionId);
272
+ if (result) {
273
+ setActiveSessionId(sessionId);
274
+ const currentUser = await oxyServices.getCurrentUser();
275
+ if (currentUser) setUser(currentUser);
276
+ }
277
+ } catch (err) {
278
+ handleAuthError(err);
279
+ }
280
+ }, [oxyServices, handleAuthError]);
281
+
282
+ const clearSessionState = useCallback(async () => {
283
+ await authManager.signOut();
284
+ setUser(null);
285
+ setActiveSessionId(null);
286
+ setSessions([]);
287
+ }, [authManager]);
288
+
289
+ useEffect(() => {
290
+ return () => { authManager.destroy(); };
291
+ }, [authManager]);
292
+
293
+ const contextValue = useMemo<WebOxyContextValue>(() => ({
294
+ user,
295
+ isAuthenticated,
296
+ isLoading,
297
+ error,
298
+ activeSessionId,
299
+ sessions,
300
+ oxyServices,
301
+ crossDomainAuth,
302
+ authManager,
303
+ signIn,
304
+ signInWithFedCM,
305
+ signInWithPopup,
306
+ signInWithRedirect,
307
+ signOut,
308
+ isFedCMSupported,
309
+ switchSession,
310
+ clearSessionState,
311
+ }), [
312
+ user, isAuthenticated, isLoading, error, activeSessionId, sessions,
313
+ oxyServices, crossDomainAuth, authManager,
314
+ signIn, signInWithFedCM, signInWithPopup, signInWithRedirect,
315
+ signOut, isFedCMSupported, switchSession, clearSessionState,
316
+ ]);
317
+
318
+ return (
319
+ <QueryClientProvider client={queryClient}>
320
+ <WebOxyContext.Provider value={contextValue}>
321
+ {children}
322
+ </WebOxyContext.Provider>
323
+ </QueryClientProvider>
324
+ );
325
+ }
326
+
327
+ /**
328
+ * Hook to access the full Web Oxy context.
329
+ */
330
+ export function useWebOxy(): WebOxyContextValue {
331
+ const context = useContext(WebOxyContext);
332
+ if (!context) {
333
+ throw new Error('useWebOxy must be used within WebOxyProvider');
334
+ }
335
+ return context;
336
+ }
337
+
338
+ /**
339
+ * Hook for authentication in web apps.
340
+ *
341
+ * @example
342
+ * ```tsx
343
+ * function LoginPage() {
344
+ * const { user, isAuthenticated, signIn, signOut } = useAuth();
345
+ * if (!isAuthenticated) return <button onClick={signIn}>Sign in</button>;
346
+ * return <button onClick={signOut}>Sign out</button>;
347
+ * }
348
+ * ```
349
+ */
350
+ export function useAuth() {
351
+ const ctx = useWebOxy();
352
+ return {
353
+ user: ctx.user,
354
+ isAuthenticated: ctx.isAuthenticated,
355
+ isLoading: ctx.isLoading,
356
+ isReady: !ctx.isLoading,
357
+ error: ctx.error,
358
+ activeSessionId: ctx.activeSessionId,
359
+ sessions: ctx.sessions,
360
+ signIn: ctx.signIn,
361
+ signInWithFedCM: ctx.signInWithFedCM,
362
+ signInWithPopup: ctx.signInWithPopup,
363
+ signInWithRedirect: ctx.signInWithRedirect,
364
+ signOut: ctx.signOut,
365
+ isFedCMSupported: ctx.isFedCMSupported,
366
+ switchSession: ctx.switchSession,
367
+ oxyServices: ctx.oxyServices,
368
+ authManager: ctx.authManager,
369
+ };
370
+ }
371
+
372
+ export default WebOxyProvider;
@@ -0,0 +1 @@
1
+ declare const __DEV__: boolean;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Mutation Hooks
3
+ *
4
+ * TanStack Query mutation hooks for updating Oxy services data.
5
+ * All mutations handle authentication, error handling, and query invalidation.
6
+ */
7
+
8
+ // Account mutation hooks
9
+ export {
10
+ useUpdateProfile,
11
+ useUploadAvatar,
12
+ useUpdateAccountSettings,
13
+ useUpdatePrivacySettings,
14
+ useUploadFile,
15
+ } from './useAccountMutations';
16
+
17
+ // Service mutation hooks (sessions, devices)
18
+ export {
19
+ useSwitchSession,
20
+ useLogoutSession,
21
+ useLogoutAll,
22
+ useUpdateDeviceName,
23
+ useRemoveDevice,
24
+ } from './useServicesMutations';
25
+
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Mutation Factory - Creates standardized mutations with optimistic updates
3
+ *
4
+ * This factory reduces boilerplate code for mutations that follow the common pattern:
5
+ * 1. Cancel outgoing queries
6
+ * 2. Snapshot previous data
7
+ * 3. Apply optimistic update
8
+ * 4. On error: rollback and show toast
9
+ * 5. On success: update cache, stores, and invalidate queries
10
+ */
11
+
12
+ import { QueryClient, UseMutationOptions } from '@tanstack/react-query';
13
+ import type { User } from '@oxyhq/core';
14
+ import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../queries/queryKeys';
15
+ import { toast } from 'sonner';
16
+ import { useAuthStore } from '../../stores/authStore';
17
+
18
+ /**
19
+ * Configuration for creating a standard profile mutation
20
+ */
21
+ export interface ProfileMutationConfig<TData, TVariables> {
22
+ /** The mutation function that makes the API call */
23
+ mutationFn: (variables: TVariables) => Promise<TData>;
24
+ /** Query keys to cancel before mutation */
25
+ cancelQueryKeys?: unknown[][];
26
+ /** Function to apply optimistic update to the user data */
27
+ optimisticUpdate?: (previousUser: User, variables: TVariables) => Partial<User>;
28
+ /** Error message to show on failure */
29
+ errorMessage?: string | ((error: Error) => string);
30
+ /** Success message to show (optional) */
31
+ successMessage?: string;
32
+ /** Whether to update authStore on success (default: true) */
33
+ updateAuthStore?: boolean;
34
+ /** Whether to invalidate user queries on success (default: true) */
35
+ invalidateUserQueries?: boolean;
36
+ /** Whether to invalidate account queries on success (default: true) */
37
+ invalidateAccountQueries?: boolean;
38
+ /** Custom onSuccess handler */
39
+ onSuccess?: (data: TData, variables: TVariables, queryClient: QueryClient) => void;
40
+ }
41
+
42
+ /**
43
+ * Creates a standard profile mutation with optimistic updates
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const updateProfile = createProfileMutation({
48
+ * mutationFn: (updates) => oxyServices.updateProfile(updates),
49
+ * optimisticUpdate: (user, updates) => updates,
50
+ * errorMessage: 'Failed to update profile',
51
+ * });
52
+ * ```
53
+ */
54
+ export function createProfileMutation<TVariables>(
55
+ config: ProfileMutationConfig<User, TVariables>,
56
+ queryClient: QueryClient,
57
+ activeSessionId: string | null
58
+ ): UseMutationOptions<User, Error, TVariables, { previousUser?: User }> {
59
+ const {
60
+ mutationFn,
61
+ cancelQueryKeys = [],
62
+ optimisticUpdate,
63
+ errorMessage = 'Operation failed',
64
+ successMessage,
65
+ updateAuthStore = true,
66
+ invalidateUserQueries: shouldInvalidateUserQueries = true,
67
+ invalidateAccountQueries: shouldInvalidateAccountQueries = true,
68
+ onSuccess: customOnSuccess,
69
+ } = config;
70
+
71
+ return {
72
+ mutationFn,
73
+
74
+ onMutate: async (variables) => {
75
+ // Cancel queries that might conflict
76
+ await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
77
+ for (const key of cancelQueryKeys) {
78
+ await queryClient.cancelQueries({ queryKey: key });
79
+ }
80
+
81
+ // Snapshot previous user data
82
+ const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
83
+
84
+ // Apply optimistic update if provided
85
+ if (previousUser && optimisticUpdate) {
86
+ const updates = optimisticUpdate(previousUser, variables);
87
+ const optimisticUser = { ...previousUser, ...updates };
88
+
89
+ queryClient.setQueryData<User>(queryKeys.accounts.current(), optimisticUser);
90
+
91
+ if (activeSessionId) {
92
+ queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), optimisticUser);
93
+ }
94
+ }
95
+
96
+ return { previousUser };
97
+ },
98
+
99
+ onError: (error, _variables, context) => {
100
+ // Rollback optimistic update
101
+ if (context?.previousUser) {
102
+ queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
103
+ if (activeSessionId) {
104
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), context.previousUser);
105
+ }
106
+ }
107
+
108
+ // Show error toast
109
+ const message = typeof errorMessage === 'function'
110
+ ? errorMessage(error)
111
+ : (error instanceof Error ? error.message : errorMessage);
112
+ toast.error(message);
113
+ },
114
+
115
+ onSuccess: (data, variables) => {
116
+ // Update cache with server response
117
+ queryClient.setQueryData(queryKeys.accounts.current(), data);
118
+ if (activeSessionId) {
119
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
120
+ }
121
+
122
+ // Update authStore for immediate UI updates
123
+ if (updateAuthStore) {
124
+ useAuthStore.getState().setUser(data);
125
+ }
126
+
127
+ // Invalidate related queries
128
+ if (shouldInvalidateUserQueries) {
129
+ invalidateUserQueries(queryClient);
130
+ }
131
+ if (shouldInvalidateAccountQueries) {
132
+ invalidateAccountQueries(queryClient);
133
+ }
134
+
135
+ // Show success toast if configured
136
+ if (successMessage) {
137
+ toast.success(successMessage);
138
+ }
139
+
140
+ // Call custom onSuccess handler
141
+ if (customOnSuccess) {
142
+ customOnSuccess(data, variables, queryClient);
143
+ }
144
+ },
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Configuration for creating a generic mutation (non-profile)
150
+ */
151
+ export interface GenericMutationConfig<TData, TVariables, TContext> {
152
+ /** The mutation function */
153
+ mutationFn: (variables: TVariables) => Promise<TData>;
154
+ /** Query key for optimistic data */
155
+ queryKey: unknown[];
156
+ /** Function to create optimistic data */
157
+ optimisticData?: (previous: TData | undefined, variables: TVariables) => TData;
158
+ /** Error message */
159
+ errorMessage?: string;
160
+ /** Success message */
161
+ successMessage?: string;
162
+ /** Additional queries to invalidate on success */
163
+ invalidateQueries?: unknown[][];
164
+ }
165
+
166
+ /**
167
+ * Creates a generic mutation with optimistic updates
168
+ */
169
+ export function createGenericMutation<TData, TVariables>(
170
+ config: GenericMutationConfig<TData, TVariables, { previous?: TData }>,
171
+ queryClient: QueryClient
172
+ ): UseMutationOptions<TData, Error, TVariables, { previous?: TData }> {
173
+ const {
174
+ mutationFn,
175
+ queryKey,
176
+ optimisticData,
177
+ errorMessage = 'Operation failed',
178
+ successMessage,
179
+ invalidateQueries = [],
180
+ } = config;
181
+
182
+ return {
183
+ mutationFn,
184
+
185
+ onMutate: async (variables) => {
186
+ await queryClient.cancelQueries({ queryKey });
187
+ const previous = queryClient.getQueryData<TData>(queryKey);
188
+
189
+ if (optimisticData) {
190
+ queryClient.setQueryData<TData>(queryKey, optimisticData(previous, variables));
191
+ }
192
+
193
+ return { previous };
194
+ },
195
+
196
+ onError: (error, _variables, context) => {
197
+ if (context?.previous !== undefined) {
198
+ queryClient.setQueryData(queryKey, context.previous);
199
+ }
200
+ toast.error(error instanceof Error ? error.message : errorMessage);
201
+ },
202
+
203
+ onSuccess: (data) => {
204
+ queryClient.setQueryData(queryKey, data);
205
+
206
+ for (const key of invalidateQueries) {
207
+ queryClient.invalidateQueries({ queryKey: key });
208
+ }
209
+
210
+ if (successMessage) {
211
+ toast.success(successMessage);
212
+ }
213
+ },
214
+ };
215
+ }