feat: add token refresh lifecycle callbacks to CalProvider (#25203)
Add three new optional callbacks to CalProvider that enable users to react during the access token refresh lifecycle: - onTokenRefreshStart: Called when token refresh begins - onTokenRefreshSuccess: Called when token refresh succeeds - onTokenRefreshError: Called when token refresh fails with error message These callbacks are invoked in both token refresh locations: 1. Response interceptor (when API returns 498 status) 2. Initial access token validation The callbacks are properly passed through the component hierarchy: CalProvider -> BaseCalProvider -> useOAuthFlow hook Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
This commit is contained in:
co-authored by
Devin AI
Rajiv Sahal
parent
e625308881
commit
bf39489899
@@ -42,6 +42,9 @@ export type CalProviderProps = {
|
||||
options: { refreshUrl?: string; apiUrl: string; readingDirection?: "ltr" | "rtl" };
|
||||
autoUpdateTimezone?: boolean;
|
||||
onTimezoneChange?: () => void;
|
||||
onTokenRefreshStart?: () => void;
|
||||
onTokenRefreshSuccess?: () => void;
|
||||
onTokenRefreshError?: (error: string) => void;
|
||||
version?: API_VERSIONS_ENUM;
|
||||
organizationId?: number;
|
||||
isEmbed?: boolean;
|
||||
@@ -57,6 +60,9 @@ export function BaseCalProvider({
|
||||
language = EN,
|
||||
organizationId,
|
||||
onTimezoneChange,
|
||||
onTokenRefreshStart,
|
||||
onTokenRefreshSuccess,
|
||||
onTokenRefreshError,
|
||||
isEmbed,
|
||||
}: CalProviderProps) {
|
||||
const [error, setError] = useState<string>("");
|
||||
@@ -100,6 +106,9 @@ export function BaseCalProvider({
|
||||
onSuccess: () => {
|
||||
setError("");
|
||||
},
|
||||
onTokenRefreshStart,
|
||||
onTokenRefreshSuccess,
|
||||
onTokenRefreshError,
|
||||
clientId,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ const queryClient = new QueryClient();
|
||||
* @param {string} [options.refreshUrl] - The url point to your refresh endpoint. - Optional, required if accessToken is provided.
|
||||
* @param {boolean} [autoUpdateTimezone=true] - Whether to automatically update the timezone. - Optional
|
||||
* @param {function} props.onTimezoneChange - The callback function for timezone change. - Optional
|
||||
* @param {function} props.onTokenRefreshStart - The callback function called when token refresh starts. - Optional
|
||||
* @param {function} props.onTokenRefreshSuccess - The callback function called when token refresh succeeds. - Optional
|
||||
* @param {function} props.onTokenRefreshError - The callback function called when token refresh fails. - Optional
|
||||
* @param {ReactNode} props.children - The child components. - Optional
|
||||
* @returns {JSX.Element} The rendered CalProvider component.
|
||||
*/
|
||||
@@ -35,6 +38,9 @@ export function CalProvider({
|
||||
labels,
|
||||
language = "en",
|
||||
onTimezoneChange,
|
||||
onTokenRefreshStart,
|
||||
onTokenRefreshSuccess,
|
||||
onTokenRefreshError,
|
||||
version = VERSION_2024_06_14,
|
||||
organizationId,
|
||||
isEmbed = false,
|
||||
@@ -59,6 +65,9 @@ export function CalProvider({
|
||||
isEmbed={isEmbed}
|
||||
autoUpdateTimezone={autoUpdateTimezone}
|
||||
onTimezoneChange={onTimezoneChange}
|
||||
onTokenRefreshStart={onTokenRefreshStart}
|
||||
onTokenRefreshSuccess={onTokenRefreshSuccess}
|
||||
onTokenRefreshError={onTokenRefreshError}
|
||||
clientId={clientId}
|
||||
accessToken={accessToken}
|
||||
options={options}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AxiosError, AxiosRequestConfig } from "axios";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
|
||||
import { debounce } from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import usePrevious from "react-use/lib/usePrevious";
|
||||
@@ -13,11 +13,23 @@ export interface useOAuthProps {
|
||||
refreshUrl?: string;
|
||||
onError?: (error: string) => void;
|
||||
onSuccess?: () => void;
|
||||
onTokenRefreshStart?: () => void;
|
||||
onTokenRefreshSuccess?: () => void;
|
||||
onTokenRefreshError?: (error: string) => void;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
const debouncedRefresh = debounce(http.refreshTokens, 10000, { leading: true, trailing: false });
|
||||
export const useOAuthFlow = ({ accessToken, refreshUrl, clientId, onError, onSuccess }: useOAuthProps) => {
|
||||
export const useOAuthFlow = ({
|
||||
accessToken,
|
||||
refreshUrl,
|
||||
clientId,
|
||||
onError,
|
||||
onSuccess,
|
||||
onTokenRefreshStart,
|
||||
onTokenRefreshSuccess,
|
||||
onTokenRefreshError,
|
||||
}: useOAuthProps) => {
|
||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||
const [clientAccessToken, setClientAccessToken] = useState<string>("");
|
||||
const prevAccessToken = usePrevious(accessToken);
|
||||
@@ -28,16 +40,20 @@ export const useOAuthFlow = ({ accessToken, refreshUrl, clientId, onError, onSuc
|
||||
const originalRequest = err.config as AxiosRequestConfig;
|
||||
if (refreshUrl && err.response?.status === 498 && !isRefreshing) {
|
||||
setIsRefreshing(true);
|
||||
onTokenRefreshStart?.();
|
||||
const refreshedToken = await debouncedRefresh(refreshUrl);
|
||||
if (refreshedToken) {
|
||||
setClientAccessToken(refreshedToken);
|
||||
onSuccess?.();
|
||||
onTokenRefreshSuccess?.();
|
||||
return http.instance({
|
||||
...originalRequest,
|
||||
headers: { ...originalRequest.headers, Authorization: `Bearer ${refreshedToken}` },
|
||||
});
|
||||
} else {
|
||||
onError?.("Invalid Refresh Token.");
|
||||
const errorMessage = "Invalid Refresh Token.";
|
||||
onError?.(errorMessage);
|
||||
onTokenRefreshError?.(errorMessage);
|
||||
}
|
||||
|
||||
setIsRefreshing(false);
|
||||
@@ -51,33 +67,35 @@ export const useOAuthFlow = ({ accessToken, refreshUrl, clientId, onError, onSuc
|
||||
http.responseInterceptor.eject(interceptorId);
|
||||
}
|
||||
};
|
||||
}, [clientAccessToken, isRefreshing, refreshUrl, onError, onSuccess]);
|
||||
}, [clientAccessToken, isRefreshing, refreshUrl, onError, onSuccess, onTokenRefreshStart, onTokenRefreshSuccess, onTokenRefreshError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken && http.getUrl() && prevAccessToken !== accessToken) {
|
||||
http.setAuthorizationHeader(accessToken);
|
||||
try {
|
||||
http
|
||||
.get<ApiResponse>(`/provider/${clientId}/access-token`)
|
||||
.catch(async (err: AxiosError) => {
|
||||
if ((err.response?.status === 498 || err.response?.status === 401) && refreshUrl) {
|
||||
setIsRefreshing(true);
|
||||
const refreshedToken = await http.refreshTokens(refreshUrl);
|
||||
if (refreshedToken) {
|
||||
setClientAccessToken(refreshedToken);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
onError?.("Invalid Refresh Token.");
|
||||
}
|
||||
setIsRefreshing(false);
|
||||
http
|
||||
.get<ApiResponse>(`/provider/${clientId}/access-token`)
|
||||
.catch(async (err: AxiosError) => {
|
||||
if ((err.response?.status === 498 || err.response?.status === 401) && refreshUrl) {
|
||||
setIsRefreshing(true);
|
||||
onTokenRefreshStart?.();
|
||||
const refreshedToken = await http.refreshTokens(refreshUrl);
|
||||
if (refreshedToken) {
|
||||
setClientAccessToken(refreshedToken);
|
||||
onSuccess?.();
|
||||
onTokenRefreshSuccess?.();
|
||||
} else {
|
||||
const errorMessage = "Invalid Refresh Token.";
|
||||
onError?.(errorMessage);
|
||||
onTokenRefreshError?.(errorMessage);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setClientAccessToken(accessToken);
|
||||
});
|
||||
} catch (err) {}
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setClientAccessToken(accessToken);
|
||||
});
|
||||
}
|
||||
}, [accessToken, clientId, refreshUrl, prevAccessToken, onError, onSuccess]);
|
||||
}, [accessToken, clientId, refreshUrl, prevAccessToken, onError, onSuccess, onTokenRefreshStart, onTokenRefreshSuccess, onTokenRefreshError]);
|
||||
|
||||
return { isRefreshing, currentAccessToken: clientAccessToken };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user