@replyke/react-js
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
102 lines • 4.96 kB
JavaScript
import { useCallback, useState } from "react";
import { useProject, useReplykeDispatch, useReplykeSelector, setTokens, setInitialized, selectAccessToken, requestNewAccessTokenThunk, } from "@replyke/core";
const BASE_URL = "https://api.replyke.com/v7";
/**
* Web-only hook for OAuth sign-in and identity linking.
* Uses window.location for redirect-based OAuth flow.
*
* Usage (sign-in):
* const { initiateOAuth, handleOAuthCallback } = useOAuthSignIn();
* await initiateOAuth("google", "https://myapp.com/auth/callback");
*
* Usage (link provider to current user):
* const { linkOAuthProvider, handleOAuthCallback } = useOAuthSignIn();
* await linkOAuthProvider("github", "https://myapp.com/settings");
*
* On the callback page (component mount):
* useEffect(() => { handleOAuthCallback(); }, []);
*/
function useOAuthSignIn() {
const { projectId } = useProject();
const dispatch = useReplykeDispatch();
const accessToken = useReplykeSelector(selectAccessToken);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Shared helper for both /authorize and /link endpoints
const startOAuthFlow = useCallback(async (endpoint, provider, redirectAfterAuth) => {
if (!projectId) {
setError("No projectId available.");
return;
}
if (endpoint === "link" && !accessToken) {
setError("Must be authenticated to link an OAuth provider.");
return;
}
setIsLoading(true);
setError(null);
try {
const redirect = redirectAfterAuth || window.location.href;
// /authorize is unauthenticated, /link requires the access token
const headers = {
"Content-Type": "application/json",
};
if (endpoint === "link") {
headers["Authorization"] = `Bearer ${accessToken}`;
}
const response = await fetch(`${BASE_URL}/${projectId}/oauth/${endpoint}`, {
method: "POST",
headers,
body: JSON.stringify({ provider, redirectAfterAuth: redirect }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "Failed to initiate OAuth");
}
const data = await response.json();
// Redirect browser to provider's authorization page.
// isLoading intentionally stays true since we're navigating away.
window.location.href = data.authorizationUrl;
}
catch (err) {
setError(err.message);
setIsLoading(false);
}
}, [projectId, accessToken]);
const initiateOAuth = useCallback(({ provider, redirectAfterAuth }) => startOAuthFlow("authorize", provider, redirectAfterAuth), [startOAuthFlow]);
const linkOAuthProvider = useCallback(({ provider, redirectAfterAuth }) => startOAuthFlow("link", provider, redirectAfterAuth), [startOAuthFlow]);
const handleOAuthCallback = useCallback(() => {
// Tokens arrive in the URL fragment (#accessToken=...&refreshToken=...)
// Errors arrive in query params (?error=...&error_description=...)
const hash = window.location.hash.substring(1); // Remove leading #
const fragmentParams = new URLSearchParams(hash);
const queryParams = new URLSearchParams(window.location.search);
const fragmentAccessToken = fragmentParams.get("accessToken");
const refreshToken = fragmentParams.get("refreshToken");
const oauthError = queryParams.get("error");
if (oauthError) {
setError(queryParams.get("error_description") || oauthError);
// Clean URL
window.history.replaceState({}, "", window.location.pathname);
return false;
}
if (fragmentAccessToken && refreshToken) {
// Store tokens in Redux. The AccountManager (via useAccountSync)
// will detect the new tokens and persist them to localStorage.
dispatch(setTokens({ accessToken: fragmentAccessToken, refreshToken }));
dispatch(setInitialized(true));
// Fetch user profile so useAccountSync can persist the account.
// The thunk reads the just-set refresh token from Redux, calls the
// server, and dispatches setUser + setUserInUserSlice on success.
if (projectId) {
dispatch(requestNewAccessTokenThunk({ projectId }));
}
// Clean URL (remove fragment with tokens)
window.history.replaceState({}, "", window.location.pathname);
return true;
}
return false;
}, [dispatch, projectId]);
return { initiateOAuth, linkOAuthProvider, handleOAuthCallback, isLoading, error };
}
export default useOAuthSignIn;
//# sourceMappingURL=useOAuthSignIn.js.map