import type { UseMutationOptions } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query"; import { usePathname } from "next/navigation"; import type { IntegrationOAuthCallbackState } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; import type { App } from "@calcom/types/App"; function gotoUrl(url: string, newTab?: boolean) { if (newTab) { window.open(url, "_blank"); return; } window.location.href = url; } type CustomUseMutationOptions = | Omit, "mutationKey" | "mutationFn" | "onSuccess"> | undefined; type AddAppMutationData = { setupPending: boolean } | void; export type UseAddAppMutationOptions = CustomUseMutationOptions & { onSuccess?: (data: AddAppMutationData) => void; installGoogleVideo?: boolean; returnTo?: string; }; function useAddAppMutation(_type: App["type"] | null, options?: UseAddAppMutationOptions) { const pathname = usePathname(); const onErrorReturnTo = `${WEBAPP_URL}${pathname}`; const mutation = useMutation< AddAppMutationData, Error, | { type?: App["type"]; variant?: string; slug?: string; teamId?: number; returnTo?: string; defaultInstall?: boolean; } | "" >({ ...options, mutationFn: async (variables) => { let type: string | null | undefined; const teamId = variables && variables.teamId ? variables.teamId : undefined; const defaultInstall = variables && variables.defaultInstall ? variables.defaultInstall : undefined; const returnTo = options?.returnTo ? options.returnTo : variables && variables.returnTo ? variables.returnTo : undefined; if (variables === "") { type = _type; } else { type = variables.type; } if (type?.endsWith("_other_calendar")) { type = type.split("_other_calendar")[0]; } if (options?.installGoogleVideo && type !== "google_calendar") throw new Error("Could not install Google Meet"); const state: IntegrationOAuthCallbackState = { onErrorReturnTo, fromApp: true, ...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }), ...(teamId && { teamId }), ...(returnTo && { returnTo }), ...(defaultInstall && { defaultInstall }), }; const stateStr = JSON.stringify(state); const searchParams = generateSearchParamString({ stateStr, teamId, returnTo, }); const res = await fetch(`/api/integrations/${type}/add${searchParams}`); if (!res.ok) { const errorBody = await res.json(); throw new Error(errorBody.message || "Something went wrong"); } const json = await res.json(); const externalUrl = /https?:\/\//.test(json?.url) && !json?.url?.startsWith(window.location.origin); // Check first that the URL is absolute, then check that it is of different origin from the current. if (externalUrl) { // TODO: For Omni installation to authenticate and come back to the page where installation was initiated, some changes need to be done in all apps' add callbacks gotoUrl(json.url, json.newTab); return { setupPending: !json.newTab }; } else if (json.url) { gotoUrl(json.url, json.newTab); return { setupPending: json?.url?.endsWith("/setup") || json?.url?.includes("/apps/installation/event-types"), }; } else if (returnTo) { gotoUrl(returnTo, false); return { setupPending: true }; } else { return { setupPending: false }; } }, }); return mutation; } export default useAddAppMutation; const generateSearchParamString = ({ stateStr, teamId, returnTo, }: { stateStr: string; teamId?: number; returnTo?: string; }) => { const url = new URL("https://example.com"); // Base URL can be anything since we only care about the search params url.searchParams.append("state", stateStr); if (teamId !== undefined) { url.searchParams.append("teamId", teamId.toString()); } if (returnTo) { url.searchParams.append("returnTo", returnTo); } // Return the search string part of the URL return url.search; };