diff --git a/apps/api/v2/tsconfig.json b/apps/api/v2/tsconfig.json index 3e738f6261..f56830b0ab 100644 --- a/apps/api/v2/tsconfig.json +++ b/apps/api/v2/tsconfig.json @@ -23,6 +23,6 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false }, - "exclude": ["./dist", "next-i18next.config.js"], - "include": ["./**/*.ts", "../../../packages/types/*.d.ts"] + "exclude": ["./dist"], + "include": ["./**/*.ts", "../../../packages/types/*.d.ts", "next-i18next.config.js"] } diff --git a/apps/web/components/apps/AppPage.tsx b/apps/web/components/apps/AppPage.tsx index 4ff36ca97a..ae2313c733 100644 --- a/apps/web/components/apps/AppPage.tsx +++ b/apps/web/components/apps/AppPage.tsx @@ -21,6 +21,7 @@ export type AppPageProps = { isGlobal?: AppType["isGlobal"]; logo: string; slug: string; + dirName: string | undefined; variant: string; body: React.ReactNode; categories: string[]; @@ -68,6 +69,7 @@ export const AppPage = ({ dependencies, concurrentMeetings, paid, + dirName, }: AppPageProps) => { const { t, i18n } = useLocale(); const hasDescriptionItems = descriptionItems && descriptionItems.length > 0; @@ -223,6 +225,7 @@ export const AppPage = ({ multiInstall concurrentMeetings={concurrentMeetings} paid={paid} + dirName={dirName} {...props} /> ); @@ -262,6 +265,7 @@ export const AppPage = ({ credentials={appDbQuery.data?.credentials} concurrentMeetings={concurrentMeetings} paid={paid} + dirName={dirName} {...props} /> ); diff --git a/apps/web/components/apps/InstallAppButtonChild.tsx b/apps/web/components/apps/InstallAppButtonChild.tsx index 640a603160..fd0a81113a 100644 --- a/apps/web/components/apps/InstallAppButtonChild.tsx +++ b/apps/web/components/apps/InstallAppButtonChild.tsx @@ -1,7 +1,14 @@ +import { useRouter } from "next/navigation"; +import { useMemo } from "react"; + import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { doesAppSupportTeamInstall } from "@calcom/app-store/utils"; import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner"; import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams"; +import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"; +import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl"; +import { shouldRedirectToAppOnboarding } from "@calcom/lib/apps/shouldRedirectToAppOnboarding"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -26,7 +33,9 @@ export const InstallAppButtonChild = ({ multiInstall, credentials, concurrentMeetings, + dirName, paid, + onClick, ...props }: { userAdminTeams?: UserAdminTeams; @@ -36,8 +45,10 @@ export const InstallAppButtonChild = ({ credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"]; concurrentMeetings?: boolean; paid?: AppFrontendPayload["paid"]; + dirName: string | undefined; } & ButtonProps) => { const { t } = useLocale(); + const router = useRouter(); const mutation = useAddAppMutation(null, { onSuccess: (data) => { @@ -49,6 +60,18 @@ export const InstallAppButtonChild = ({ }, }); const shouldDisableInstallation = !multiInstall ? !!(credentials && credentials.length) : false; + const appMetadata = appStoreMetadata[dirName as keyof typeof appStoreMetadata]; + const redirectToAppOnboarding = useMemo(() => shouldRedirectToAppOnboarding(appMetadata), [appMetadata]); + + const _onClick = (e: React.MouseEvent) => { + if (redirectToAppOnboarding) { + router.push( + getAppOnboardingUrl({ slug: addAppMutationInput.slug, step: AppOnboardingSteps.ACCOUNTS_STEP }) + ); + } else if (onClick) { + onClick(e); + } + }; // Paid apps don't support team installs at the moment // Also, cal.ai(the only paid app at the moment) doesn't support team install either @@ -56,6 +79,7 @@ export const InstallAppButtonChild = ({ return ( + ); + } return ( diff --git a/apps/web/components/apps/installation/AccountsStepCard.tsx b/apps/web/components/apps/installation/AccountsStepCard.tsx new file mode 100644 index 0000000000..7ccb612bd7 --- /dev/null +++ b/apps/web/components/apps/installation/AccountsStepCard.tsx @@ -0,0 +1,97 @@ +import type { FC } from "react"; +import React, { useState } from "react"; + +import { classNames } from "@calcom/lib"; +import { CAL_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { Team, User } from "@calcom/prisma/client"; +import { Avatar, StepCard } from "@calcom/ui"; + +type AccountSelectorProps = { + avatar?: string; + name: string; + alreadyInstalled: boolean; + onClick?: () => void; + loading: boolean; + testId: string; +}; + +const AccountSelector: FC = ({ + avatar, + alreadyInstalled, + name, + onClick, + loading, + testId, +}) => { + const { t } = useLocale(); + const [selected, setSelected] = useState(false); + return ( +
{ + if (onClick) { + setSelected(true); + onClick(); + } + }}> + +
+ {name} + {alreadyInstalled ? {t("already_installed")} : ""} +
+
+ ); +}; + +export type PersonalAccountProps = Pick & { alreadyInstalled: boolean }; + +export type TeamsProp = (Pick & { + alreadyInstalled: boolean; +})[]; + +type AccountStepCardProps = { + teams: TeamsProp; + personalAccount: PersonalAccountProps; + onSelect: (id?: number) => void; + loading: boolean; +}; + +export const AccountsStepCard: FC = ({ teams, personalAccount, onSelect, loading }) => { + const { t } = useLocale(); + return ( + +
{t("install_app_on")}
+
+ !personalAccount.alreadyInstalled && !loading && onSelect()} + loading={loading} + /> + {teams.map((team) => ( + !team.alreadyInstalled && !loading && onSelect(team.id)} + loading={loading} + /> + ))} +
+
+ ); +}; diff --git a/apps/web/components/apps/installation/ConfigureStepCard.tsx b/apps/web/components/apps/installation/ConfigureStepCard.tsx new file mode 100644 index 0000000000..9eb8b4bccb --- /dev/null +++ b/apps/web/components/apps/installation/ConfigureStepCard.tsx @@ -0,0 +1,206 @@ +import type { TEventType, TEventTypesForm } from "@pages/apps/installation/[[...step]]"; +import { X } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; +import type { FC } from "react"; +import React, { forwardRef, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { useForm } from "react-hook-form"; +import type { z } from "zod"; + +import { EventTypeAppSettings } from "@calcom/app-store/_components/EventTypeAppSettingsInterface"; +import type { EventTypeAppsList } from "@calcom/app-store/utils"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { AppCategories } from "@calcom/prisma/enums"; +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { Button, Form } from "@calcom/ui"; + +import useAppsData from "@lib/hooks/useAppsData"; + +type TFormType = { + metadata: z.infer; +}; + +type ConfigureStepCardProps = { + slug: string; + userName: string; + categories: AppCategories[]; + credentialId?: number; + loading?: boolean; + formPortalRef: React.RefObject; + eventTypes: TEventType[] | undefined; + setConfigureStep: Dispatch>; +}; + +type EventTypeAppSettingsFormProps = Pick< + ConfigureStepCardProps, + "slug" | "userName" | "categories" | "credentialId" | "loading" +> & { + eventType: TEventType; + handleDelete: () => void; + onSubmit: (values: z.infer) => void; +}; + +type EventTypeAppSettingsWrapperProps = Pick< + ConfigureStepCardProps, + "slug" | "userName" | "categories" | "credentialId" +> & { + eventType: TEventType; +}; + +const EventTypeAppSettingsWrapper: FC = ({ + slug, + eventType, + categories, + credentialId, +}) => { + const { getAppDataGetter, getAppDataSetter } = useAppsData(); + + useEffect(() => { + const appDataSetter = getAppDataSetter(slug as EventTypeAppsList, categories, credentialId); + appDataSetter("enabled", true); + }, []); + + return ( + + ); +}; + +const EventTypeAppSettingsForm = forwardRef( + function EventTypeAppSettingsForm(props, ref) { + const { handleDelete, onSubmit, eventType, loading } = props; + + const formMethods = useForm({ + defaultValues: { + metadata: eventType?.metadata, + }, + }); + + return ( +
{ + const data = formMethods.getValues("metadata"); + onSubmit(data); + }}> +
+
+
+ {eventType.title}{" "} + + /{eventType.team ? eventType.team.slug : props.userName}/{eventType.slug} + +
+ + !loading && handleDelete()} + /> + +
+
+
+ ); + } +); + +export const ConfigureStepCard: FC = ({ + loading, + formPortalRef, + eventTypes, + setConfigureStep, + ...props +}) => { + const { t } = useLocale(); + const { control, getValues } = useFormContext(); + const { fields, update } = useFieldArray({ + control, + name: "eventTypes", + keyName: "fieldId", + }); + + const submitRefs = useRef>>([]); + submitRefs.current = fields.map( + (_ref, index) => (submitRefs.current[index] = React.createRef()) + ); + const mainForSubmitRef = useRef(null); + const [updatedEventTypesStatus, setUpdatedEventTypesStatus] = useState( + fields.filter((field) => field.selected).map((field) => ({ id: field.id, updated: false })) + ); + const [submit, setSubmit] = useState(false); + const allUpdated = updatedEventTypesStatus.every((item) => item.updated); + + useEffect(() => { + setUpdatedEventTypesStatus((prev) => + prev.filter((state) => fields.some((field) => field.id === state.id && field.selected)) + ); + if (!fields.some((field) => field.selected)) { + setConfigureStep(false); + } + }, [fields]); + + useEffect(() => { + if (submit && allUpdated && mainForSubmitRef.current) { + mainForSubmitRef.current?.click(); + setSubmit(false); + } + }, [submit, allUpdated, getValues, mainForSubmitRef]); + + return ( + formPortalRef?.current && + createPortal( +
+
+ {fields.map((field, index) => { + return ( + field.selected && ( + { + const eventMetadataDb = eventTypes?.find( + (eventType) => eventType.id == field.id + )?.metadata; + update(index, { ...field, selected: false, metadata: eventMetadataDb }); + }} + onSubmit={(data) => { + update(index, { ...field, metadata: data }); + setUpdatedEventTypesStatus((prev) => + prev.map((item) => (item.id === field.id ? { ...item, updated: true } : item)) + ); + }} + ref={submitRefs.current[index]} + {...props} + /> + ) + ); + })} +
+ + + +
, + formPortalRef?.current + ) + ); +}; diff --git a/apps/web/components/apps/installation/EventTypesStepCard.tsx b/apps/web/components/apps/installation/EventTypesStepCard.tsx new file mode 100644 index 0000000000..4b5993f8a3 --- /dev/null +++ b/apps/web/components/apps/installation/EventTypesStepCard.tsx @@ -0,0 +1,110 @@ +import type { TEventType, TEventTypesForm } from "@pages/apps/installation/[[...step]]"; +import type { Dispatch, SetStateAction } from "react"; +import type { FC } from "react"; +import React from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { ScrollableArea, Badge, Button } from "@calcom/ui"; + +type EventTypesCardProps = { + userName: string; + setConfigureStep: Dispatch>; +}; + +export const EventTypesStepCard: FC = ({ setConfigureStep, userName }) => { + const { t } = useLocale(); + const { control } = useFormContext(); + const { fields, update } = useFieldArray({ + control, + name: "eventTypes", + keyName: "fieldId", + }); + + return ( + <> +
+ +
    + {fields.map((field, index) => ( + update(index, { ...field, selected: !field.selected })} + userName={userName} + key={field.fieldId} + {...field} + /> + ))} +
+
+
+ + + + ); +}; + +type EventTypeCardProps = TEventType & { userName: string; handleSelect: () => void }; + +const EventTypeCard: FC = ({ + title, + description, + id, + metadata, + length, + selected, + slug, + handleSelect, + team, + userName, +}) => { + const parsedMetaData = EventTypeMetaDataSchema.safeParse(metadata); + const durations = + parsedMetaData.success && + parsedMetaData.data?.multipleDuration && + Boolean(parsedMetaData.data?.multipleDuration.length) + ? [length, ...parsedMetaData.data?.multipleDuration?.filter((duration) => duration !== length)].sort() + : [length]; + return ( +
handleSelect()}> + + +
+ ); +}; diff --git a/apps/web/components/apps/installation/StepHeader.tsx b/apps/web/components/apps/installation/StepHeader.tsx new file mode 100644 index 0000000000..e79dbc2ea6 --- /dev/null +++ b/apps/web/components/apps/installation/StepHeader.tsx @@ -0,0 +1,19 @@ +import type { FC, ReactNode } from "react"; + +type StepHeaderProps = { + children?: ReactNode; + title: string; + subtitle: string; +}; +export const StepHeader: FC = ({ children, title, subtitle }) => { + return ( +
+
+

{title}

+ +

{subtitle}

+
+ {children} +
+ ); +}; diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index 8d56dd00b1..8515c5a074 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import type { EventTypeSetupProps } from "pages/event-types/[type]"; import { useFormContext } from "react-hook-form"; -import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface"; import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; @@ -13,6 +12,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Alert, Button, EmptyScreen } from "@calcom/ui"; +import useAppsData from "@lib/hooks/useAppsData"; + export type EventType = Pick["eventType"] & EventTypeAppCardComponentProps["eventType"]; @@ -28,51 +29,8 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || []; const notInstalledApps = eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || []; - const allAppsData = formMethods.watch("metadata")?.apps || {}; - const setAllAppsData = (_allAppsData: typeof allAppsData) => { - formMethods.setValue( - "metadata", - { - ...formMethods.getValues("metadata"), - apps: _allAppsData, - }, - { shouldDirty: true } - ); - }; - - const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => { - return function (key) { - const appData = allAppsData[appId as keyof typeof allAppsData] || {}; - if (key) { - return appData[key as keyof typeof appData]; - } - return appData; - }; - }; - - const eventTypeFormMetadata = formMethods.getValues("metadata"); - - const getAppDataSetter = ( - appId: EventTypeAppsList, - appCategories: string[], - credentialId?: number - ): SetAppData => { - return function (key, value) { - // Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render) - const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {}; - const appData = allAppsDataFromForm[appId]; - setAllAppsData({ - ...allAppsDataFromForm, - [appId]: { - ...appData, - [key]: value, - credentialId, - appCategories, - }, - }); - }; - }; + const { getAppDataGetter, getAppDataSetter, eventTypeFormMetadata } = useAppsData(); const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({ eventType, diff --git a/apps/web/lib/hooks/useAppsData.ts b/apps/web/lib/hooks/useAppsData.ts new file mode 100644 index 0000000000..b5496a867a --- /dev/null +++ b/apps/web/lib/hooks/useAppsData.ts @@ -0,0 +1,58 @@ +import type { FormValues } from "pages/event-types/[type]"; +import { useFormContext } from "react-hook-form"; + +import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; +import type { EventTypeAppsList } from "@calcom/app-store/utils"; + +const useAppsData = () => { + const formMethods = useFormContext(); + const allAppsData = formMethods.watch("metadata")?.apps || {}; + + const setAllAppsData = (_allAppsData: typeof allAppsData) => { + formMethods.setValue( + "metadata", + { + ...formMethods.getValues("metadata"), + apps: _allAppsData, + }, + { shouldDirty: true } + ); + }; + + const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => { + return function (key) { + const appData = allAppsData[appId as keyof typeof allAppsData] || {}; + if (key) { + return appData[key as keyof typeof appData]; + } + return appData; + }; + }; + + const eventTypeFormMetadata = formMethods.getValues("metadata"); + + const getAppDataSetter = ( + appId: EventTypeAppsList, + appCategories: string[], + credentialId?: number + ): SetAppData => { + return function (key, value) { + // Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render) + const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {}; + const appData = allAppsDataFromForm[appId]; + setAllAppsData({ + ...allAppsDataFromForm, + [appId]: { + ...appData, + [key]: value, + credentialId, + appCategories, + }, + }); + }; + }; + + return { getAppDataGetter, getAppDataSetter, eventTypeFormMetadata }; +}; + +export default useAppsData; diff --git a/apps/web/pages/apps/[slug]/index.tsx b/apps/web/pages/apps/[slug]/index.tsx index 8eee52c3f5..1927fc8671 100644 --- a/apps/web/pages/apps/[slug]/index.tsx +++ b/apps/web/pages/apps/[slug]/index.tsx @@ -53,6 +53,7 @@ function SingleAppPage(props: inferSSRProps) { slug={data.slug} variant={data.variant} type={data.type} + dirName={data.dirName} logo={data.logo} categories={data.categories ?? [data.category]} author={data.publisher} diff --git a/apps/web/pages/apps/installation/[[...step]].tsx b/apps/web/pages/apps/installation/[[...step]].tsx new file mode 100644 index 0000000000..a8f4040775 --- /dev/null +++ b/apps/web/pages/apps/installation/[[...step]].tsx @@ -0,0 +1,497 @@ +import type { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import Head from "next/head"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import { Toaster } from "react-hot-toast"; +import { z } from "zod"; + +import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import type { EventTypeAppSettingsComponentProps, EventTypeModel } from "@calcom/app-store/types"; +import { getLocale } from "@calcom/features/auth/lib/getLocale"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"; +import { getAppOnboardingRedirectUrl } from "@calcom/lib/apps/getAppOnboardingRedirectUrl"; +import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl"; +import { CAL_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import prisma from "@calcom/prisma"; +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { trpc } from "@calcom/trpc/react"; +import type { AppMeta } from "@calcom/types/App"; +import { Form, Steps, showToast } from "@calcom/ui"; + +import { HttpError } from "@lib/core/http/error"; + +import PageWrapper from "@components/PageWrapper"; +import type { PersonalAccountProps, TeamsProp } from "@components/apps/installation/AccountsStepCard"; +import { AccountsStepCard } from "@components/apps/installation/AccountsStepCard"; +import { ConfigureStepCard } from "@components/apps/installation/ConfigureStepCard"; +import { EventTypesStepCard } from "@components/apps/installation/EventTypesStepCard"; +import { StepHeader } from "@components/apps/installation/StepHeader"; + +export type TEventType = EventTypeAppSettingsComponentProps["eventType"] & + Pick & { + selected: boolean; + }; + +export type TEventTypesForm = { + eventTypes: TEventType[]; +}; + +const STEPS = [ + AppOnboardingSteps.ACCOUNTS_STEP, + AppOnboardingSteps.EVENT_TYPES_STEP, + AppOnboardingSteps.CONFIGURE_STEP, +] as const; +const MAX_NUMBER_OF_STEPS = STEPS.length; + +type StepType = (typeof STEPS)[number]; + +type StepObj = Record< + StepType, + { + getTitle: (appName: string) => string; + getDescription: (appName: string) => string; + stepNumber: number; + } +>; + +type OnboardingPageProps = { + appMetadata: AppMeta; + step: StepType; + teams: TeamsProp; + personalAccount: PersonalAccountProps; + eventTypes?: TEventType[]; + userName: string; + credentialId?: number; +}; + +const OnboardingPage = ({ + step, + teams, + personalAccount, + appMetadata, + eventTypes, + userName, + credentialId, +}: OnboardingPageProps) => { + const { t } = useLocale(); + const pathname = usePathname(); + const router = useRouter(); + + const STEPS_MAP: StepObj = { + [AppOnboardingSteps.ACCOUNTS_STEP]: { + getTitle: () => `${t("select_account_header")}`, + getDescription: (appName) => `${t("select_account_description", { appName })}`, + stepNumber: 1, + }, + [AppOnboardingSteps.EVENT_TYPES_STEP]: { + getTitle: () => `${t("select_event_types_header")}`, + getDescription: (appName) => `${t("select_event_types_description", { appName })}`, + stepNumber: 2, + }, + [AppOnboardingSteps.CONFIGURE_STEP]: { + getTitle: (appName) => `${t("configure_app_header", { appName })}`, + getDescription: () => `${t("configure_app_description")}`, + stepNumber: 3, + }, + } as const; + const [configureStep, setConfigureStep] = useState(false); + const [isSelectingAccount, setIsSelectingAccount] = useState(false); + + const currentStep: AppOnboardingSteps = useMemo(() => { + if (step == AppOnboardingSteps.EVENT_TYPES_STEP && configureStep) { + return AppOnboardingSteps.CONFIGURE_STEP; + } + return step; + }, [step, configureStep]); + const stepObj = STEPS_MAP[currentStep]; + + const utils = trpc.useContext(); + + const formPortalRef = useRef(null); + + const formMethods = useForm({ + defaultValues: { + eventTypes, + }, + }); + + useEffect(() => { + eventTypes && formMethods.setValue("eventTypes", eventTypes); + }, [eventTypes]); + + const updateMutation = trpc.viewer.eventTypes.update.useMutation({ + onSuccess: async (data) => { + showToast(t("event_type_updated_successfully", { eventTypeTitle: data.eventType?.title }), "success"); + router.push("/event-types"); + }, + async onSettled() { + await utils.viewer.eventTypes.get.invalidate(); + }, + onError: (err) => { + let message = ""; + if (err instanceof HttpError) { + const message = `${err.statusCode}: ${err.message}`; + showToast(message, "error"); + } + + if (err.data?.code === "UNAUTHORIZED") { + message = `${err.data.code}: ${t("error_event_type_unauthorized_update")}`; + } + + if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") { + message = `${err.data.code}: ${t(err.message)}`; + } + + if (err.data?.code === "INTERNAL_SERVER_ERROR") { + message = t("unexpected_error_try_again"); + } + + showToast(message ? t(message) : t(err.message), "error"); + }, + }); + + const handleSelectAccount = async (teamId?: number) => { + try { + setIsSelectingAccount(true); + if (appMetadata.isOAuth) { + const state = JSON.stringify({ + appOnboardingRedirectUrl: getAppOnboardingRedirectUrl(appMetadata.slug, teamId), + teamId, + }); + + const res = await fetch( + `/api/integrations/${ + appMetadata.slug == "stripe" ? "stripepayment" : appMetadata.slug + }/add?state=${state}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + const oAuthUrl = (await res.json())?.url; + router.push(oAuthUrl); + return; + } else { + await fetch(`/api/integrations/${appMetadata.slug}/add${teamId ? `?teamId=${teamId}` : ""}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + router.push( + getAppOnboardingUrl({ + slug: appMetadata.slug, + step: AppOnboardingSteps.EVENT_TYPES_STEP, + teamId, + }) + ); + } + } catch (error) { + setIsSelectingAccount(false); + router.push(`/apps`); + } + }; + + return ( +
+ + + {t("install")} {appMetadata?.name ?? ""} + + + +
+
+
+
{ + const mutationPromises = values?.eventTypes + .filter((eventType) => eventType.selected) + .map((value: TEventType) => { + // Prevent two payment apps to be enabled + // Ok to cast type here because this metadata will be updated as the event type metadata + if ( + checkForMultiplePaymentApps(value.metadata as z.infer) + ) + throw new Error(t("event_setup_multiple_payment_apps_error")); + if (value.metadata?.apps?.stripe?.paymentOption === "HOLD" && value.seatsPerTimeSlot) { + throw new Error(t("seats_and_no_show_fee_error")); + } + return updateMutation.mutateAsync({ + id: value.id, + metadata: value.metadata, + }); + }); + await Promise.all(mutationPromises); + }}> + + + + {currentStep === AppOnboardingSteps.ACCOUNTS_STEP && ( + + )} + {currentStep === AppOnboardingSteps.EVENT_TYPES_STEP && + eventTypes && + Boolean(eventTypes?.length) && ( + + )} + {currentStep === AppOnboardingSteps.CONFIGURE_STEP && formPortalRef.current && ( + + )} + +
+
+
+ +
+ ); +}; + +// Redirect Error map to give context on edge cases, this is for the devs, never shown to users +const ERROR_MESSAGES = { + appNotFound: "App not found", + userNotAuthed: "User is not logged in", + userNotFound: "User from session not found", + appNotExtendsEventType: "App does not extend EventTypes", + userNotInTeam: "User is not in provided team", +} as const; + +const getUser = async (userId: number) => { + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + id: true, + avatar: true, + name: true, + username: true, + teams: { + where: { + accepted: true, + team: { + members: { + some: { + userId, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + }, + }, + }, + select: { + team: { + select: { + id: true, + name: true, + logo: true, + }, + }, + }, + }, + }, + }); + + if (!user) { + throw new Error(ERROR_MESSAGES.userNotFound); + } + return user; +}; + +const getAppBySlug = async (appSlug: string) => { + const app = await prisma.app.findUnique({ + where: { slug: appSlug, enabled: true }, + select: { slug: true, keys: true, enabled: true, dirName: true }, + }); + if (!app) throw new Error(ERROR_MESSAGES.appNotFound); + return app; +}; + +const getEventTypes = async (userId: number, teamId?: number) => { + const eventTypes = ( + await prisma.eventType.findMany({ + select: { + id: true, + description: true, + durationLimits: true, + metadata: true, + length: true, + title: true, + position: true, + recurringEvent: true, + requiresConfirmation: true, + team: { select: { slug: true } }, + schedulingType: true, + teamId: true, + users: { select: { username: true } }, + seatsPerTimeSlot: true, + slug: true, + }, + where: teamId ? { teamId } : { userId }, + }) + ).sort((eventTypeA, eventTypeB) => { + return eventTypeB.position - eventTypeA.position; + }); + if (eventTypes.length === 0) { + return []; + } + return eventTypes.map((item) => ({ + ...item, + URL: `${CAL_URL}/${item.team ? `team/${item.team.slug}` : item?.users?.[0]?.username}/${item.slug}`, + selected: false, + })); +}; + +const getAppInstallsBySlug = async (appSlug: string, userId: number, teamIds?: number[]) => { + const appInstalls = await prisma.credential.findMany({ + where: { + OR: [ + { + appId: appSlug, + userId: userId, + }, + teamIds && Boolean(teamIds.length) + ? { + appId: appSlug, + teamId: { in: teamIds }, + } + : {}, + ], + }, + }); + return appInstalls; +}; + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + try { + let eventTypes: TEventType[] | null = null; + const { req, res, query, params } = context; + const stepsEnum = z.enum(STEPS); + const parsedAppSlug = z.coerce.string().parse(query?.slug); + const parsedStepParam = z.coerce.string().parse(params?.step); + const parsedTeamIdParam = z.coerce.number().optional().parse(query?.teamId); + const _ = stepsEnum.parse(parsedStepParam); + const session = await getServerSession({ req, res }); + const locale = await getLocale(context.req); + const app = await getAppBySlug(parsedAppSlug); + const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata]; + const hasEventTypes = appMetadata?.extendsFeature === "EventType"; + + if (!session?.user?.id) throw new Error(ERROR_MESSAGES.userNotAuthed); + if (!hasEventTypes) { + throw new Error(ERROR_MESSAGES.appNotExtendsEventType); + } + + const user = await getUser(session.user.id); + + const userAcceptedTeams = user.teams.map((team) => ({ ...team.team })); + const hasTeams = Boolean(userAcceptedTeams.length); + + const appInstalls = await getAppInstallsBySlug( + parsedAppSlug, + user.id, + userAcceptedTeams.map(({ id }) => id) + ); + + if (parsedTeamIdParam) { + const isUserMemberOfTeam = userAcceptedTeams.some((team) => team.id === parsedTeamIdParam); + if (!isUserMemberOfTeam) { + throw new Error(ERROR_MESSAGES.userNotInTeam); + } + } + + if (parsedStepParam == AppOnboardingSteps.EVENT_TYPES_STEP) { + eventTypes = await getEventTypes(user.id, parsedTeamIdParam); + if (eventTypes.length === 0) { + return { + redirect: { permanent: false, destination: `/apps/installed/${appMetadata.categories[0]}` }, + }; + } + } + + const personalAccount = { + id: user.id, + name: user.name, + avatar: user.avatar, + alreadyInstalled: appInstalls.some((install) => !Boolean(install.teamId) && install.userId === user.id), + }; + + const teamsWithIsAppInstalled = hasTeams + ? userAcceptedTeams.map((team) => ({ + ...team, + alreadyInstalled: appInstalls.some( + (install) => Boolean(install.teamId) && install.teamId === team.id + ), + })) + : []; + let credentialId = null; + if (parsedTeamIdParam) { + credentialId = + appInstalls.find((item) => !!item.teamId && item.teamId == parsedTeamIdParam)?.id ?? null; + } else { + credentialId = appInstalls.find((item) => !!item.userId && item.userId == user.id)?.id ?? null; + } + return { + props: { + ...(await serverSideTranslations(locale, ["common"])), + app, + appMetadata, + step: parsedStepParam, + teams: teamsWithIsAppInstalled, + personalAccount, + eventTypes, + teamId: parsedTeamIdParam ?? null, + userName: user.username, + credentialId, + } as OnboardingPageProps, + }; + } catch (err) { + if (err instanceof z.ZodError) { + return { redirect: { permanent: false, destination: "/apps" } }; + } + + if (err instanceof Error) { + switch (err.message) { + case ERROR_MESSAGES.userNotAuthed: + return { redirect: { permanent: false, destination: "/auth/login" } }; + case ERROR_MESSAGES.userNotFound: + return { redirect: { permanent: false, destination: "/auth/login" } }; + default: + return { redirect: { permanent: false, destination: "/apps" } }; + } + } + } +}; + +OnboardingPage.PageWrapper = PageWrapper; + +export default OnboardingPage; diff --git a/apps/web/playwright/fixtures/apps.ts b/apps/web/playwright/fixtures/apps.ts index 61c649684a..6b0a294b6b 100644 --- a/apps/web/playwright/fixtures/apps.ts +++ b/apps/web/playwright/fixtures/apps.ts @@ -1,5 +1,8 @@ import { expect, type Page } from "@playwright/test"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import { shouldRedirectToAppOnboarding } from "@calcom/lib/apps/shouldRedirectToAppOnboarding"; + export function createAppsFixture(page: Page) { return { goToAppsCategory: async (category: string) => { @@ -9,6 +12,13 @@ export function createAppsFixture(page: Page) { installApp: async (app: string) => { await page.getByTestId(`app-store-app-card-${app}`).click(); await page.getByTestId("install-app-button").click(); + + const appMetadata = appStoreMetadata[app as keyof typeof appStoreMetadata]; + if (shouldRedirectToAppOnboarding(appMetadata)) { + await page.click('[data-testid="install-app-on-personal-account"]'); + await page.waitForURL(`apps/installation/event-types?slug=${app}`); + await page.goto(`/apps/installed/${appMetadata.categories[0]}?hl=${app}`); + } }, goBackToAppsPage: async () => { await page.getByTestId("add-apps").click(); diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 4e7badbaa3..679a19056e 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -912,14 +912,11 @@ export async function getPaymentCredential(page: Page) { await page.goto("/apps/stripe"); /** We start the Stripe flow */ - await Promise.all([ - page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"), - page.click('[data-testid="install-app-button"]'), - ]); - - await Promise.all([ - page.waitForURL("/apps/installed/payment?hl=stripe"), - /** We skip filling Stripe forms (testing mode only) */ - page.click('[id="skip-account-app"]'), - ]); + await page.click('[data-testid="install-app-button"]'); + await page.click('[data-testid="install-app-on-personal-account"]'); + await page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"); + /** We skip filling Stripe forms (testing mode only) */ + await page.click('[id="skip-account-app"]'); + await page.waitForURL("apps/installation/event-types?slug=stripe"); + await page.goto("/apps/installed/payment?hl=stripe"); } diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index 19291b6345..31c00a35c9 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -92,18 +92,13 @@ test.describe("Stripe integration", () => { await page.goto("/apps/stripe"); /** We start the Stripe flow */ - await Promise.all([ - page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"), - page.click('[data-testid="install-app-button"]'), - page.click('[data-testid="anything else"]'), - ]); - - await Promise.all([ - page.waitForURL("/apps/installed/payment?hl=stripe"), - /** We skip filling Stripe forms (testing mode only) */ - page.click('[id="skip-account-app"]'), - ]); - + await page.click('[data-testid="install-app-button"]'); + await page.click(`[data-testid="install-app-on-team-${team.id}"]`); + await page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"); + await page.click('[id="skip-account-app"]'); + await page.waitForURL(`apps/installation/event-types?slug=stripe&teamId=${team.id}`); + await page.goto("/apps/installed/payment?hl=stripe"); + /** We skip filling Stripe forms (testing mode only) */ await owner.setupEventWithPrice(teamEvent, "stripe"); // Need to wait for the DB to be updated with the metadata diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 8be7bd789a..95fb1fef09 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2366,6 +2366,13 @@ "lock_org_users_eventtypes_description": "Prevent members from creating their own event types.", "cookie_consent_checkbox": "I consent to our privacy policy and cookie usage", "make_a_call": "Make a Call", + "select_account_header": "Select Account", + "select_account_description": "Install {{appName}} on your personal account or on a team account.", + "select_event_types_header": "Select Event Types", + "select_event_types_description": "On which event type do you want to install {{appName}}?", + "configure_app_header": "Configure {{appName}}", + "configure_app_description": "Finalise the App setup. You can change these settings later.", + "already_installed": "already installed", "ooo_reasons_unspecified": "Unspecified", "ooo_reasons_vacation": "Vacation", "ooo_reasons_travel": "Travel", diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 68702b7710..91c461b548 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -328,6 +328,14 @@ function generateFiles() { lazyImport: true, }) ); + browserOutput.push( + ...getExportedObject("EventTypeSettingsMap", { + importConfig: { + fileToBeImported: "components/EventTypeAppSettingsInterface.tsx", + }, + lazyImport: true, + }) + ); const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. diff --git a/packages/app-store/_components/DynamicComponent.tsx b/packages/app-store/_components/DynamicComponent.tsx index 30e00222f8..e13a5a4292 100644 --- a/packages/app-store/_components/DynamicComponent.tsx +++ b/packages/app-store/_components/DynamicComponent.tsx @@ -8,7 +8,7 @@ export function DynamicComponent { + const { slug, ...rest } = props; + return ; +}; diff --git a/packages/app-store/_utils/oauth/decodeOAuthState.ts b/packages/app-store/_utils/oauth/decodeOAuthState.ts index c300a808c6..2a432997ef 100644 --- a/packages/app-store/_utils/oauth/decodeOAuthState.ts +++ b/packages/app-store/_utils/oauth/decodeOAuthState.ts @@ -7,6 +7,9 @@ export function decodeOAuthState(req: NextApiRequest) { return undefined; } const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state); + if (state.appOnboardingRedirectUrl) { + state.appOnboardingRedirectUrl = decodeURIComponent(state.appOnboardingRedirectUrl); + } return state; } diff --git a/packages/app-store/alby/components/EventTypeAppCardInterface.tsx b/packages/app-store/alby/components/EventTypeAppCardInterface.tsx index 3bc5e0e3fb..70b497522e 100644 --- a/packages/app-store/alby/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/alby/components/EventTypeAppCardInterface.tsx @@ -1,20 +1,15 @@ import { usePathname, useSearchParams } from "next/navigation"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; -import { currencyOptions } from "@calcom/app-store/alby/lib/currencyOptions"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert, Select, TextField } from "@calcom/ui"; -import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol"; import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; import type { appDataSchema } from "../zod"; -import { PaypalPaymentOptions as paymentOptions } from "../zod"; - -type Option = { value: string; label: string }; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, @@ -22,38 +17,19 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventTypeFormMetadata, }) { const searchParams = useSearchParams(); + const { t } = useLocale(); /** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/ const pathname = usePathname(); const asPath = useMemo( () => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`, [pathname, searchParams] ); - const { getAppData, setAppData } = useAppContextWithSchema(); - const price = getAppData("price"); - const currency = getAppData("currency"); - const [selectedCurrency, setSelectedCurrency] = useState( - currencyOptions.find((c) => c.value === currency) || currencyOptions[0] - ); - const paymentOption = getAppData("paymentOption"); - const paymentOptionSelectValue = paymentOptions?.find((option) => paymentOption === option.value) || { - label: paymentOptions[0].label, - value: paymentOptions[0].value, - }; - const seatsEnabled = !!eventType.seatsPerTimeSlot; + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); - const { t } = useLocale(); - const recurringEventDefined = eventType.recurringEvent?.count !== undefined; const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; - // make sure a currency is selected - useEffect(() => { - if (!currency && requirePayment) { - setAppData("currency", selectedCurrency.value); - } - }, [currency, selectedCurrency, setAppData, requirePayment]); - return ( Add bitcoin lightning payments to your events} disableSwitch={shouldDisableSwitch} switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> - <> - {recurringEventDefined ? ( - - ) : ( - requirePayment && ( - <> -
- } - addOnSuffix={selectedCurrency.unit || selectedCurrency.value} - type="number" - required - className="block w-full rounded-sm border-gray-300 pl-2 pr-12 text-sm" - placeholder="Price" - onChange={(e) => { - setAppData("price", Number(e.target.value)); - if (currency) { - setAppData("currency", currency); - } - }} - value={price && price > 0 ? price : undefined} - /> -
-
- - { + if (e) { + setSelectedCurrency(e); + setAppData("currency", e.value); + } + }} + /> +
+ +
+ + + defaultValue={ + paymentOptionSelectValue + ? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) } + : { ...paymentOptions[0], label: t(paymentOptions[0].label) } + } + options={paymentOptions.map((option) => { + return { ...option, label: t(option.label) || option.label }; + })} + onChange={(input) => { + if (input) setAppData("paymentOption", input.value); + }} + className="mb-1 h-[38px] w-full" + isDisabled={seatsEnabled} + /> +
+ {seatsEnabled && paymentOption === "HOLD" && ( + + )} + + ) + )} + + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/alby/config.json b/packages/app-store/alby/config.json index 632ef31447..318d26addf 100644 --- a/packages/app-store/alby/config.json +++ b/packages/app-store/alby/config.json @@ -14,5 +14,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "event-type-app-card", - "dirName": "alby" + "dirName": "alby", + "isOAuth": false } diff --git a/packages/app-store/amie/config.json b/packages/app-store/amie/config.json index 74a9210dc5..ac87eed277 100644 --- a/packages/app-store/amie/config.json +++ b/packages/app-store/amie/config.json @@ -12,5 +12,6 @@ "description": "The joyful productivity app\r\r", "__createdUsingCli": true, "dependencies": ["google-calendar"], - "dirName": "amie" + "dirName": "amie", + "isOAuth": false } diff --git a/packages/app-store/applecalendar/_metadata.ts b/packages/app-store/applecalendar/_metadata.ts index 1b468204a3..b5947fad57 100644 --- a/packages/app-store/applecalendar/_metadata.ts +++ b/packages/app-store/applecalendar/_metadata.ts @@ -17,6 +17,7 @@ export const metadata = { url: "https://cal.com/", email: "help@cal.com", dirName: "applecalendar", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index 19142d4eb5..96f1633ca6 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -41,3 +41,16 @@ export const EventTypeAddonMap = { import("./templates/event-type-app-card/components/EventTypeAppCardInterface") ), }; +export const EventTypeSettingsMap = { + alby: dynamic(() => import("./alby/components/EventTypeAppSettingsInterface")), + basecamp3: dynamic(() => import("./basecamp3/components/EventTypeAppSettingsInterface")), + fathom: dynamic(() => import("./fathom/components/EventTypeAppSettingsInterface")), + ga4: dynamic(() => import("./ga4/components/EventTypeAppSettingsInterface")), + giphy: dynamic(() => import("./giphy/components/EventTypeAppSettingsInterface")), + gtm: dynamic(() => import("./gtm/components/EventTypeAppSettingsInterface")), + metapixel: dynamic(() => import("./metapixel/components/EventTypeAppSettingsInterface")), + paypal: dynamic(() => import("./paypal/components/EventTypeAppSettingsInterface")), + plausible: dynamic(() => import("./plausible/components/EventTypeAppSettingsInterface")), + qr_code: dynamic(() => import("./qr_code/components/EventTypeAppSettingsInterface")), + stripepayment: dynamic(() => import("./stripepayment/components/EventTypeAppSettingsInterface")), +}; diff --git a/packages/app-store/around/config.json b/packages/app-store/around/config.json index 0cc2358e49..186802b882 100644 --- a/packages/app-store/around/config.json +++ b/packages/app-store/around/config.json @@ -20,5 +20,6 @@ "urlRegExp": "^http(s)?:\\/\\/(www\\.)?around.co\\/[a-zA-Z0-9]*", "organizerInputPlaceholder": "https://www.around.co/rick" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/basecamp3/api/callback.ts b/packages/app-store/basecamp3/api/callback.ts index c2dbb9fad1..fb121835f5 100644 --- a/packages/app-store/basecamp3/api/callback.ts +++ b/packages/app-store/basecamp3/api/callback.ts @@ -5,6 +5,7 @@ import prisma from "@calcom/prisma"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; import appConfig from "../config.json"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -86,5 +87,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); + const state = decodeOAuthState(req); + + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + res.redirect(getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug })); } diff --git a/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx b/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx index 98a5518ab8..d359812bea 100644 --- a/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx @@ -1,76 +1,29 @@ -import { useState, useEffect } from "react"; - import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { trpc } from "@calcom/trpc/react"; -import { Select } from "@calcom/ui"; +import useIsAppEnabled from "../../_utils/useIsAppEnabled"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; -const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) { - const { getAppData } = useAppContextWithSchema(); - const [enabled, setEnabled] = useState(getAppData("enabled")); - const [projects, setProjects] = useState(); - const [selectedProject, setSelectedProject] = useState(); - const { data } = trpc.viewer.appBasecamp3.projects.useQuery(); - const setProject = trpc.viewer.appBasecamp3.projectMutation.useMutation(); - - useEffect( - function refactorMeWithoutEffect() { - setSelectedProject({ - value: data?.projects.currentProject, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name, - }); - setProjects( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?.projects?.map((project: any) => { - return { - value: project.id, - label: project.name, - }; - }) - ); - }, - [data] - ); +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); + const { enabled, updateEnabled } = useIsAppEnabled(app); return ( { - if (!e) { - setEnabled(false); - } else { - setEnabled(true); - } + updateEnabled(e); }} switchChecked={enabled}> -
-
-
-

