@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,147 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getStorageKeys = exports.STORAGE_KEY_PREFIX = exports.createPlatformStorage = exports.isReactNative = void 0;
37
+ /**
38
+ * Create an in-memory storage implementation used as a safe fallback.
39
+ */
40
+ const MEMORY_STORAGE = () => {
41
+ const store = new Map();
42
+ return {
43
+ async getItem(key) {
44
+ return store.has(key) ? store.get(key) : null;
45
+ },
46
+ async setItem(key, value) {
47
+ store.set(key, value);
48
+ },
49
+ async removeItem(key) {
50
+ store.delete(key);
51
+ },
52
+ async clear() {
53
+ store.clear();
54
+ },
55
+ };
56
+ };
57
+ /**
58
+ * Create a web storage implementation backed by `localStorage`.
59
+ * Falls back to in-memory storage when unavailable.
60
+ */
61
+ const createWebStorage = () => {
62
+ if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
63
+ return MEMORY_STORAGE();
64
+ }
65
+ return {
66
+ async getItem(key) {
67
+ try {
68
+ return window.localStorage.getItem(key);
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ },
74
+ async setItem(key, value) {
75
+ try {
76
+ window.localStorage.setItem(key, value);
77
+ }
78
+ catch {
79
+ // Ignore quota or access issues for now.
80
+ }
81
+ },
82
+ async removeItem(key) {
83
+ try {
84
+ window.localStorage.removeItem(key);
85
+ }
86
+ catch {
87
+ // Ignore failures.
88
+ }
89
+ },
90
+ async clear() {
91
+ try {
92
+ window.localStorage.clear();
93
+ }
94
+ catch {
95
+ // Ignore failures.
96
+ }
97
+ },
98
+ };
99
+ };
100
+ let asyncStorageInstance = null;
101
+ /**
102
+ * Lazily import React Native AsyncStorage implementation.
103
+ */
104
+ const createNativeStorage = async () => {
105
+ if (asyncStorageInstance) {
106
+ return asyncStorageInstance;
107
+ }
108
+ try {
109
+ const asyncStorageModule = await Promise.resolve().then(() => __importStar(require('@react-native-async-storage/async-storage')));
110
+ asyncStorageInstance = asyncStorageModule.default;
111
+ return asyncStorageInstance;
112
+ }
113
+ catch (error) {
114
+ if (__DEV__) {
115
+ console.error('Failed to import AsyncStorage:', error);
116
+ }
117
+ throw new Error('AsyncStorage is required in React Native environment');
118
+ }
119
+ };
120
+ /**
121
+ * Detect whether the current runtime is React Native.
122
+ */
123
+ const isReactNative = () => typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
124
+ exports.isReactNative = isReactNative;
125
+ /**
126
+ * Create a platform-appropriate storage implementation.
127
+ * Defaults to in-memory storage when no platform storage is available.
128
+ */
129
+ const createPlatformStorage = async () => {
130
+ if ((0, exports.isReactNative)()) {
131
+ return createNativeStorage();
132
+ }
133
+ return createWebStorage();
134
+ };
135
+ exports.createPlatformStorage = createPlatformStorage;
136
+ exports.STORAGE_KEY_PREFIX = 'oxy_session';
137
+ /**
138
+ * Produce strongly typed storage key names for the supplied prefix.
139
+ *
140
+ * @param prefix - Storage key prefix
141
+ */
142
+ const getStorageKeys = (prefix = exports.STORAGE_KEY_PREFIX) => ({
143
+ activeSessionId: `${prefix}_active_session_id`,
144
+ sessionIds: `${prefix}_session_ids`,
145
+ language: `${prefix}_language`,
146
+ });
147
+ exports.getStorageKeys = getStorageKeys;
@@ -0,0 +1,282 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @oxyhq/auth — Web Authentication Provider
4
+ *
5
+ * Clean implementation with ZERO React Native dependencies.
6
+ * Provides FedCM, popup, and redirect authentication methods.
7
+ * Uses centralized AuthManager for token and session management.
8
+ */
9
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
10
+ import { OxyServices, CrossDomainAuth, createAuthManager, } from '@oxyhq/core';
11
+ import { QueryClientProvider } from '@tanstack/react-query';
12
+ import { createQueryClient } from './hooks/queryClient';
13
+ const WebOxyContext = createContext(null);
14
+ /**
15
+ * Web-only Oxy Provider
16
+ *
17
+ * Provides authentication context for pure web applications (React, Next.js, Vite).
18
+ * Supports FedCM, popup, and redirect authentication methods.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * import { WebOxyProvider, useAuth } from '@oxyhq/auth';
23
+ *
24
+ * function App() {
25
+ * return (
26
+ * <WebOxyProvider baseURL="https://api.oxy.so">
27
+ * <YourApp />
28
+ * </WebOxyProvider>
29
+ * );
30
+ * }
31
+ * ```
32
+ */
33
+ export function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onError, preferredAuthMethod = 'auto', skipAutoCheck = false, }) {
34
+ const [oxyServices] = useState(() => new OxyServices({ baseURL, authWebUrl }));
35
+ const [crossDomainAuth] = useState(() => new CrossDomainAuth(oxyServices));
36
+ const [authManager] = useState(() => createAuthManager(oxyServices, { autoRefresh: true }));
37
+ const [queryClient] = useState(() => createQueryClient());
38
+ // Auth state
39
+ const [user, setUser] = useState(null);
40
+ const [isLoading, setIsLoading] = useState(!skipAutoCheck);
41
+ const [error, setError] = useState(null);
42
+ const [activeSessionId, setActiveSessionId] = useState(null);
43
+ const [sessions, setSessions] = useState([]);
44
+ const isAuthenticated = !!user;
45
+ const handleAuthSuccess = useCallback(async (session, method = 'credentials') => {
46
+ await authManager.handleAuthSuccess(session, method);
47
+ // Set active session
48
+ if (session.sessionId) {
49
+ setActiveSessionId(session.sessionId);
50
+ }
51
+ // Fetch full user profile
52
+ try {
53
+ const fullUser = await oxyServices.getCurrentUser();
54
+ if (fullUser) {
55
+ setUser(fullUser);
56
+ }
57
+ else {
58
+ setUser(session.user);
59
+ }
60
+ }
61
+ catch {
62
+ setUser(session.user);
63
+ }
64
+ setError(null);
65
+ setIsLoading(false);
66
+ }, [authManager, oxyServices]);
67
+ const handleAuthError = useCallback((err) => {
68
+ const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
69
+ setError(errorMessage);
70
+ setIsLoading(false);
71
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
72
+ }, [onError]);
73
+ // Initialize
74
+ useEffect(() => {
75
+ if (skipAutoCheck)
76
+ return;
77
+ let mounted = true;
78
+ const initAuth = async () => {
79
+ try {
80
+ const callbackSession = crossDomainAuth.handleRedirectCallback();
81
+ if (callbackSession && mounted) {
82
+ await handleAuthSuccess(callbackSession, 'redirect');
83
+ return;
84
+ }
85
+ const restoredUser = await authManager.initialize();
86
+ if (restoredUser && mounted) {
87
+ try {
88
+ const currentUser = await oxyServices.getCurrentUser();
89
+ if (mounted && currentUser) {
90
+ setUser(currentUser);
91
+ setIsLoading(false);
92
+ return;
93
+ }
94
+ }
95
+ catch {
96
+ await authManager.signOut();
97
+ }
98
+ }
99
+ try {
100
+ const session = await crossDomainAuth.silentSignIn();
101
+ if (mounted && session?.user) {
102
+ await handleAuthSuccess(session, 'fedcm');
103
+ return;
104
+ }
105
+ }
106
+ catch {
107
+ // Silent sign-in failed
108
+ }
109
+ if (mounted)
110
+ setIsLoading(false);
111
+ }
112
+ catch {
113
+ if (mounted)
114
+ setIsLoading(false);
115
+ }
116
+ };
117
+ initAuth();
118
+ return () => { mounted = false; };
119
+ }, [oxyServices, crossDomainAuth, authManager, skipAutoCheck, handleAuthSuccess]);
120
+ useEffect(() => {
121
+ onAuthStateChange?.(user);
122
+ }, [user, onAuthStateChange]);
123
+ const signIn = useCallback(async () => {
124
+ setError(null);
125
+ setIsLoading(true);
126
+ let selectedMethod = 'popup';
127
+ try {
128
+ const session = await crossDomainAuth.signIn({
129
+ method: preferredAuthMethod,
130
+ onMethodSelected: (method) => {
131
+ selectedMethod = method;
132
+ },
133
+ });
134
+ if (session) {
135
+ await handleAuthSuccess(session, selectedMethod);
136
+ }
137
+ else {
138
+ setIsLoading(false);
139
+ }
140
+ }
141
+ catch (err) {
142
+ handleAuthError(err);
143
+ }
144
+ }, [crossDomainAuth, preferredAuthMethod, handleAuthSuccess, handleAuthError]);
145
+ const signInWithFedCM = useCallback(async () => {
146
+ setError(null);
147
+ setIsLoading(true);
148
+ try {
149
+ const session = await crossDomainAuth.signInWithFedCM();
150
+ await handleAuthSuccess(session, 'fedcm');
151
+ }
152
+ catch (err) {
153
+ handleAuthError(err);
154
+ }
155
+ }, [crossDomainAuth, handleAuthSuccess, handleAuthError]);
156
+ const signInWithPopup = useCallback(async () => {
157
+ setError(null);
158
+ setIsLoading(true);
159
+ try {
160
+ const session = await crossDomainAuth.signInWithPopup();
161
+ await handleAuthSuccess(session, 'popup');
162
+ }
163
+ catch (err) {
164
+ handleAuthError(err);
165
+ }
166
+ }, [crossDomainAuth, handleAuthSuccess, handleAuthError]);
167
+ const signInWithRedirect = useCallback(() => {
168
+ setError(null);
169
+ crossDomainAuth.signInWithRedirect({
170
+ redirectUri: typeof window !== 'undefined' ? window.location.href : undefined,
171
+ });
172
+ }, [crossDomainAuth]);
173
+ const isFedCMSupported = useCallback(() => {
174
+ return crossDomainAuth.isFedCMSupported();
175
+ }, [crossDomainAuth]);
176
+ const signOut = useCallback(async () => {
177
+ setError(null);
178
+ try {
179
+ await authManager.signOut();
180
+ setUser(null);
181
+ setActiveSessionId(null);
182
+ setSessions([]);
183
+ }
184
+ catch (err) {
185
+ const errorMessage = err instanceof Error ? err.message : 'Sign out failed';
186
+ setError(errorMessage);
187
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
188
+ }
189
+ }, [authManager, onError]);
190
+ const switchSession = useCallback(async (sessionId) => {
191
+ try {
192
+ const result = await oxyServices.getTokenBySession(sessionId);
193
+ if (result) {
194
+ setActiveSessionId(sessionId);
195
+ const currentUser = await oxyServices.getCurrentUser();
196
+ if (currentUser)
197
+ setUser(currentUser);
198
+ }
199
+ }
200
+ catch (err) {
201
+ handleAuthError(err);
202
+ }
203
+ }, [oxyServices, handleAuthError]);
204
+ const clearSessionState = useCallback(async () => {
205
+ await authManager.signOut();
206
+ setUser(null);
207
+ setActiveSessionId(null);
208
+ setSessions([]);
209
+ }, [authManager]);
210
+ useEffect(() => {
211
+ return () => { authManager.destroy(); };
212
+ }, [authManager]);
213
+ const contextValue = useMemo(() => ({
214
+ user,
215
+ isAuthenticated,
216
+ isLoading,
217
+ error,
218
+ activeSessionId,
219
+ sessions,
220
+ oxyServices,
221
+ crossDomainAuth,
222
+ authManager,
223
+ signIn,
224
+ signInWithFedCM,
225
+ signInWithPopup,
226
+ signInWithRedirect,
227
+ signOut,
228
+ isFedCMSupported,
229
+ switchSession,
230
+ clearSessionState,
231
+ }), [
232
+ user, isAuthenticated, isLoading, error, activeSessionId, sessions,
233
+ oxyServices, crossDomainAuth, authManager,
234
+ signIn, signInWithFedCM, signInWithPopup, signInWithRedirect,
235
+ signOut, isFedCMSupported, switchSession, clearSessionState,
236
+ ]);
237
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(WebOxyContext.Provider, { value: contextValue, children: children }) }));
238
+ }
239
+ /**
240
+ * Hook to access the full Web Oxy context.
241
+ */
242
+ export function useWebOxy() {
243
+ const context = useContext(WebOxyContext);
244
+ if (!context) {
245
+ throw new Error('useWebOxy must be used within WebOxyProvider');
246
+ }
247
+ return context;
248
+ }
249
+ /**
250
+ * Hook for authentication in web apps.
251
+ *
252
+ * @example
253
+ * ```tsx
254
+ * function LoginPage() {
255
+ * const { user, isAuthenticated, signIn, signOut } = useAuth();
256
+ * if (!isAuthenticated) return <button onClick={signIn}>Sign in</button>;
257
+ * return <button onClick={signOut}>Sign out</button>;
258
+ * }
259
+ * ```
260
+ */
261
+ export function useAuth() {
262
+ const ctx = useWebOxy();
263
+ return {
264
+ user: ctx.user,
265
+ isAuthenticated: ctx.isAuthenticated,
266
+ isLoading: ctx.isLoading,
267
+ isReady: !ctx.isLoading,
268
+ error: ctx.error,
269
+ activeSessionId: ctx.activeSessionId,
270
+ sessions: ctx.sessions,
271
+ signIn: ctx.signIn,
272
+ signInWithFedCM: ctx.signInWithFedCM,
273
+ signInWithPopup: ctx.signInWithPopup,
274
+ signInWithRedirect: ctx.signInWithRedirect,
275
+ signOut: ctx.signOut,
276
+ isFedCMSupported: ctx.isFedCMSupported,
277
+ switchSession: ctx.switchSession,
278
+ oxyServices: ctx.oxyServices,
279
+ authManager: ctx.authManager,
280
+ };
281
+ }
282
+ export default WebOxyProvider;
@@ -0,0 +1,10 @@
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
+ // Account mutation hooks
8
+ export { useUpdateProfile, useUploadAvatar, useUpdateAccountSettings, useUpdatePrivacySettings, useUploadFile, } from './useAccountMutations';
9
+ // Service mutation hooks (sessions, devices)
10
+ export { useSwitchSession, useLogoutSession, useLogoutAll, useUpdateDeviceName, useRemoveDevice, } from './useServicesMutations';
@@ -0,0 +1,122 @@
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
+ import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../queries/queryKeys';
12
+ import { toast } from 'sonner';
13
+ import { useAuthStore } from '../../stores/authStore';
14
+ /**
15
+ * Creates a standard profile mutation with optimistic updates
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const updateProfile = createProfileMutation({
20
+ * mutationFn: (updates) => oxyServices.updateProfile(updates),
21
+ * optimisticUpdate: (user, updates) => updates,
22
+ * errorMessage: 'Failed to update profile',
23
+ * });
24
+ * ```
25
+ */
26
+ export function createProfileMutation(config, queryClient, activeSessionId) {
27
+ const { mutationFn, cancelQueryKeys = [], optimisticUpdate, errorMessage = 'Operation failed', successMessage, updateAuthStore = true, invalidateUserQueries: shouldInvalidateUserQueries = true, invalidateAccountQueries: shouldInvalidateAccountQueries = true, onSuccess: customOnSuccess, } = config;
28
+ return {
29
+ mutationFn,
30
+ onMutate: async (variables) => {
31
+ // Cancel queries that might conflict
32
+ await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
33
+ for (const key of cancelQueryKeys) {
34
+ await queryClient.cancelQueries({ queryKey: key });
35
+ }
36
+ // Snapshot previous user data
37
+ const previousUser = queryClient.getQueryData(queryKeys.accounts.current());
38
+ // Apply optimistic update if provided
39
+ if (previousUser && optimisticUpdate) {
40
+ const updates = optimisticUpdate(previousUser, variables);
41
+ const optimisticUser = { ...previousUser, ...updates };
42
+ queryClient.setQueryData(queryKeys.accounts.current(), optimisticUser);
43
+ if (activeSessionId) {
44
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), optimisticUser);
45
+ }
46
+ }
47
+ return { previousUser };
48
+ },
49
+ onError: (error, _variables, context) => {
50
+ // Rollback optimistic update
51
+ if (context?.previousUser) {
52
+ queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
53
+ if (activeSessionId) {
54
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), context.previousUser);
55
+ }
56
+ }
57
+ // Show error toast
58
+ const message = typeof errorMessage === 'function'
59
+ ? errorMessage(error)
60
+ : (error instanceof Error ? error.message : errorMessage);
61
+ toast.error(message);
62
+ },
63
+ onSuccess: (data, variables) => {
64
+ // Update cache with server response
65
+ queryClient.setQueryData(queryKeys.accounts.current(), data);
66
+ if (activeSessionId) {
67
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
68
+ }
69
+ // Update authStore for immediate UI updates
70
+ if (updateAuthStore) {
71
+ useAuthStore.getState().setUser(data);
72
+ }
73
+ // Invalidate related queries
74
+ if (shouldInvalidateUserQueries) {
75
+ invalidateUserQueries(queryClient);
76
+ }
77
+ if (shouldInvalidateAccountQueries) {
78
+ invalidateAccountQueries(queryClient);
79
+ }
80
+ // Show success toast if configured
81
+ if (successMessage) {
82
+ toast.success(successMessage);
83
+ }
84
+ // Call custom onSuccess handler
85
+ if (customOnSuccess) {
86
+ customOnSuccess(data, variables, queryClient);
87
+ }
88
+ },
89
+ };
90
+ }
91
+ /**
92
+ * Creates a generic mutation with optimistic updates
93
+ */
94
+ export function createGenericMutation(config, queryClient) {
95
+ const { mutationFn, queryKey, optimisticData, errorMessage = 'Operation failed', successMessage, invalidateQueries = [], } = config;
96
+ return {
97
+ mutationFn,
98
+ onMutate: async (variables) => {
99
+ await queryClient.cancelQueries({ queryKey });
100
+ const previous = queryClient.getQueryData(queryKey);
101
+ if (optimisticData) {
102
+ queryClient.setQueryData(queryKey, optimisticData(previous, variables));
103
+ }
104
+ return { previous };
105
+ },
106
+ onError: (error, _variables, context) => {
107
+ if (context?.previous !== undefined) {
108
+ queryClient.setQueryData(queryKey, context.previous);
109
+ }
110
+ toast.error(error instanceof Error ? error.message : errorMessage);
111
+ },
112
+ onSuccess: (data) => {
113
+ queryClient.setQueryData(queryKey, data);
114
+ for (const key of invalidateQueries) {
115
+ queryClient.invalidateQueries({ queryKey: key });
116
+ }
117
+ if (successMessage) {
118
+ toast.success(successMessage);
119
+ }
120
+ },
121
+ };
122
+ }