@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.
- package/README.md +56 -0
- package/dist/cjs/WebOxyProvider.js +287 -0
- package/dist/cjs/hooks/mutations/index.js +23 -0
- package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
- package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
- package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
- package/dist/cjs/hooks/queries/index.js +35 -0
- package/dist/cjs/hooks/queries/queryKeys.js +82 -0
- package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
- package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
- package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
- package/dist/cjs/hooks/queryClient.js +110 -0
- package/dist/cjs/hooks/useAssets.js +225 -0
- package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
- package/dist/cjs/hooks/useFileFiltering.js +81 -0
- package/dist/cjs/hooks/useFollow.js +159 -0
- package/dist/cjs/hooks/useFollow.types.js +4 -0
- package/dist/cjs/hooks/useQueryClient.js +16 -0
- package/dist/cjs/hooks/useSessionSocket.js +215 -0
- package/dist/cjs/hooks/useWebSSO.js +146 -0
- package/dist/cjs/index.js +115 -0
- package/dist/cjs/stores/accountStore.js +226 -0
- package/dist/cjs/stores/assetStore.js +192 -0
- package/dist/cjs/stores/authStore.js +47 -0
- package/dist/cjs/stores/followStore.js +154 -0
- package/dist/cjs/utils/authHelpers.js +154 -0
- package/dist/cjs/utils/avatarUtils.js +77 -0
- package/dist/cjs/utils/errorHandlers.js +128 -0
- package/dist/cjs/utils/sessionHelpers.js +90 -0
- package/dist/cjs/utils/storageHelpers.js +147 -0
- package/dist/esm/WebOxyProvider.js +282 -0
- package/dist/esm/hooks/mutations/index.js +10 -0
- package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
- package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
- package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
- package/dist/esm/hooks/queries/index.js +14 -0
- package/dist/esm/hooks/queries/queryKeys.js +76 -0
- package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
- package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
- package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
- package/dist/esm/hooks/queryClient.js +104 -0
- package/dist/esm/hooks/useAssets.js +220 -0
- package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
- package/dist/esm/hooks/useFileFiltering.js +78 -0
- package/dist/esm/hooks/useFollow.js +154 -0
- package/dist/esm/hooks/useFollow.types.js +3 -0
- package/dist/esm/hooks/useQueryClient.js +12 -0
- package/dist/esm/hooks/useSessionSocket.js +209 -0
- package/dist/esm/hooks/useWebSSO.js +143 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/stores/accountStore.js +219 -0
- package/dist/esm/stores/assetStore.js +180 -0
- package/dist/esm/stores/authStore.js +44 -0
- package/dist/esm/stores/followStore.js +151 -0
- package/dist/esm/utils/authHelpers.js +145 -0
- package/dist/esm/utils/avatarUtils.js +72 -0
- package/dist/esm/utils/errorHandlers.js +121 -0
- package/dist/esm/utils/sessionHelpers.js +84 -0
- package/dist/esm/utils/storageHelpers.js +108 -0
- package/dist/types/WebOxyProvider.d.ts +97 -0
- package/dist/types/hooks/mutations/index.d.ts +8 -0
- package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
- package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
- package/dist/types/hooks/queries/index.d.ts +10 -0
- package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
- package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
- package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
- package/dist/types/hooks/queryClient.d.ts +18 -0
- package/dist/types/hooks/useAssets.d.ts +34 -0
- package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
- package/dist/types/hooks/useFileFiltering.d.ts +28 -0
- package/dist/types/hooks/useFollow.d.ts +61 -0
- package/dist/types/hooks/useFollow.types.d.ts +32 -0
- package/dist/types/hooks/useQueryClient.d.ts +6 -0
- package/dist/types/hooks/useSessionSocket.d.ts +13 -0
- package/dist/types/hooks/useWebSSO.d.ts +57 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/types/stores/accountStore.d.ts +33 -0
- package/dist/types/stores/assetStore.d.ts +53 -0
- package/dist/types/stores/authStore.d.ts +16 -0
- package/dist/types/stores/followStore.d.ts +24 -0
- package/dist/types/utils/authHelpers.d.ts +98 -0
- package/dist/types/utils/avatarUtils.d.ts +33 -0
- package/dist/types/utils/errorHandlers.d.ts +34 -0
- package/dist/types/utils/sessionHelpers.d.ts +63 -0
- package/dist/types/utils/storageHelpers.d.ts +27 -0
- package/package.json +71 -0
- package/src/WebOxyProvider.tsx +372 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/mutations/index.ts +25 -0
- package/src/hooks/mutations/mutationFactory.ts +215 -0
- package/src/hooks/mutations/useAccountMutations.ts +344 -0
- package/src/hooks/mutations/useServicesMutations.ts +164 -0
- package/src/hooks/queries/index.ts +36 -0
- package/src/hooks/queries/queryKeys.ts +88 -0
- package/src/hooks/queries/useAccountQueries.ts +152 -0
- package/src/hooks/queries/useSecurityQueries.ts +64 -0
- package/src/hooks/queries/useServicesQueries.ts +126 -0
- package/src/hooks/queryClient.ts +112 -0
- package/src/hooks/useAssets.ts +291 -0
- package/src/hooks/useFileDownloadUrl.ts +118 -0
- package/src/hooks/useFileFiltering.ts +115 -0
- package/src/hooks/useFollow.ts +175 -0
- package/src/hooks/useFollow.types.ts +33 -0
- package/src/hooks/useQueryClient.ts +17 -0
- package/src/hooks/useSessionSocket.ts +233 -0
- package/src/hooks/useWebSSO.ts +187 -0
- package/src/index.ts +144 -0
- package/src/stores/accountStore.ts +296 -0
- package/src/stores/assetStore.ts +281 -0
- package/src/stores/authStore.ts +63 -0
- package/src/stores/followStore.ts +181 -0
- package/src/utils/authHelpers.ts +183 -0
- package/src/utils/avatarUtils.ts +103 -0
- package/src/utils/errorHandlers.ts +194 -0
- package/src/utils/sessionHelpers.ts +151 -0
- 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
|
+
}
|