Link a Basecamp project to this event:

-
- { + if (project) { + setProject.mutate({ projectId: project?.value.toString() }); + setSelectedProject(project); + } + }} + value={selectedProject} + /> +
+
+ Please note that as of now you can only link one of your projects to + cal.com +
+
+ ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/basecamp3/config.json b/packages/app-store/basecamp3/config.json index a0cad118dc..af9d241373 100644 --- a/packages/app-store/basecamp3/config.json +++ b/packages/app-store/basecamp3/config.json @@ -13,5 +13,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "event-type-app-card", - "dirName": "basecamp3" + "dirName": "basecamp3", + "isOAuth": true } diff --git a/packages/app-store/cal-ai/config.json b/packages/app-store/cal-ai/config.json index 6ec6551057..c13c045312 100644 --- a/packages/app-store/cal-ai/config.json +++ b/packages/app-store/cal-ai/config.json @@ -14,6 +14,7 @@ "__createdUsingCli": true, "__template": "basic", "dirName": "cal-ai", + "isOAuth": false, "paid": { "priceInUsd": 8, "priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP", diff --git a/packages/app-store/caldavcalendar/_metadata.ts b/packages/app-store/caldavcalendar/_metadata.ts index e342f74fb2..1ea69be584 100644 --- a/packages/app-store/caldavcalendar/_metadata.ts +++ b/packages/app-store/caldavcalendar/_metadata.ts @@ -17,6 +17,7 @@ export const metadata = { url: "https://cal.com/", email: "ali@cal.com", dirName: "caldavcalendar", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/campfire/config.json b/packages/app-store/campfire/config.json index 7f8e670066..2fb381f1ff 100644 --- a/packages/app-store/campfire/config.json +++ b/packages/app-store/campfire/config.json @@ -19,5 +19,6 @@ "organizerInputPlaceholder": "https://party.campfire.to/your-team", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?party.campfire.to\\/[a-zA-Z0-9]*" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/closecom/config.json b/packages/app-store/closecom/config.json index 58a46a86e9..5224a8ab02 100644 --- a/packages/app-store/closecom/config.json +++ b/packages/app-store/closecom/config.json @@ -11,5 +11,6 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/cron/config.json b/packages/app-store/cron/config.json index 8e0b71952b..d5c8dae388 100644 --- a/packages/app-store/cron/config.json +++ b/packages/app-store/cron/config.json @@ -13,5 +13,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "link-as-an-app", - "dependencies": ["google-calendar"] + "dependencies": ["google-calendar"], + "isOAuth": false } diff --git a/packages/app-store/dailyvideo/_metadata.ts b/packages/app-store/dailyvideo/_metadata.ts index b416739784..38c6b5df78 100644 --- a/packages/app-store/dailyvideo/_metadata.ts +++ b/packages/app-store/dailyvideo/_metadata.ts @@ -26,6 +26,7 @@ export const metadata = { }, key: { apikey: process.env.DAILY_API_KEY }, dirName: "dailyvideo", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/discord/config.json b/packages/app-store/discord/config.json index 34682ccce9..1449a0169c 100644 --- a/packages/app-store/discord/config.json +++ b/packages/app-store/discord/config.json @@ -21,5 +21,6 @@ "description": "Copy your server invite link and start scheduling calls in Discord! Discord is a VoIP and instant messaging social platform. Users have the ability to communicate with voice calls, video calls, text messaging, media and files in private chats or as part of communities.", "isTemplate": false, "__createdUsingCli": true, - "__template": "event-type-location-video-static" + "__template": "event-type-location-video-static", + "isOAuth": false } diff --git a/packages/app-store/eightxeight/config.json b/packages/app-store/eightxeight/config.json index a809d0ff54..c0baba41f8 100644 --- a/packages/app-store/eightxeight/config.json +++ b/packages/app-store/eightxeight/config.json @@ -22,5 +22,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "event-type-location-video-static", - "dirName": "eightxeight" + "dirName": "eightxeight", + "isOAuth": false } diff --git a/packages/app-store/element-call/config.json b/packages/app-store/element-call/config.json index e27a228af1..7de9af060a 100644 --- a/packages/app-store/element-call/config.json +++ b/packages/app-store/element-call/config.json @@ -22,5 +22,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "event-type-location-video-static", - "dirName": "element-call" + "dirName": "element-call", + "isOAuth": false } diff --git a/packages/app-store/exchange2013calendar/_metadata.ts b/packages/app-store/exchange2013calendar/_metadata.ts index 3d7c0734af..8859f4fdf2 100644 --- a/packages/app-store/exchange2013calendar/_metadata.ts +++ b/packages/app-store/exchange2013calendar/_metadata.ts @@ -18,6 +18,7 @@ export const metadata = { url: "https://cal.com/", email: "help@cal.com", dirName: "exchange2013calendar", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/exchange2016calendar/_metadata.ts b/packages/app-store/exchange2016calendar/_metadata.ts index 138b5e5442..a851408161 100644 --- a/packages/app-store/exchange2016calendar/_metadata.ts +++ b/packages/app-store/exchange2016calendar/_metadata.ts @@ -18,6 +18,7 @@ export const metadata = { url: "https://cal.com/", email: "help@cal.com", dirName: "exchange2016calendar", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/exchangecalendar/config.json b/packages/app-store/exchangecalendar/config.json index 113fef68a6..c55b9bd13a 100644 --- a/packages/app-store/exchangecalendar/config.json +++ b/packages/app-store/exchangecalendar/config.json @@ -12,5 +12,6 @@ "publisher": "Cal.com", "email": "help@cal.com", "description": "Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/facetime/config.json b/packages/app-store/facetime/config.json index 874468fbf6..ca7c28ad3c 100644 --- a/packages/app-store/facetime/config.json +++ b/packages/app-store/facetime/config.json @@ -21,5 +21,6 @@ "urlRegExp": "^https?:\\/\\/facetime\\.apple\\.com\\/join.+$" } }, - "isTemplate": false + "isTemplate": false, + "isOAuth": false } diff --git a/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx b/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx index 09899e57f7..10e0a0f8a6 100644 --- a/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx @@ -2,13 +2,12 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( @@ -20,14 +19,12 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ }} switchChecked={enabled} teamId={eventType.team?.id || undefined}> - { - setAppData("trackingId", e.target.value); - }} + getAppData={getAppData} + setAppData={setAppData} />
); diff --git a/packages/app-store/fathom/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/fathom/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..ea2a79002c --- /dev/null +++ b/packages/app-store/fathom/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,25 @@ +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { TextField } from "@calcom/ui"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, + slug, +}) => { + const trackingId = getAppData("trackingId"); + + return ( + { + setAppData("trackingId", e.target.value); + }} + /> + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/fathom/config.json b/packages/app-store/fathom/config.json index b491e51160..029bbaf649 100644 --- a/packages/app-store/fathom/config.json +++ b/packages/app-store/fathom/config.json @@ -23,5 +23,6 @@ } }, "description": "Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative.", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx b/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx index 09899e57f7..10e0a0f8a6 100644 --- a/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx @@ -2,13 +2,12 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( @@ -20,14 +19,12 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ }} switchChecked={enabled} teamId={eventType.team?.id || undefined}> - { - setAppData("trackingId", e.target.value); - }} + getAppData={getAppData} + setAppData={setAppData} />
); diff --git a/packages/app-store/ga4/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/ga4/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..0ad1148d49 --- /dev/null +++ b/packages/app-store/ga4/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,23 @@ +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { TextField } from "@calcom/ui"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, +}) => { + const trackingId = getAppData("trackingId"); + + return ( + { + setAppData("trackingId", e.target.value); + }} + /> + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/ga4/config.json b/packages/app-store/ga4/config.json index b0bdb496d5..30d6e8eb06 100644 --- a/packages/app-store/ga4/config.json +++ b/packages/app-store/ga4/config.json @@ -24,5 +24,6 @@ ] } }, - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/giphy/_metadata.ts b/packages/app-store/giphy/_metadata.ts index 050f4ab234..5be513f2a7 100644 --- a/packages/app-store/giphy/_metadata.ts +++ b/packages/app-store/giphy/_metadata.ts @@ -17,6 +17,7 @@ export const metadata = { extendsFeature: "EventType", email: "help@cal.com", dirName: "giphy", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx b/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx index b285eeedef..b48ce29e34 100644 --- a/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx @@ -1,15 +1,14 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; -import { SelectGifInput } from "@calcom/app-store/giphy/components"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const thankYouPage = getAppData("thankYouPage"); const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app); const { t } = useLocale(); @@ -23,15 +22,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ }} switchChecked={showGifSelection} teamId={eventType.team?.id || undefined}> - {showGifSelection && ( - { - setAppData("thankYouPage", url); - }} - /> - )} + ); }; diff --git a/packages/app-store/giphy/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/giphy/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..0cb8017443 --- /dev/null +++ b/packages/app-store/giphy/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,23 @@ +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; + +import SelectGifInput from "./SelectGifInput"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, +}) => { + const thankYouPage = getAppData("thankYouPage"); + + return ( + { + setAppData("thankYouPage", url); + }} + /> + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/googlecalendar/_metadata.ts b/packages/app-store/googlecalendar/_metadata.ts index dbdb831035..4701bde435 100644 --- a/packages/app-store/googlecalendar/_metadata.ts +++ b/packages/app-store/googlecalendar/_metadata.ts @@ -18,6 +18,7 @@ export const metadata = { url: "https://cal.com/", email: "help@cal.com", dirName: "googlecalendar", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/googlevideo/_metadata.ts b/packages/app-store/googlevideo/_metadata.ts index 1fc1bbc03d..c835c79d22 100644 --- a/packages/app-store/googlevideo/_metadata.ts +++ b/packages/app-store/googlevideo/_metadata.ts @@ -27,6 +27,7 @@ export const metadata = { }, dirName: "googlevideo", dependencies: ["google-calendar"], + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx b/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx index 09899e57f7..10e0a0f8a6 100644 --- a/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx @@ -2,13 +2,12 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( @@ -20,14 +19,12 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ }} switchChecked={enabled} teamId={eventType.team?.id || undefined}> - { - setAppData("trackingId", e.target.value); - }} + getAppData={getAppData} + setAppData={setAppData} /> ); diff --git a/packages/app-store/gtm/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/gtm/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..0ad1148d49 --- /dev/null +++ b/packages/app-store/gtm/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,23 @@ +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { TextField } from "@calcom/ui"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, +}) => { + const trackingId = getAppData("trackingId"); + + return ( + { + setAppData("trackingId", e.target.value); + }} + /> + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/gtm/config.json b/packages/app-store/gtm/config.json index b2faf7243d..f535c680e8 100644 --- a/packages/app-store/gtm/config.json +++ b/packages/app-store/gtm/config.json @@ -21,5 +21,6 @@ }, "isTemplate": false, "__createdUsingCli": true, - "__template": "booking-pages-tag" + "__template": "booking-pages-tag", + "isOAuth": false } diff --git a/packages/app-store/hubspot/_metadata.ts b/packages/app-store/hubspot/_metadata.ts index ea5379a233..7212d9635d 100644 --- a/packages/app-store/hubspot/_metadata.ts +++ b/packages/app-store/hubspot/_metadata.ts @@ -17,6 +17,7 @@ export const metadata = { title: "HubSpot CRM", email: "help@cal.com", dirName: "hubspot", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/huddle01video/_metadata.ts b/packages/app-store/huddle01video/_metadata.ts index 9a0f3d6003..65f90fd31a 100644 --- a/packages/app-store/huddle01video/_metadata.ts +++ b/packages/app-store/huddle01video/_metadata.ts @@ -28,6 +28,7 @@ export const metadata = { key: { apikey: randomString(12) }, dirName: "huddle01video", concurrentMeetings: true, + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/intercom/api/callback.ts b/packages/app-store/intercom/api/callback.ts index 6665aba5b6..64c05ceb5d 100644 --- a/packages/app-store/intercom/api/callback.ts +++ b/packages/app-store/intercom/api/callback.ts @@ -8,6 +8,7 @@ import prisma from "@calcom/prisma"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential"; +import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; const log = logger.getSubLogger({ prefix: [`[[intercom/api/callback]`] }); @@ -85,6 +86,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) req ); + const state = decodeOAuthState(req); + + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + res.redirect( getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/automation?hl=intercom`) ?? getInstalledAppPath({ variant: "automation", slug: "intercom" }) diff --git a/packages/app-store/intercom/config.json b/packages/app-store/intercom/config.json index 37c6831818..ef9a2955c2 100644 --- a/packages/app-store/intercom/config.json +++ b/packages/app-store/intercom/config.json @@ -13,5 +13,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "basic", - "dirName": "intercom" + "dirName": "intercom", + "isOAuth": true } diff --git a/packages/app-store/jitsivideo/_metadata.ts b/packages/app-store/jitsivideo/_metadata.ts index 6f6b042431..2a5b71700e 100644 --- a/packages/app-store/jitsivideo/_metadata.ts +++ b/packages/app-store/jitsivideo/_metadata.ts @@ -25,6 +25,7 @@ export const metadata = { }, dirName: "jitsivideo", concurrentMeetings: true, + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/larkcalendar/_metadata.ts b/packages/app-store/larkcalendar/_metadata.ts index 050e2ed7d8..ea00f1cde6 100644 --- a/packages/app-store/larkcalendar/_metadata.ts +++ b/packages/app-store/larkcalendar/_metadata.ts @@ -16,6 +16,7 @@ export const metadata = { url: "https://larksuite.com/", email: "alan@larksuite.com", dirName: "larkcalendar", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/make/config.json b/packages/app-store/make/config.json index bc2a1349da..9d51eab49c 100644 --- a/packages/app-store/make/config.json +++ b/packages/app-store/make/config.json @@ -14,5 +14,6 @@ "__createdUsingCli": true, "__template": "basic", "imageSrc": "icon.svg", - "dirName": "make" + "dirName": "make", + "isOAuth": false } diff --git a/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx b/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx index 38ee45f214..8bab69cac9 100644 --- a/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx @@ -2,13 +2,12 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( @@ -18,14 +17,12 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ switchOnClick={updateEnabled} switchChecked={enabled} teamId={eventType.team?.id || undefined}> - { - setAppData("trackingId", e.target.value); - }} + getAppData={getAppData} + setAppData={setAppData} /> ); diff --git a/packages/app-store/metapixel/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/metapixel/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..14b3103673 --- /dev/null +++ b/packages/app-store/metapixel/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,23 @@ +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { TextField } from "@calcom/ui"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, +}) => { + const trackingId = getAppData("trackingId"); + + return ( + { + setAppData("trackingId", e.target.value); + }} + /> + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/metapixel/config.json b/packages/app-store/metapixel/config.json index 7d055c3b49..a56804c0e8 100644 --- a/packages/app-store/metapixel/config.json +++ b/packages/app-store/metapixel/config.json @@ -21,5 +21,6 @@ } ] } - } + }, + "isOAuth": false } diff --git a/packages/app-store/mirotalk/config.json b/packages/app-store/mirotalk/config.json index c0cea23d8e..d1dbcf09fa 100644 --- a/packages/app-store/mirotalk/config.json +++ b/packages/app-store/mirotalk/config.json @@ -22,5 +22,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "event-type-location-video-static", - "dirName": "mirotalk" + "dirName": "mirotalk", + "isOAuth": false } diff --git a/packages/app-store/n8n/config.json b/packages/app-store/n8n/config.json index 1b6cec2091..2770acacc5 100644 --- a/packages/app-store/n8n/config.json +++ b/packages/app-store/n8n/config.json @@ -10,5 +10,6 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "description": "Automate without limits. The workflow automation platform that doesn't box you in, that you never outgrow", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/office365calendar/_metadata.ts b/packages/app-store/office365calendar/_metadata.ts index 6389322c54..98ded5de73 100644 --- a/packages/app-store/office365calendar/_metadata.ts +++ b/packages/app-store/office365calendar/_metadata.ts @@ -16,6 +16,7 @@ export const metadata = { dirName: "office365calendar", url: "https://cal.com/", email: "help@cal.com", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/office365video/api/callback.ts b/packages/app-store/office365video/api/callback.ts index fd2db4ae66..cfadcde47b 100644 --- a/packages/app-store/office365video/api/callback.ts +++ b/packages/app-store/office365video/api/callback.ts @@ -104,6 +104,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req); + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + return res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "conferencing", slug: "msteams" }) ); diff --git a/packages/app-store/office365video/config.json b/packages/app-store/office365video/config.json index 4880ce02e6..9b663ad63d 100644 --- a/packages/app-store/office365video/config.json +++ b/packages/app-store/office365video/config.json @@ -22,5 +22,6 @@ } }, "dirName": "office365video", - "concurrentMeetings": true + "concurrentMeetings": true, + "isOAuth": true } diff --git a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx index 02fcb22c5b..3dc6efbfe6 100644 --- a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx @@ -1,23 +1,15 @@ import { usePathname, useSearchParams } from "next/navigation"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; -import { - currencyOptions, - currencySymbols, - isAcceptedCurrencyCode, -} from "@calcom/app-store/paypal/lib/currencyOptions"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert, Select, TextField } from "@calcom/ui"; import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; import type { appDataSchema } from "../zod"; -import { PaypalPaymentOptions as paymentOptions } from "../zod"; - -type Option = { value: string; label: string }; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, @@ -31,40 +23,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ () => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`, [pathname, searchParams] ); - const { getAppData, setAppData } = useAppContextWithSchema(); - const price = getAppData("price"); - - const currency = getAppData("currency"); - const [selectedCurrency, setSelectedCurrency] = useState(currencyOptions.find((c) => c.value === currency)); - const [currencySymbol, setCurrencySymbol] = useState( - isAcceptedCurrencyCode(currency) ? currencySymbols[currency] : "" - ); - - const paymentOption = getAppData("paymentOption"); - const paymentOptionSelectValue = paymentOptions?.find((option) => paymentOption === option.value) || { - label: paymentOptions[0].label, - value: paymentOptions[0].value, - }; - const seatsEnabled = !!eventType.seatsPerTimeSlot; + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); - const { t } = useLocale(); - const recurringEventDefined = eventType.recurringEvent?.count !== undefined; const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); + const { t } = useLocale(); const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; - useEffect(() => { - if (requirePayment) { - if (!getAppData("currency")) { - setAppData("currency", currencyOptions[0].value); - } - if (!getAppData("paymentOption")) { - setAppData("paymentOption", paymentOptions[0].value); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( <> - {recurringEventDefined ? ( - - ) : ( - requirePayment && ( - <> -
- { - setAppData("price", Number(e.target.value) * 100); - if (selectedCurrency) { - setAppData("currency", selectedCurrency.value); - } - }} - value={price > 0 ? price / 100 : undefined} - /> -
-
- - { + if (e) { + setSelectedCurrency(e); + setCurrencySymbol(currencySymbols[e.value]); + setAppData("currency", e.value); + } + }} + /> +
+ +
+ + + data-testid="paypal-payment-option-select" + defaultValue={ + paymentOptionSelectValue + ? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) } + : { ...paymentOptions[0], label: t(paymentOptions[0].label) } + } + options={paymentOptions.map((option) => { + return { ...option, label: t(option.label) || option.label }; + })} + onChange={(input) => { + if (input) setAppData("paymentOption", input.value); + }} + className="mb-1 h-[38px] w-full" + isDisabled={seatsEnabled} + /> +
+ {seatsEnabled && paymentOption === "HOLD" && ( + + )} + + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/paypal/config.json b/packages/app-store/paypal/config.json index 5624fd3f39..86558342a0 100644 --- a/packages/app-store/paypal/config.json +++ b/packages/app-store/paypal/config.json @@ -14,5 +14,6 @@ "__createdUsingCli": true, "imageSrc": "icon.svg", "__template": "event-type-app-card", - "dirName": "paypal" + "dirName": "paypal", + "isOAuth": true } diff --git a/packages/app-store/ping/config.json b/packages/app-store/ping/config.json index 2559c47fb1..dadb6e2ba2 100644 --- a/packages/app-store/ping/config.json +++ b/packages/app-store/ping/config.json @@ -20,5 +20,6 @@ "organizerInputPlaceholder": "https://www.ping.gg/call/theo", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/pipedream/config.json b/packages/app-store/pipedream/config.json index 37fa14a103..7812bad8df 100644 --- a/packages/app-store/pipedream/config.json +++ b/packages/app-store/pipedream/config.json @@ -10,5 +10,6 @@ "publisher": "Pipedream, Inc.", "email": "support@pipedream.com", "description": "Connect APIs, remarkably fast. Stop writing boilerplate code, struggling with authentication and managing infrastructure. Start connecting APIs with code-level control when you need it — and no code when you don't", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx index 306d0be058..440f307ff8 100644 --- a/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx @@ -2,14 +2,13 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const plausibleUrl = getAppData("PLAUSIBLE_URL"); - const trackingId = getAppData("trackingId"); + const { enabled, updateEnabled } = useIsAppEnabled(app); return ( @@ -21,29 +20,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ }} switchChecked={enabled} teamId={eventType.team?.id || undefined}> -
- { - setAppData("PLAUSIBLE_URL", e.target.value); - }} - /> - { - setAppData("trackingId", e.target.value); - }} - /> -
+
); }; diff --git a/packages/app-store/plausible/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/plausible/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..bafa35679f --- /dev/null +++ b/packages/app-store/plausible/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,40 @@ +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { TextField } from "@calcom/ui"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, + slug, +}) => { + const plausibleUrl = getAppData("PLAUSIBLE_URL"); + const trackingId = getAppData("trackingId"); + + return ( +
+ { + setAppData("PLAUSIBLE_URL", e.target.value); + }} + /> + { + setAppData("trackingId", e.target.value); + }} + /> +
+ ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/plausible/config.json b/packages/app-store/plausible/config.json index 2fa2baee89..8d157e8814 100644 --- a/packages/app-store/plausible/config.json +++ b/packages/app-store/plausible/config.json @@ -23,5 +23,6 @@ } }, "description": "Simple, privacy-friendly Google Analytics alternative.", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx b/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx index acab0974a2..53df00b922 100644 --- a/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx @@ -1,39 +1,14 @@ -import { useState } from "react"; - import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Tooltip, TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) { - const { t } = useLocale(); - const { disabled } = useAppContextWithSchema(); - const [additionalParameters, setAdditionalParameters] = useState(""); const { enabled, updateEnabled } = useIsAppEnabled(app); - - const query = additionalParameters !== "" ? `?${additionalParameters}` : ""; - const eventTypeURL = eventType.URL + query; - - function QRCode({ size, data }: { size: number; data: string }) { - const QR_URL = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${data}`; - return ( - - - {eventTypeURL} - - - ); - } + const { disabled, getAppData, setAppData } = useAppContextWithSchema(); return ( -
-
- setAdditionalParameters(e.target.value)} - label={t("additional_url_parameters")} - containerClassName="w-full" - /> -
- -
- - - -
-
+
); }; diff --git a/packages/app-store/qr_code/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/qr_code/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 0000000000..cc069d32eb --- /dev/null +++ b/packages/app-store/qr_code/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; + +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { classNames } from "@calcom/lib"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { TextField, Tooltip } from "@calcom/ui"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ eventType, disabled }) => { + const { t } = useLocale(); + const [additionalParameters, setAdditionalParameters] = useState(""); + const query = additionalParameters !== "" ? `?${additionalParameters}` : ""; + const eventTypeURL = eventType.URL + query; + + function QRCode({ size, data }: { size: number; data: string }) { + const QR_URL = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${data}`; + return ( + + + = 256 && "min-h-32" + )} + style={{ padding: size / 16, borderRadius: size / 20 }} + width={size} + src={QR_URL} + alt={eventTypeURL} + /> + + + ); + } + return ( +
+
+ setAdditionalParameters(e.target.value)} + label={t("additional_url_parameters")} + containerClassName="w-full" + /> +
+ +
+ + + +
+
+ ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/qr_code/config.json b/packages/app-store/qr_code/config.json index 3597091bbd..25763202ba 100644 --- a/packages/app-store/qr_code/config.json +++ b/packages/app-store/qr_code/config.json @@ -11,5 +11,6 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "Easily generate a QR code for your links to print, share, or embed.", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/raycast/config.json b/packages/app-store/raycast/config.json index 12680a74c3..aac51930e6 100644 --- a/packages/app-store/raycast/config.json +++ b/packages/app-store/raycast/config.json @@ -10,5 +10,6 @@ "publisher": "Eric Luce", "email": "info@restlessmindstech.com", "description": "Quickly share your Cal.com meeting links with Raycast", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/riverside/config.json b/packages/app-store/riverside/config.json index 45788adfe7..947d2c3ecc 100644 --- a/packages/app-store/riverside/config.json +++ b/packages/app-store/riverside/config.json @@ -18,5 +18,6 @@ "type": "integrations:riverside_video", "linkType": "static" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/routing-forms/config.json b/packages/app-store/routing-forms/config.json index 55ad690527..22eacf8097 100644 --- a/packages/app-store/routing-forms/config.json +++ b/packages/app-store/routing-forms/config.json @@ -17,5 +17,6 @@ "upgradeUrl": "/routing-forms/forms" }, "description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/salesforce/api/callback.ts b/packages/app-store/salesforce/api/callback.ts index aad019a560..a764389404 100644 --- a/packages/app-store/salesforce/api/callback.ts +++ b/packages/app-store/salesforce/api/callback.ts @@ -42,6 +42,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, salesforceTokenInfo, req); const state = decodeOAuthState(req); + + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other", slug: "salesforce" }) ); diff --git a/packages/app-store/salesforce/config.json b/packages/app-store/salesforce/config.json index 3f95a3c9b0..d159830f94 100644 --- a/packages/app-store/salesforce/config.json +++ b/packages/app-store/salesforce/config.json @@ -10,5 +10,6 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": true } diff --git a/packages/app-store/sendgrid/config.json b/packages/app-store/sendgrid/config.json index 3f15a92402..d98cf3af04 100644 --- a/packages/app-store/sendgrid/config.json +++ b/packages/app-store/sendgrid/config.json @@ -10,5 +10,6 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/signal/config.json b/packages/app-store/signal/config.json index bb0e8f5a93..fe3bf5bd2f 100644 --- a/packages/app-store/signal/config.json +++ b/packages/app-store/signal/config.json @@ -19,5 +19,6 @@ "organizerInputPlaceholder": "https://signal.me/#p/+11234567890", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?signal.me\\/[a-zA-Z0-9]*" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/sirius_video/config.json b/packages/app-store/sirius_video/config.json index b44436beba..d8c02a2f65 100644 --- a/packages/app-store/sirius_video/config.json +++ b/packages/app-store/sirius_video/config.json @@ -19,5 +19,6 @@ "organizerInputPlaceholder": "https://sirius.video/sebastian", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?sirius.video\\/[a-zA-Z0-9]*" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/stripepayment/_metadata.ts b/packages/app-store/stripepayment/_metadata.ts index 26bd34023d..54f87d7210 100644 --- a/packages/app-store/stripepayment/_metadata.ts +++ b/packages/app-store/stripepayment/_metadata.ts @@ -23,6 +23,7 @@ export const metadata = { extendsFeature: "EventType", email: "help@cal.com", dirName: "stripepayment", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/stripepayment/api/callback.ts b/packages/app-store/stripepayment/api/callback.ts index 98e3b4ca87..61d16f7506 100644 --- a/packages/app-store/stripepayment/api/callback.ts +++ b/packages/app-store/stripepayment/api/callback.ts @@ -53,6 +53,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) req ); + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + const returnTo = getReturnToValueFromQueryState(req); res.redirect(returnTo || getInstalledAppPath({ variant: "payment", slug: "stripe" })); } diff --git a/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx index 886f626913..24f406e14c 100644 --- a/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx @@ -1,157 +1,48 @@ import { usePathname } from "next/navigation"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert, Select, TextField } from "@calcom/ui"; import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; -import { paymentOptions } from "../lib/constants"; -import { - convertToSmallestCurrencyUnit, - convertFromSmallestToPresentableCurrencyUnit, -} from "../lib/currencyConversions"; -import { currencyOptions } from "../lib/currencyOptions"; +import useIsAppEnabled from "../../_utils/useIsAppEnabled"; import type { appDataSchema } from "../zod"; - -type Option = { value: string; label: string }; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType, eventTypeFormMetadata, }) { + const { t } = useLocale(); const pathname = usePathname(); const { getAppData, setAppData, disabled } = useAppContextWithSchema(); - const price = getAppData("price"); - const currency = getAppData("currency") || currencyOptions[0].value; - const [selectedCurrency, setSelectedCurrency] = useState( - currencyOptions.find((c) => c.value === currency) || { - label: currencyOptions[0].label, - value: currencyOptions[0].value, - } - ); - const paymentOption = getAppData("paymentOption"); - const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value); - const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); + const { enabled, updateEnabled } = useIsAppEnabled(app); const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); - + const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; - const { t } = useLocale(); - const recurringEventDefined = eventType.recurringEvent?.count !== undefined; - const seatsEnabled = !!eventType.seatsPerTimeSlot; - const getCurrencySymbol = (locale: string, currency: string) => - (0) - .toLocaleString(locale, { - style: "currency", - currency, - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }) - .replace(/\d/g, "") - .trim(); - - useEffect(() => { - if (requirePayment) { - if (!getAppData("currency")) { - setAppData("currency", currencyOptions[0].value); - } - if (!getAppData("paymentOption")) { - setAppData("paymentOption", paymentOptions[0].value); - } - } - }, [requirePayment, getAppData, setAppData]); - return ( { - setRequirePayment(enabled); + switchChecked={enabled} + switchOnClick={(e) => { + updateEnabled(e); }} teamId={eventType.team?.id || undefined} disableSwitch={shouldDisableSwitch} switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> - <> - {recurringEventDefined && ( - - )} - {!recurringEventDefined && requirePayment && ( - <> -
- {selectedCurrency.value ? getCurrencySymbol("en", selectedCurrency.value) : ""} - } - addOnSuffix={currency.toUpperCase()} - addOnClassname="h-[38px]" - step="0.01" - min="0.5" - type="number" - required - placeholder="Price" - disabled={disabled} - onChange={(e) => { - setAppData("price", convertToSmallestCurrencyUnit(Number(e.target.value), currency)); - }} - value={price > 0 ? convertFromSmallestToPresentableCurrencyUnit(price, currency) : undefined} - /> -
-
- - { + if (e) { + setSelectedCurrency(e); + setAppData("currency", e.value); + } + }} + /> +
+
+ + + data-testid="stripe-payment-option-select" + defaultValue={ + paymentOptionSelectValue + ? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) } + : { ...paymentOptions[0], label: t(paymentOptions[0].label) } + } + options={paymentOptions.map((option) => { + return { ...option, label: t(option.label) || option.label }; + })} + onChange={(input) => { + if (input) setAppData("paymentOption", input.value); + }} + className="mb-1 h-[38px] w-full" + isDisabled={seatsEnabled || disabled} + /> +
+ + {seatsEnabled && paymentOption === "HOLD" && ( + + )} + + )} + + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/sylapsvideo/config.json b/packages/app-store/sylapsvideo/config.json index 424ba6ce6c..6ab5fe68ff 100644 --- a/packages/app-store/sylapsvideo/config.json +++ b/packages/app-store/sylapsvideo/config.json @@ -18,5 +18,6 @@ "type": "integrations:sylaps_video", "label": "Sylaps" } - } + }, + "isOAuth": false } diff --git a/packages/app-store/tandemvideo/_metadata.ts b/packages/app-store/tandemvideo/_metadata.ts index 28f9477129..6deef75e2d 100644 --- a/packages/app-store/tandemvideo/_metadata.ts +++ b/packages/app-store/tandemvideo/_metadata.ts @@ -24,6 +24,7 @@ export const metadata = { }, }, dirName: "tandemvideo", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/typeform/config.json b/packages/app-store/typeform/config.json index daf11d5a7d..ff0a14c376 100644 --- a/packages/app-store/typeform/config.json +++ b/packages/app-store/typeform/config.json @@ -10,5 +10,6 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "description": "Adds a link to copy Typeform Redirect URL", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/types.d.ts b/packages/app-store/types.d.ts index dfb22ab85b..da1fb01df9 100644 --- a/packages/app-store/types.d.ts +++ b/packages/app-store/types.d.ts @@ -10,6 +10,7 @@ export type IntegrationOAuthCallbackState = { onErrorReturnTo: string; fromApp: boolean; installGoogleVideo?: boolean; + appOnboardingRedirectUrl?: string; teamId?: number; }; @@ -56,4 +57,23 @@ export type EventTypeAppCardComponentProps = { LockedIcon?: JSX.Element | false; eventTypeFormMetadata?: z.infer; }; + +export type EventTypeAppSettingsComponentProps = { + // Limit what data should be accessible to apps\ + eventType: Pick< + z.infer, + "id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot" | "team" + > & { + URL: string; + }; + getAppData: GetAppData; + setAppData: SetAppData; + disabled?: boolean; + slug: string; +}; + export type EventTypeAppCardComponent = React.FC; + +export type EventTypeAppSettingsComponent = React.FC; + +export type EventTypeModel = z.infer; diff --git a/packages/app-store/vimcal/config.json b/packages/app-store/vimcal/config.json index f38f25ad8f..4baeb62de9 100644 --- a/packages/app-store/vimcal/config.json +++ b/packages/app-store/vimcal/config.json @@ -11,5 +11,6 @@ "email": "support@cal.com", "description": "The world's fastest calendar, beautifully designed for a remote world\r", "__createdUsingCli": true, - "dependencies": ["google-calendar"] + "dependencies": ["google-calendar"], + "isOAuth": false } diff --git a/packages/app-store/vital/_metadata.ts b/packages/app-store/vital/_metadata.ts index f0359e0b04..19d47d1527 100644 --- a/packages/app-store/vital/_metadata.ts +++ b/packages/app-store/vital/_metadata.ts @@ -18,6 +18,7 @@ export const metadata = { variant: "other", email: "support@tryvital.io", dirName: "vital", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/app-store/weather_in_your_calendar/config.json b/packages/app-store/weather_in_your_calendar/config.json index e7769ebd57..6193d26c1c 100644 --- a/packages/app-store/weather_in_your_calendar/config.json +++ b/packages/app-store/weather_in_your_calendar/config.json @@ -10,5 +10,6 @@ "publisher": "Andreas Vejnø Andersen", "email": "info@vejnoe.dk", "description": "Get the local weather forecast with icons in your calendar\r\r", - "__createdUsingCli": true + "__createdUsingCli": true, + "isOAuth": false } diff --git a/packages/app-store/webex/config.json b/packages/app-store/webex/config.json index 587fc561f5..f37f9b5ac2 100644 --- a/packages/app-store/webex/config.json +++ b/packages/app-store/webex/config.json @@ -22,5 +22,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "basic", - "concurrentMeetings": true + "concurrentMeetings": true, + "isAuth": true } diff --git a/packages/app-store/whatsapp/config.json b/packages/app-store/whatsapp/config.json index dde161c6ae..af42fc8ce5 100644 --- a/packages/app-store/whatsapp/config.json +++ b/packages/app-store/whatsapp/config.json @@ -19,5 +19,6 @@ "organizerInputPlaceholder": "https://wa.me/send?phone=1234567890", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?wa.me\\/[a-zA-Z0-9]*" } - } + }, + "isAuth": false } diff --git a/packages/app-store/whereby/config.json b/packages/app-store/whereby/config.json index 3844faa3f2..5bf0dcd9d6 100644 --- a/packages/app-store/whereby/config.json +++ b/packages/app-store/whereby/config.json @@ -20,5 +20,6 @@ "organizerInputPlaceholder": "https://www.whereby.com/cal", "urlRegExp": "^(?:https?://)?(?:(?!.*-\\.)(?:\\w+(-\\w+)*\\.))*whereby\\.com(/[\\w\\-._~:?#\\[\\]@!$&'()*+,;%=]+)*$" } - } + }, + "isAuth": false } diff --git a/packages/app-store/wipemycalother/_metadata.ts b/packages/app-store/wipemycalother/_metadata.ts index 7b1c1d9c4e..d7819dea08 100644 --- a/packages/app-store/wipemycalother/_metadata.ts +++ b/packages/app-store/wipemycalother/_metadata.ts @@ -18,6 +18,7 @@ export const metadata = { variant: "other", email: "help@cal.com", dirName: "wipemycalother", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/wordpress/config.json b/packages/app-store/wordpress/config.json index 260e7411a0..c29179fedf 100644 --- a/packages/app-store/wordpress/config.json +++ b/packages/app-store/wordpress/config.json @@ -10,5 +10,6 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "Embedded booking pages right into your wordpress page", - "__createdUsingCli": true + "__createdUsingCli": true, + "isAuth": false } diff --git a/packages/app-store/zapier/_metadata.ts b/packages/app-store/zapier/_metadata.ts index 26b50fb9d2..b8be473f70 100644 --- a/packages/app-store/zapier/_metadata.ts +++ b/packages/app-store/zapier/_metadata.ts @@ -17,6 +17,7 @@ export const metadata = { variant: "automation", email: "help@cal.com", dirName: "zapier", + isOAuth: false, } as AppMeta; export default metadata; diff --git a/packages/app-store/zoho-bigin/api/callback.ts b/packages/app-store/zoho-bigin/api/callback.ts index f1ab94a2a8..96a3ad3a05 100644 --- a/packages/app-store/zoho-bigin/api/callback.ts +++ b/packages/app-store/zoho-bigin/api/callback.ts @@ -55,6 +55,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, tokenInfo.data, req); const state = decodeOAuthState(req); + + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug }) diff --git a/packages/app-store/zoho-bigin/config.json b/packages/app-store/zoho-bigin/config.json index aea58ef2c7..d4a2851271 100644 --- a/packages/app-store/zoho-bigin/config.json +++ b/packages/app-store/zoho-bigin/config.json @@ -13,5 +13,6 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "basic", - "scope": "ZohoBigin.modules.events.ALL,ZohoBigin.modules.contacts.ALL" + "scope": "ZohoBigin.modules.events.ALL,ZohoBigin.modules.contacts.ALL", + "isOAuth": true } diff --git a/packages/app-store/zohocalendar/config.json b/packages/app-store/zohocalendar/config.json index 6d43249f52..5bdbb60829 100644 --- a/packages/app-store/zohocalendar/config.json +++ b/packages/app-store/zohocalendar/config.json @@ -10,5 +10,6 @@ "logo": "icon.svg", "publisher": "Cal.com", "url": "https://cal.com/", - "email": "help@cal.com" + "email": "help@cal.com", + "isAuth": true } diff --git a/packages/app-store/zohocrm/api/callback.ts b/packages/app-store/zohocrm/api/callback.ts index 9f02f28201..bcbed269fa 100644 --- a/packages/app-store/zohocrm/api/callback.ts +++ b/packages/app-store/zohocrm/api/callback.ts @@ -55,6 +55,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, zohoCrmTokenInfo.data, req); const state = decodeOAuthState(req); + + if (state?.appOnboardingRedirectUrl && state.appOnboardingRedirectUrl !== "") { + return res.redirect(state.appOnboardingRedirectUrl); + } + res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other", slug: "zohocrm" }) ); diff --git a/packages/app-store/zohocrm/config.json b/packages/app-store/zohocrm/config.json index daea3c1162..2a0cf837ca 100644 --- a/packages/app-store/zohocrm/config.json +++ b/packages/app-store/zohocrm/config.json @@ -12,5 +12,6 @@ "description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day", "isTemplate": false, "__createdUsingCli": true, - "__template": "basic" + "__template": "basic", + "isOAuth": true } diff --git a/packages/app-store/zoomvideo/_metadata.ts b/packages/app-store/zoomvideo/_metadata.ts index 665efb8446..4a65fe8480 100644 --- a/packages/app-store/zoomvideo/_metadata.ts +++ b/packages/app-store/zoomvideo/_metadata.ts @@ -25,6 +25,7 @@ export const metadata = { }, }, dirName: "zoomvideo", + isOAuth: true, } as AppMeta; export default metadata; diff --git a/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts b/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts index 1d6106a1ab..e84bf3187e 100644 --- a/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts +++ b/packages/embeds/embed-core/playwright/tests/action-based.e2e.ts @@ -1,8 +1,11 @@ import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; +// eslint-disable-next-line no-restricted-imports import { test } from "@calcom/web/playwright/lib/fixtures"; +// eslint-disable-next-line no-restricted-imports import type { Fixtures } from "@calcom/web/playwright/lib/fixtures"; +// eslint-disable-next-line no-restricted-imports import { selectFirstAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils"; import { diff --git a/packages/features/auth/SAMLLogin.tsx b/packages/features/auth/SAMLLogin.tsx index b0d9973d5f..2c74571f4f 100644 --- a/packages/features/auth/SAMLLogin.tsx +++ b/packages/features/auth/SAMLLogin.tsx @@ -6,7 +6,7 @@ import z from "zod"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { Button, Icon } from "@calcom/ui"; +import { Button } from "@calcom/ui"; interface Props { samlTenantID: string; diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx index ab4eeaef6f..642af0dac3 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx @@ -9,7 +9,6 @@ import { DialogClose, DialogContent, EmptyScreen, - Icon, ListItem, ListItemText, ListItemTitle, diff --git a/packages/features/calendars/weeklyview/components/heading/SchedulerHeading.tsx b/packages/features/calendars/weeklyview/components/heading/SchedulerHeading.tsx index 4ff86c79fe..17832275cc 100644 --- a/packages/features/calendars/weeklyview/components/heading/SchedulerHeading.tsx +++ b/packages/features/calendars/weeklyview/components/heading/SchedulerHeading.tsx @@ -1,5 +1,5 @@ import dayjs from "@calcom/dayjs"; -import { Button, ButtonGroup, Icon } from "@calcom/ui"; +import { Button, ButtonGroup } from "@calcom/ui"; import { useCalendarStore } from "../../state/store"; diff --git a/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx b/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx index c2ab1b305f..3230a003b7 100644 --- a/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx +++ b/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx @@ -6,17 +6,7 @@ import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem"; import LicenseRequired from "@calcom/ee/common/components/LicenseRequired"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { - Button, - DialogFooter, - Form, - Icon, - SelectField, - showToast, - Switch, - TextField, - Tooltip, -} from "@calcom/ui"; +import { Button, DialogFooter, Form, SelectField, showToast, Switch, TextField, Tooltip } from "@calcom/ui"; export default function ApiKeyDialogForm({ defaultValues, diff --git a/packages/features/ee/dsync/components/DirectoryInfo.tsx b/packages/features/ee/dsync/components/DirectoryInfo.tsx index 43f47653a3..005cbc412b 100644 --- a/packages/features/ee/dsync/components/DirectoryInfo.tsx +++ b/packages/features/ee/dsync/components/DirectoryInfo.tsx @@ -2,7 +2,6 @@ import type { Directory } from "@boxyhq/saml-jackson"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, showToast, Label, Tooltip } from "@calcom/ui"; -import { Icon } from "@calcom/ui"; const DirectoryInfo = ({ directory }: { directory: Directory }) => { const { t } = useLocale(); diff --git a/packages/features/ee/organizations/pages/components/MemberListItem.tsx b/packages/features/ee/organizations/pages/components/MemberListItem.tsx index f48e62fa64..94158465be 100644 --- a/packages/features/ee/organizations/pages/components/MemberListItem.tsx +++ b/packages/features/ee/organizations/pages/components/MemberListItem.tsx @@ -11,7 +11,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - Icon, Tooltip, UserAvatar, } from "@calcom/ui"; @@ -82,12 +81,7 @@ export default function MemberListItem(props: Props) {
-
- - {t("preview")} - + {t("preview")} { - diff --git a/packages/features/ee/organizations/pages/settings/profile.tsx b/packages/features/ee/organizations/pages/settings/profile.tsx index bce21646ac..9dfe272be3 100644 --- a/packages/features/ee/organizations/pages/settings/profile.tsx +++ b/packages/features/ee/organizations/pages/settings/profile.tsx @@ -22,7 +22,6 @@ import { Button, Editor, Form, - Icon, ImageUploader, Label, LinkIconButton, diff --git a/packages/features/ee/teams/components/AddNewTeamMembers.tsx b/packages/features/ee/teams/components/AddNewTeamMembers.tsx index 7b7eac226e..26603456ab 100644 --- a/packages/features/ee/teams/components/AddNewTeamMembers.tsx +++ b/packages/features/ee/teams/components/AddNewTeamMembers.tsx @@ -16,7 +16,6 @@ import { trpc } from "@calcom/trpc/react"; import { Badge, Button, - Icon, showToast, SkeletonButton, SkeletonContainer, diff --git a/packages/features/ee/teams/pages/team-members-view.tsx b/packages/features/ee/teams/pages/team-members-view.tsx index d534118eb2..576fcf2981 100644 --- a/packages/features/ee/teams/pages/team-members-view.tsx +++ b/packages/features/ee/teams/pages/team-members-view.tsx @@ -11,7 +11,6 @@ import { MembershipRole } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { Button, Meta, showToast, TextField } from "@calcom/ui"; -import { Icon } from "@calcom/ui"; import { getLayout } from "../../../settings/layouts/SettingsLayout"; import DisableTeamImpersonation from "../components/DisableTeamImpersonation"; diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 5f67d3e343..0299b8ba2e 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -252,12 +252,7 @@ export default function WorkflowListPage({ workflows }: Props) {
- } diff --git a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx index 97bf068eac..2e5b04e6bd 100644 --- a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx +++ b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx @@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { MembershipRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc"; import type { UserProfile } from "@calcom/types/UserProfile"; -import { Button, ButtonGroup, DataTable, Icon, UserAvatar } from "@calcom/ui"; +import { Button, ButtonGroup, DataTable, UserAvatar } from "@calcom/ui"; import { UpgradeTip } from "../../tips/UpgradeTip"; import { createTimezoneBuddyStore, TBContext } from "../store"; diff --git a/packages/features/troubleshooter/components/TroubleshooterHeader.tsx b/packages/features/troubleshooter/components/TroubleshooterHeader.tsx index 063f74cbe9..a781f0495e 100644 --- a/packages/features/troubleshooter/components/TroubleshooterHeader.tsx +++ b/packages/features/troubleshooter/components/TroubleshooterHeader.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Button, ButtonGroup, Icon } from "@calcom/ui"; +import { Button, ButtonGroup } from "@calcom/ui"; import { useTroubleshooterStore } from "../store"; diff --git a/packages/features/webhooks/components/WebhookTestDisclosure.tsx b/packages/features/webhooks/components/WebhookTestDisclosure.tsx index e8b448a969..5bf6d048d0 100644 --- a/packages/features/webhooks/components/WebhookTestDisclosure.tsx +++ b/packages/features/webhooks/components/WebhookTestDisclosure.tsx @@ -4,7 +4,7 @@ import { ZodError } from "zod"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { ZTestTriggerInputSchema } from "@calcom/trpc/server/routers/viewer/webhook/testTrigger.schema"; -import { Badge, Button, Icon, showToast } from "@calcom/ui"; +import { Badge, Button, showToast } from "@calcom/ui"; export default function WebhookTestDisclosure() { const [subscriberUrl, webhookSecret]: [string, string] = useWatch({ name: ["subscriberUrl", "secret"] }); diff --git a/packages/lib/apps/appOnboardingSteps.ts b/packages/lib/apps/appOnboardingSteps.ts new file mode 100644 index 0000000000..e6117b9296 --- /dev/null +++ b/packages/lib/apps/appOnboardingSteps.ts @@ -0,0 +1,5 @@ +export enum AppOnboardingSteps { + ACCOUNTS_STEP = "accounts", + EVENT_TYPES_STEP = "event-types", + CONFIGURE_STEP = "configure", +} diff --git a/packages/lib/apps/getAppOnboardingRedirectUrl.ts b/packages/lib/apps/getAppOnboardingRedirectUrl.ts new file mode 100644 index 0000000000..8a7bb37630 --- /dev/null +++ b/packages/lib/apps/getAppOnboardingRedirectUrl.ts @@ -0,0 +1,8 @@ +import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"; + +import { getAppOnboardingUrl } from "./getAppOnboardingUrl"; + +export const getAppOnboardingRedirectUrl = (slug: string, teamId?: number) => { + const url = getAppOnboardingUrl({ slug, teamId, step: AppOnboardingSteps.EVENT_TYPES_STEP }); + return encodeURIComponent(url); +}; diff --git a/packages/lib/apps/getAppOnboardingUrl.ts b/packages/lib/apps/getAppOnboardingUrl.ts new file mode 100644 index 0000000000..fda259dfe7 --- /dev/null +++ b/packages/lib/apps/getAppOnboardingUrl.ts @@ -0,0 +1,27 @@ +import { stringify } from "querystring"; + +import type { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"; + +export const getAppOnboardingUrl = ({ + slug, + step, + teamId, + eventTypeIds, +}: { + slug: string; + step: AppOnboardingSteps; + teamId?: number; + eventTypeIds?: number[]; +}) => { + const params: { [key: string]: string | number | number[] } = { slug }; + if (!!eventTypeIds && eventTypeIds.length > 0) { + params.eventTypeIds = eventTypeIds.join(","); + } + + if (!!teamId) { + params.teamId = teamId; + } + const query = stringify(params); + + return `/apps/installation/${step}?${query}`; +}; diff --git a/packages/lib/apps/shouldRedirectToAppOnboarding.ts b/packages/lib/apps/shouldRedirectToAppOnboarding.ts new file mode 100644 index 0000000000..868167e877 --- /dev/null +++ b/packages/lib/apps/shouldRedirectToAppOnboarding.ts @@ -0,0 +1,9 @@ +import type { AppMeta } from "@calcom/types/App"; + +export const shouldRedirectToAppOnboarding = (appMetadata: AppMeta) => { + // const appMetadata = appStoreMetadata[dirName as keyof typeof appStoreMetadata]; + const hasEventTypes = appMetadata?.extendsFeature == "EventType"; + const isOAuth = appMetadata?.isOAuth; + const isCalendar = appMetadata?.category === "calendar"; + return !isCalendar && (hasEventTypes || isOAuth); +}; diff --git a/packages/platform/atoms/availability/AvailabilitySettings.tsx b/packages/platform/atoms/availability/AvailabilitySettings.tsx index 20f39f5626..cfdcaab105 100644 --- a/packages/platform/atoms/availability/AvailabilitySettings.tsx +++ b/packages/platform/atoms/availability/AvailabilitySettings.tsx @@ -178,10 +178,7 @@ const DateOverride = ({ onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))} userTimeFormat={userTimeFormat} Trigger={ - } @@ -324,11 +321,7 @@ export function AvailabilitySettings({ openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0" )}>
- + ); + } return ( diff --git a/packages/ui/components/form/step/Steps.tsx b/packages/ui/components/form/step/Steps.tsx index 3c52946628..fb042d69a1 100644 --- a/packages/ui/components/form/step/Steps.tsx +++ b/packages/ui/components/form/step/Steps.tsx @@ -1,17 +1,30 @@ import classNames from "@calcom/lib/classNames"; -interface ISteps { +type StepWithNav = { maxSteps: number; currentStep: number; navigateToStep: (step: number) => void; + disableNavigation?: false; stepLabel?: (currentStep: number, maxSteps: number) => string; -} +}; -const Steps = (props: ISteps) => { +type StepWithoutNav = { + maxSteps: number; + currentStep: number; + navigateToStep?: undefined; + disableNavigation: true; + stepLabel?: (currentStep: number, maxSteps: number) => string; +}; + +// Discriminative union on disableNavigation prop +type StepsProps = StepWithNav | StepWithoutNav; + +const Steps = (props: StepsProps) => { const { maxSteps, currentStep, navigateToStep, + disableNavigation = false, stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`, } = props; return ( @@ -22,10 +35,10 @@ const Steps = (props: ISteps) => { return index <= currentStep - 1 ? (
navigateToStep(index)} + onClick={() => navigateToStep?.(index)} className={classNames( "bg-inverted h-1 w-full rounded-[1px]", - index < currentStep - 1 ? "cursor-pointer" : "" + index < currentStep - 1 && !disableNavigation ? "cursor-pointer" : "" )} data-testid={`step-indicator-${index}`} /> diff --git a/packages/ui/components/scrollable/ScrollableArea.tsx b/packages/ui/components/scrollable/ScrollableArea.tsx index 1e1306e2bb..1583c046f5 100644 --- a/packages/ui/components/scrollable/ScrollableArea.tsx +++ b/packages/ui/components/scrollable/ScrollableArea.tsx @@ -35,7 +35,7 @@ const ScrollableArea = ({ children, className }: PropsWithChildren<{ className?: className // Pass in your max-w / max-h )}> {children} - {isOverflowingY &&
} + {isOverflowingY &&
}
); };