import Link from "next/link"; import { useRouter } from "next/navigation"; import type { IframeHTMLAttributes } from "react"; import React, { useEffect, useState } from "react"; import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation"; import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components"; import { doesAppSupportTeamInstall, isConferencing } from "@calcom/app-store/utils"; import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"; import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl"; import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS, WEBAPP_URL } from "@calcom/lib/constants"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import type { App as AppType } from "@calcom/types/App"; import classNames from "@calcom/ui/classNames"; import { Badge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; import { Icon } from "@calcom/ui/components/icon"; import { SkeletonButton, SkeletonText } from "@calcom/ui/components/skeleton"; import { showToast } from "@calcom/ui/components/toast"; import { InstallAppButtonChild } from "./InstallAppButtonChild"; import { MultiDisconnectIntegration } from "./MultiDisconnectIntegration"; export type AppPageProps = { name: string; description: AppType["description"]; type: AppType["type"]; isGlobal?: AppType["isGlobal"]; logo: string; slug: string; variant: string; body: React.ReactNode; categories: string[]; author: string; pro?: boolean; price?: number; commission?: number; feeType?: AppType["feeType"]; docs?: string; website?: string; email: string; // required tos?: string; privacy?: string; licenseRequired: AppType["licenseRequired"]; teamsPlanRequired: AppType["teamsPlanRequired"]; descriptionItems?: Array }>; isTemplate?: boolean; disableInstall?: boolean; dependencies?: string[]; concurrentMeetings: AppType["concurrentMeetings"]; paid?: AppType["paid"]; }; export const AppPage = ({ name, type, logo, slug, variant, body, categories, author, price = 0, commission, isGlobal = false, feeType, docs, website, email, tos, privacy, teamsPlanRequired, descriptionItems, isTemplate, dependencies, concurrentMeetings, paid, }: AppPageProps) => { const { t, i18n } = useLocale(); const router = useRouter(); const searchParams = useCompatSearchParams(); const hasDescriptionItems = descriptionItems && descriptionItems.length > 0; const utils = trpc.useUtils(); const mutation = useAddAppMutation(null, { onSuccess: async (data) => { if (data?.setupPending) return; setIsLoading(false); showToast(data?.message || t("app_successfully_installed"), "success"); await utils.viewer.apps.appCredentialsByType.invalidate({ appType: type }); }, onError: (error) => { if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error"); setIsLoading(false); }, }); /** * @todo Refactor to eliminate the isLoading state by using mutation.isPending directly. * Currently, the isLoading state is used to manage the loading indicator due to the delay in loading the next page, * which is caused by heavy queries in getServersideProps. This causes the loader to turn off before the page changes. */ const [isLoading, setIsLoading] = useState(mutation.isPending); const availableForTeams = doesAppSupportTeamInstall({ appCategories: categories, concurrentMeetings: concurrentMeetings, isPaid: !!paid, }); const handleAppInstall = () => { setIsLoading(true); if (isConferencing(categories)) { mutation.mutate({ type, variant, slug, returnTo: WEBAPP_URL + getAppOnboardingUrl({ slug, step: AppOnboardingSteps.EVENT_TYPES_STEP, }), }); } else if (!availableForTeams) { mutation.mutate({ type }); } else { router.push(getAppOnboardingUrl({ slug, step: AppOnboardingSteps.ACCOUNTS_STEP })); } }; const priceInDollar = Intl.NumberFormat("en-US", { style: "currency", currency: "USD", useGrouping: false, }).format(price); const [existingCredentials, setExistingCredentials] = useState< NonNullable["credentials"] >([]); /** * Marks whether the app is installed for all possible teams and the user. */ const [appInstalledForAllTargets, setAppInstalledForAllTargets] = useState(false); const appDbQuery = trpc.viewer.apps.appCredentialsByType.useQuery({ appType: type }); useEffect( function refactorMeWithoutEffect() { const data = appDbQuery.data; const credentialsCount = data?.credentials.length || 0; setExistingCredentials(data?.credentials || []); const appInstalledForAllTargets = availableForTeams && data?.userAdminTeams && data.userAdminTeams.length > 0 ? credentialsCount >= data.userAdminTeams.length : credentialsCount > 0; setAppInstalledForAllTargets(appInstalledForAllTargets); }, [appDbQuery.data, availableForTeams] ); const dependencyData = trpc.viewer.apps.queryForDependencies.useQuery(dependencies, { enabled: !!dependencies, }); const disableInstall = dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed); // const disableInstall = requiresGCal && !gCalInstalled.data; // variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal // Such apps, can only be installed once. const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other"; useEffect(() => { if (searchParams?.get("defaultInstall") === "true") { mutation.mutate({ type, variant, slug, defaultInstall: true }); } }, []); const installOrDisconnectAppButton = () => { if (appDbQuery.isPending) { return ; } const MultiInstallButtonEl = ( { if (useDefaultComponent) { props = { ...props, onClick: () => { handleAppInstall(); }, loading: isLoading, }; } return ; }} /> ); const SingleInstallButtonEl = ( { if (useDefaultComponent) { props = { ...props, onClick: () => { handleAppInstall(); }, loading: isLoading, }; } return ; }} /> ); return (
{isGlobal || (existingCredentials.length > 0 && allowedMultipleInstalls ? (
{!isGlobal && !appInstalledForAllTargets && MultiInstallButtonEl}
) : ( !appInstalledForAllTargets && SingleInstallButtonEl ))} {existingCredentials.length > 0 && ( <> {existingCredentials.length > 1 ? ( appDbQuery.refetch()} /> ) : ( appDbQuery.refetch()} /> )} )}
); }; return (
{hasDescriptionItems && (
{descriptionItems ? ( descriptionItems.map((descriptionItem, index) => typeof descriptionItem === "object" ? (