Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/platform/atoms/cal-provider/BaseCalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -57,6 +60,9 @@ export function BaseCalProvider({
language = EN,
organizationId,
onTimezoneChange,
onTokenRefreshStart,
onTokenRefreshSuccess,
onTokenRefreshError,
isEmbed,
}: CalProviderProps) {
const [error, setError] = useState<string>("");
Expand Down Expand Up @@ -100,6 +106,9 @@ export function BaseCalProvider({
onSuccess: () => {
setError("");
},
onTokenRefreshStart,
onTokenRefreshSuccess,
onTokenRefreshError,
clientId,
});

Expand Down
9 changes: 9 additions & 0 deletions packages/platform/atoms/cal-provider/CalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -35,6 +38,9 @@ export function CalProvider({
labels,
language = "en",
onTimezoneChange,
onTokenRefreshStart,
onTokenRefreshSuccess,
onTokenRefreshError,
version = VERSION_2024_06_14,
organizationId,
isEmbed = false,
Expand All @@ -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}
Expand Down
66 changes: 42 additions & 24 deletions packages/platform/atoms/hooks/useOAuthFlow.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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 };
};
Loading