146 files changed
+698
-2151
No files matched your search
@@ -23,6 +23,6 @@
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
},
|
||||
"exclude": ["./dist"],
|
||||
"include": ["./**/*.ts", "../../../packages/types/*.d.ts", "next-i18next.config.js"]
|
||||
"exclude": ["./dist", "next-i18next.config.js"],
|
||||
"include": ["./**/*.ts", "../../../packages/types/*.d.ts"]
|
||||
}
|
||||
@@ -21,7 +21,6 @@ export type AppPageProps = {
|
||||
isGlobal?: AppType["isGlobal"];
|
||||
logo: string;
|
||||
slug: string;
|
||||
dirName: string | undefined;
|
||||
variant: string;
|
||||
body: React.ReactNode;
|
||||
categories: string[];
|
||||
@@ -69,7 +68,6 @@ export const AppPage = ({
|
||||
dependencies,
|
||||
concurrentMeetings,
|
||||
paid,
|
||||
dirName,
|
||||
}: AppPageProps) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||
@@ -225,7 +223,6 @@ export const AppPage = ({
|
||||
multiInstall
|
||||
concurrentMeetings={concurrentMeetings}
|
||||
paid={paid}
|
||||
dirName={dirName}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -265,7 +262,6 @@ export const AppPage = ({
|
||||
credentials={appDbQuery.data?.credentials}
|
||||
concurrentMeetings={concurrentMeetings}
|
||||
paid={paid}
|
||||
dirName={dirName}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
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";
|
||||
@@ -33,9 +26,7 @@ export const InstallAppButtonChild = ({
|
||||
multiInstall,
|
||||
credentials,
|
||||
concurrentMeetings,
|
||||
dirName,
|
||||
paid,
|
||||
onClick,
|
||||
...props
|
||||
}: {
|
||||
userAdminTeams?: UserAdminTeams;
|
||||
@@ -45,10 +36,8 @@ 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) => {
|
||||
@@ -60,18 +49,6 @@ 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<HTMLElement, 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
|
||||
@@ -79,7 +56,6 @@ export const InstallAppButtonChild = ({
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
onClick={_onClick}
|
||||
{...props}
|
||||
disabled={shouldDisableInstallation}
|
||||
color="primary"
|
||||
@@ -96,7 +72,6 @@ export const InstallAppButtonChild = ({
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
onClick={_onClick}
|
||||
{...props}
|
||||
// @TODO: Overriding color and size prevent us from
|
||||
// having to duplicate InstallAppButton for now.
|
||||
@@ -108,19 +83,6 @@ export const InstallAppButtonChild = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (redirectToAppOnboarding) {
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
disabled={shouldDisableInstallation}
|
||||
onClick={_onClick}
|
||||
color="primary"
|
||||
size="base"
|
||||
{...props}>
|
||||
{multiInstall ? t("install_another") : t("install_app")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
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<AccountSelectorProps> = ({
|
||||
avatar,
|
||||
alreadyInstalled,
|
||||
name,
|
||||
onClick,
|
||||
loading,
|
||||
testId,
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [selected, setSelected] = useState(false);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"hover:bg-muted flex cursor-pointer flex-row items-center gap-2 p-1",
|
||||
(alreadyInstalled || loading) && "cursor-not-allowed",
|
||||
selected && "bg-muted animate-pulse"
|
||||
)}
|
||||
data-testid={testId}
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
setSelected(true);
|
||||
onClick();
|
||||
}
|
||||
}}>
|
||||
<Avatar
|
||||
alt={avatar || ""}
|
||||
imageSrc={avatar || `${CAL_URL}/${avatar}`} // if no image, use default avatar
|
||||
size="sm"
|
||||
/>
|
||||
<div className="text-md pt-0.5 font-medium text-gray-500">
|
||||
{name}
|
||||
{alreadyInstalled ? <span className="ml-1 text-sm text-gray-400">{t("already_installed")}</span> : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type PersonalAccountProps = Pick<User, "id" | "avatar" | "name"> & { alreadyInstalled: boolean };
|
||||
|
||||
export type TeamsProp = (Pick<Team, "id" | "name" | "logo"> & {
|
||||
alreadyInstalled: boolean;
|
||||
})[];
|
||||
|
||||
type AccountStepCardProps = {
|
||||
teams: TeamsProp;
|
||||
personalAccount: PersonalAccountProps;
|
||||
onSelect: (id?: number) => void;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const AccountsStepCard: FC<AccountStepCardProps> = ({ teams, personalAccount, onSelect, loading }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<StepCard>
|
||||
<div className="text-sm font-medium text-gray-400">{t("install_app_on")}</div>
|
||||
<div className={classNames("mt-2 flex flex-col gap-2 ")}>
|
||||
<AccountSelector
|
||||
testId="install-app-on-personal-account"
|
||||
avatar={personalAccount.avatar ?? ""}
|
||||
name={personalAccount.name ?? ""}
|
||||
alreadyInstalled={personalAccount.alreadyInstalled}
|
||||
onClick={() => !personalAccount.alreadyInstalled && !loading && onSelect()}
|
||||
loading={loading}
|
||||
/>
|
||||
{teams.map((team) => (
|
||||
<AccountSelector
|
||||
key={team.id}
|
||||
testId={`install-app-on-team-${team.id}`}
|
||||
alreadyInstalled={team.alreadyInstalled}
|
||||
avatar={team.logo ?? ""}
|
||||
name={team.name}
|
||||
onClick={() => !team.alreadyInstalled && !loading && onSelect(team.id)}
|
||||
loading={loading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</StepCard>
|
||||
);
|
||||
};
|
||||
@@ -1,206 +0,0 @@
|
||||
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<typeof EventTypeMetaDataSchema>;
|
||||
};
|
||||
|
||||
type ConfigureStepCardProps = {
|
||||
slug: string;
|
||||
userName: string;
|
||||
categories: AppCategories[];
|
||||
credentialId?: number;
|
||||
loading?: boolean;
|
||||
formPortalRef: React.RefObject<HTMLDivElement>;
|
||||
eventTypes: TEventType[] | undefined;
|
||||
setConfigureStep: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
type EventTypeAppSettingsFormProps = Pick<
|
||||
ConfigureStepCardProps,
|
||||
"slug" | "userName" | "categories" | "credentialId" | "loading"
|
||||
> & {
|
||||
eventType: TEventType;
|
||||
handleDelete: () => void;
|
||||
onSubmit: (values: z.infer<typeof EventTypeMetaDataSchema>) => void;
|
||||
};
|
||||
|
||||
type EventTypeAppSettingsWrapperProps = Pick<
|
||||
ConfigureStepCardProps,
|
||||
"slug" | "userName" | "categories" | "credentialId"
|
||||
> & {
|
||||
eventType: TEventType;
|
||||
};
|
||||
|
||||
const EventTypeAppSettingsWrapper: FC<EventTypeAppSettingsWrapperProps> = ({
|
||||
slug,
|
||||
eventType,
|
||||
categories,
|
||||
credentialId,
|
||||
}) => {
|
||||
const { getAppDataGetter, getAppDataSetter } = useAppsData();
|
||||
|
||||
useEffect(() => {
|
||||
const appDataSetter = getAppDataSetter(slug as EventTypeAppsList, categories, credentialId);
|
||||
appDataSetter("enabled", true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EventTypeAppSettings
|
||||
slug={slug}
|
||||
eventType={eventType}
|
||||
getAppData={getAppDataGetter(slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(slug as EventTypeAppsList, categories, credentialId)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypeAppSettingsForm = forwardRef<HTMLButtonElement, EventTypeAppSettingsFormProps>(
|
||||
function EventTypeAppSettingsForm(props, ref) {
|
||||
const { handleDelete, onSubmit, eventType, loading } = props;
|
||||
|
||||
const formMethods = useForm<TFormType>({
|
||||
defaultValues: {
|
||||
metadata: eventType?.metadata,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formMethods}
|
||||
id={`eventtype-${eventType.id}`}
|
||||
handleSubmit={() => {
|
||||
const data = formMethods.getValues("metadata");
|
||||
onSubmit(data);
|
||||
}}>
|
||||
<div>
|
||||
<div className="sm:border-subtle bg-default relative border p-4 dark:bg-black sm:rounded-md">
|
||||
<div>
|
||||
<span className="text-default font-semibold ltr:mr-1 rtl:ml-1">{eventType.title}</span>{" "}
|
||||
<small className="text-subtle hidden font-normal sm:inline">
|
||||
/{eventType.team ? eventType.team.slug : props.userName}/{eventType.slug}
|
||||
</small>
|
||||
</div>
|
||||
<EventTypeAppSettingsWrapper {...props} />
|
||||
<X
|
||||
className="absolute right-4 top-4 h-4 w-4 cursor-pointer"
|
||||
onClick={() => !loading && handleDelete()}
|
||||
/>
|
||||
<button type="submit" className="hidden" ref={ref}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const ConfigureStepCard: FC<ConfigureStepCardProps> = ({
|
||||
loading,
|
||||
formPortalRef,
|
||||
eventTypes,
|
||||
setConfigureStep,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { control, getValues } = useFormContext<TEventTypesForm>();
|
||||
const { fields, update } = useFieldArray({
|
||||
control,
|
||||
name: "eventTypes",
|
||||
keyName: "fieldId",
|
||||
});
|
||||
|
||||
const submitRefs = useRef<Array<React.RefObject<HTMLButtonElement>>>([]);
|
||||
submitRefs.current = fields.map(
|
||||
(_ref, index) => (submitRefs.current[index] = React.createRef<HTMLButtonElement>())
|
||||
);
|
||||
const mainForSubmitRef = useRef<HTMLButtonElement>(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(
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
field.selected && (
|
||||
<EventTypeAppSettingsForm
|
||||
key={field.fieldId}
|
||||
eventType={field}
|
||||
loading={loading}
|
||||
handleDelete={() => {
|
||||
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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
|
||||
Save
|
||||
</button>
|
||||
<Button
|
||||
className="text-md mt-6 w-full justify-center"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
submitRefs.current.reverse().map((ref) => ref.current?.click());
|
||||
setSubmit(true);
|
||||
}}
|
||||
loading={loading}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>,
|
||||
formPortalRef?.current
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
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<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const EventTypesStepCard: FC<EventTypesCardProps> = ({ setConfigureStep, userName }) => {
|
||||
const { t } = useLocale();
|
||||
const { control } = useFormContext<TEventTypesForm>();
|
||||
const { fields, update } = useFieldArray({
|
||||
control,
|
||||
name: "eventTypes",
|
||||
keyName: "fieldId",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sm:border-subtle bg-default mt-10 border dark:bg-black sm:rounded-md">
|
||||
<ScrollableArea className="rounded-md">
|
||||
<ul className="border-subtle max-h-97 !static w-full divide-y">
|
||||
{fields.map((field, index) => (
|
||||
<EventTypeCard
|
||||
handleSelect={() => update(index, { ...field, selected: !field.selected })}
|
||||
userName={userName}
|
||||
key={field.fieldId}
|
||||
{...field}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="text-md mt-6 w-full justify-center"
|
||||
onClick={() => {
|
||||
setConfigureStep(true);
|
||||
}}
|
||||
disabled={!fields.some((field) => field.selected === true)}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type EventTypeCardProps = TEventType & { userName: string; handleSelect: () => void };
|
||||
|
||||
const EventTypeCard: FC<EventTypeCardProps> = ({
|
||||
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 (
|
||||
<div
|
||||
className="hover:bg-muted min-h-20 box-border flex w-full cursor-pointer select-none items-center space-x-4 px-4 py-3"
|
||||
onClick={() => handleSelect()}>
|
||||
<input
|
||||
id={`${id}`}
|
||||
checked={selected}
|
||||
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label htmlFor={`${id}`} className="cursor-pointer text-sm">
|
||||
<li>
|
||||
<div>
|
||||
<span className="text-default font-semibold ltr:mr-1 rtl:ml-1">{title}</span>{" "}
|
||||
<small className="text-subtle hidden font-normal sm:inline">
|
||||
/{team ? team.slug : userName}/{slug}
|
||||
</small>
|
||||
</div>
|
||||
{Boolean(description) && (
|
||||
<div className="text-subtle line-clamp-4 break-words text-sm sm:max-w-[650px] [&>*:not(:first-child)]:hidden [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex flex-row flex-wrap gap-2">
|
||||
{Boolean(durations.length) &&
|
||||
durations.map((duration) => (
|
||||
<Badge key={`event-type-${id}-duration-${duration}`} variant="gray" startIcon="clock">
|
||||
{duration}m
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
type StepHeaderProps = {
|
||||
children?: ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
export const StepHeader: FC<StepHeaderProps> = ({ children, title, subtitle }) => {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<p className="font-cal mb-3 text-[28px] font-medium capitalize leading-7">{title}</p>
|
||||
|
||||
<p className="text-subtle font-sans text-sm font-normal">{subtitle}</p>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
@@ -12,8 +13,6 @@ 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<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
EventTypeAppCardComponentProps["eventType"];
|
||||
|
||||
@@ -29,8 +28,51 @@ 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 { getAppDataGetter, getAppDataSetter, eventTypeFormMetadata } = useAppsData();
|
||||
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 { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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<FormValues>();
|
||||
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;
|
||||
@@ -53,7 +53,6 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
|
||||
slug={data.slug}
|
||||
variant={data.variant}
|
||||
type={data.type}
|
||||
dirName={data.dirName}
|
||||
logo={data.logo}
|
||||
categories={data.categories ?? [data.category]}
|
||||
author={data.publisher}
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
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<EventTypeModel, "metadata" | "schedulingType" | "slug" | "requiresConfirmation" | "position"> & {
|
||||
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<HTMLDivElement>(null);
|
||||
|
||||
const formMethods = useForm<TEventTypesForm>({
|
||||
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 (
|
||||
<div
|
||||
key={pathname}
|
||||
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen px-4"
|
||||
data-testid="onboarding">
|
||||
<Head>
|
||||
<title>
|
||||
{t("install")} {appMetadata?.name ?? ""}
|
||||
</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="mx-auto py-6 sm:px-4 md:py-24">
|
||||
<div className="relative">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]" ref={formPortalRef}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="outer-event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
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<typeof EventTypeMetaDataSchema>)
|
||||
)
|
||||
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);
|
||||
}}>
|
||||
<StepHeader
|
||||
title={stepObj.getTitle(appMetadata.name)}
|
||||
subtitle={stepObj.getDescription(appMetadata.name)}>
|
||||
<Steps maxSteps={MAX_NUMBER_OF_STEPS} currentStep={stepObj.stepNumber} disableNavigation />
|
||||
</StepHeader>
|
||||
{currentStep === AppOnboardingSteps.ACCOUNTS_STEP && (
|
||||
<AccountsStepCard
|
||||
teams={teams}
|
||||
personalAccount={personalAccount}
|
||||
onSelect={handleSelectAccount}
|
||||
loading={isSelectingAccount}
|
||||
/>
|
||||
)}
|
||||
{currentStep === AppOnboardingSteps.EVENT_TYPES_STEP &&
|
||||
eventTypes &&
|
||||
Boolean(eventTypes?.length) && (
|
||||
<EventTypesStepCard setConfigureStep={setConfigureStep} userName={userName} />
|
||||
)}
|
||||
{currentStep === AppOnboardingSteps.CONFIGURE_STEP && formPortalRef.current && (
|
||||
<ConfigureStepCard
|
||||
slug={appMetadata.slug}
|
||||
categories={appMetadata.categories}
|
||||
credentialId={credentialId}
|
||||
userName={userName}
|
||||
loading={updateMutation.isPending}
|
||||
formPortalRef={formPortalRef}
|
||||
setConfigureStep={setConfigureStep}
|
||||
eventTypes={eventTypes}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -1,8 +1,5 @@
|
||||
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) => {
|
||||
@@ -12,13 +9,6 @@ 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();
|
||||
|
||||
@@ -912,11 +912,14 @@ export async function getPaymentCredential(page: Page) {
|
||||
await page.goto("/apps/stripe");
|
||||
|
||||
/** We start the Stripe flow */
|
||||
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");
|
||||
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"]'),
|
||||
]);
|
||||
}
|
||||
@@ -92,13 +92,18 @@ test.describe("Stripe integration", () => {
|
||||
await page.goto("/apps/stripe");
|
||||
|
||||
/** We start the Stripe flow */
|
||||
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 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 owner.setupEventWithPrice(teamEvent, "stripe");
|
||||
|
||||
// Need to wait for the DB to be updated with the metadata
|
||||
|
||||
@@ -2371,13 +2371,6 @@
|
||||
"disconnect_account_hint": "Disconnecting your connected account will change the way you log in. You will only be able to login to your account using email + password",
|
||||
"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",
|
||||
|
||||
@@ -328,14 +328,6 @@ 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\`.
|
||||
|
||||
@@ -8,7 +8,7 @@ export function DynamicComponent<T extends Record<string, React.ComponentType<an
|
||||
const dirName = slug === "stripe" ? "stripepayment" : slug;
|
||||
|
||||
// There can be apps with no matching component
|
||||
if (!componentMap[dirName]) return null;
|
||||
if (!componentMap[slug]) return null;
|
||||
|
||||
const Component = componentMap[dirName];
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { EventTypeSettingsMap } from "@calcom/app-store/apps.browser.generated";
|
||||
|
||||
import type { EventTypeAppSettingsComponentProps } from "../types";
|
||||
import { DynamicComponent } from "./DynamicComponent";
|
||||
|
||||
export const EventTypeAppSettings = (props: EventTypeAppSettingsComponentProps) => {
|
||||
const { slug, ...rest } = props;
|
||||
return <DynamicComponent slug={slug} componentMap={EventTypeSettingsMap} {...rest} />;
|
||||
};
|
||||
@@ -7,9 +7,6 @@ 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;
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useEffect, 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 EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface";
|
||||
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
app,
|
||||
@@ -17,19 +22,38 @@ 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, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
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 [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 (
|
||||
<AppCard
|
||||
returnTo={WEBAPP_URL + asPath}
|
||||
@@ -41,13 +65,77 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
description={<>Add bitcoin lightning payments to your events</>}
|
||||
disableSwitch={shouldDisableSwitch}
|
||||
switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
<>
|
||||
{recurringEventDefined ? (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
) : (
|
||||
requirePayment && (
|
||||
<>
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<TextField
|
||||
label="Price"
|
||||
labelSrOnly
|
||||
addOnLeading={<SatSymbol className="h-4 w-4" />}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
variant="default"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 w-60">
|
||||
<label className="text-default block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { currencyOptions } from "@calcom/app-store/alby/lib/currencyOptions";
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Select, TextField } from "@calcom/ui";
|
||||
import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol";
|
||||
|
||||
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
eventType,
|
||||
getAppData,
|
||||
setAppData,
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
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 [requirePayment] = useState(getAppData("enabled"));
|
||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||
|
||||
// make sure a currency is selected
|
||||
useEffect(() => {
|
||||
if (!currency && requirePayment) {
|
||||
setAppData("currency", selectedCurrency.value);
|
||||
}
|
||||
}, [currency, selectedCurrency, setAppData, requirePayment]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{recurringEventDefined ? (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
) : (
|
||||
requirePayment && (
|
||||
<>
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<TextField
|
||||
label="Price"
|
||||
labelSrOnly
|
||||
addOnLeading={<SatSymbol className="h-4 w-4" />}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
variant="default"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 w-60">
|
||||
<label className="text-default block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -14,6 +14,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-app-card",
|
||||
"dirName": "alby",
|
||||
"isOAuth": false
|
||||
"dirName": "alby"
|
||||
}
|
||||
@@ -12,6 +12,5 @@
|
||||
"description": "The joyful productivity app\r\r",
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"],
|
||||
"dirName": "amie",
|
||||
"isOAuth": false
|
||||
"dirName": "amie"
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "applecalendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -41,16 +41,3 @@ 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")),
|
||||
};
|
||||
@@ -20,6 +20,5 @@
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?around.co\\/[a-zA-Z0-9]*",
|
||||
"organizerInputPlaceholder": "https://www.around.co/rick"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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) {
|
||||
@@ -87,11 +86,5 @@ 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 }));
|
||||
}
|
||||
@@ -1,29 +1,76 @@
|
||||
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, eventType }) {
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
||||
const { getAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
||||
const [projects, setProjects] = useState();
|
||||
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
app={app}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
if (!e) {
|
||||
setEnabled(false);
|
||||
} else {
|
||||
setEnabled(true);
|
||||
}
|
||||
}}
|
||||
switchChecked={enabled}>
|
||||
<EventTypeAppSettingsInterface
|
||||
slug={app.slug}
|
||||
eventType={eventType}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
<div className="mt-2 text-sm">
|
||||
<div className="flex gap-3">
|
||||
<div className="items-center">
|
||||
<p className="py-2">Link a Basecamp project to this event:</p>
|
||||
</div>
|
||||
<Select
|
||||
placeholder="Select project"
|
||||
options={projects}
|
||||
isLoading={!projects}
|
||||
className="md:min-w-[120px]"
|
||||
onChange={(project) => {
|
||||
if (project) {
|
||||
setProject.mutate({ projectId: project?.value.toString() });
|
||||
setSelectedProject(project);
|
||||
}
|
||||
}}
|
||||
value={selectedProject}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
Please note that as of now you can only link <span className="italic">one</span> of your projects to
|
||||
cal.com
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Select } from "@calcom/ui";
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({}) => {
|
||||
const [projects, setProjects] = useState();
|
||||
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-2 text-sm">
|
||||
<div className="flex gap-3">
|
||||
<div className="items-center">
|
||||
<p className="py-2">Link a Basecamp project to this event:</p>
|
||||
</div>
|
||||
<Select
|
||||
placeholder="Select project"
|
||||
options={projects}
|
||||
isLoading={!projects}
|
||||
className="md:min-w-[120px]"
|
||||
onChange={(project) => {
|
||||
if (project) {
|
||||
setProject.mutate({ projectId: project?.value.toString() });
|
||||
setSelectedProject(project);
|
||||
}
|
||||
}}
|
||||
value={selectedProject}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
Please note that as of now you can only link <span className="italic">one</span> of your projects to
|
||||
cal.com
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -13,6 +13,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-app-card",
|
||||
"dirName": "basecamp3",
|
||||
"isOAuth": true
|
||||
"dirName": "basecamp3"
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "cal-ai",
|
||||
"isOAuth": false,
|
||||
"paid": {
|
||||
"priceInUsd": 8,
|
||||
"priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP",
|
||||
|
||||
@@ -17,7 +17,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
email: "ali@cal.com",
|
||||
dirName: "caldavcalendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -19,6 +19,5 @@
|
||||
"organizerInputPlaceholder": "https://party.campfire.to/your-team",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?party.campfire.to\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,5 @@
|
||||
"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,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -13,6 +13,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "link-as-an-app",
|
||||
"dependencies": ["google-calendar"],
|
||||
"isOAuth": false
|
||||
"dependencies": ["google-calendar"]
|
||||
}
|
||||
@@ -26,7 +26,6 @@ export const metadata = {
|
||||
},
|
||||
key: { apikey: process.env.DAILY_API_KEY },
|
||||
dirName: "dailyvideo",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -21,6 +21,5 @@
|
||||
"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",
|
||||
"isOAuth": false
|
||||
"__template": "event-type-location-video-static"
|
||||
}
|
||||
@@ -22,6 +22,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static",
|
||||
"dirName": "eightxeight",
|
||||
"isOAuth": false
|
||||
"dirName": "eightxeight"
|
||||
}
|
||||
@@ -22,6 +22,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static",
|
||||
"dirName": "element-call",
|
||||
"isOAuth": false
|
||||
"dirName": "element-call"
|
||||
}
|
||||
@@ -18,7 +18,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "exchange2013calendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -18,7 +18,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "exchange2016calendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -12,6 +12,5 @@
|
||||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"description": "Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -21,6 +21,5 @@
|
||||
"urlRegExp": "^https?:\\/\\/facetime\\.apple\\.com\\/join.+$"
|
||||
}
|
||||
},
|
||||
"isTemplate": false,
|
||||
"isOAuth": false
|
||||
"isTemplate": false
|
||||
}
|
||||
@@ -2,12 +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<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
@@ -19,12 +20,14 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
<TextField
|
||||
dataTestid={app.slug}
|
||||
name="Tracking ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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 (
|
||||
<TextField
|
||||
dataTestid={slug}
|
||||
name="Tracking ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -23,6 +23,5 @@
|
||||
}
|
||||
},
|
||||
"description": "Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative.",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -2,12 +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<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
@@ -19,12 +20,14 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
<TextField
|
||||
dataTestid={app.slug}
|
||||
name="Tracking ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { TextField } from "@calcom/ui";
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
getAppData,
|
||||
setAppData,
|
||||
disabled,
|
||||
}) => {
|
||||
const trackingId = getAppData("trackingId");
|
||||
|
||||
return (
|
||||
<TextField
|
||||
name="Tracking ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -24,6 +24,5 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export const metadata = {
|
||||
extendsFeature: "EventType",
|
||||
email: "help@cal.com",
|
||||
dirName: "giphy",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -1,14 +1,15 @@
|
||||
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<typeof appDataSchema>();
|
||||
const thankYouPage = getAppData("thankYouPage");
|
||||
const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app);
|
||||
|
||||
const { t } = useLocale();
|
||||
@@ -22,13 +23,15 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={showGifSelection}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
{showGifSelection && (
|
||||
<SelectGifInput
|
||||
defaultValue={thankYouPage}
|
||||
disabled={disabled}
|
||||
onChange={(url: string) => {
|
||||
setAppData("thankYouPage", url);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
|
||||
import SelectGifInput from "./SelectGifInput";
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
getAppData,
|
||||
setAppData,
|
||||
disabled,
|
||||
}) => {
|
||||
const thankYouPage = getAppData("thankYouPage");
|
||||
|
||||
return (
|
||||
<SelectGifInput
|
||||
defaultValue={thankYouPage}
|
||||
disabled={disabled}
|
||||
onChange={(url: string) => {
|
||||
setAppData("thankYouPage", url);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -18,7 +18,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "googlecalendar",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -27,7 +27,6 @@ export const metadata = {
|
||||
},
|
||||
dirName: "googlevideo",
|
||||
dependencies: ["google-calendar"],
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -2,12 +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<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
@@ -19,12 +20,14 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
<TextField
|
||||
dataTestid={app.slug}
|
||||
name="Tracking ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { TextField } from "@calcom/ui";
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
getAppData,
|
||||
setAppData,
|
||||
disabled,
|
||||
}) => {
|
||||
const trackingId = getAppData("trackingId");
|
||||
|
||||
return (
|
||||
<TextField
|
||||
name="Tracking ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -21,6 +21,5 @@
|
||||
},
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "booking-pages-tag",
|
||||
"isOAuth": false
|
||||
"__template": "booking-pages-tag"
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export const metadata = {
|
||||
title: "HubSpot CRM",
|
||||
email: "help@cal.com",
|
||||
dirName: "hubspot",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -28,7 +28,6 @@ export const metadata = {
|
||||
key: { apikey: randomString(12) },
|
||||
dirName: "huddle01video",
|
||||
concurrentMeetings: true,
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -8,7 +8,6 @@ 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]`] });
|
||||
|
||||
@@ -86,12 +85,6 @@ 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" })
|
||||
|
||||
@@ -13,6 +13,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "intercom",
|
||||
"isOAuth": true
|
||||
"dirName": "intercom"
|
||||
}
|
||||
@@ -25,7 +25,6 @@ export const metadata = {
|
||||
},
|
||||
dirName: "jitsivideo",
|
||||
concurrentMeetings: true,
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -16,7 +16,6 @@ export const metadata = {
|
||||
url: "https://larksuite.com/",
|
||||
email: "alan@larksuite.com",
|
||||
dirName: "larkcalendar",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -14,6 +14,5 @@
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"imageSrc": "icon.svg",
|
||||
"dirName": "make",
|
||||
"isOAuth": false
|
||||
"dirName": "make"
|
||||
}
|
||||
@@ -2,12 +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<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
@@ -17,12 +18,14 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
switchOnClick={updateEnabled}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
<TextField
|
||||
dataTestid={app.slug}
|
||||
name="Pixel ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { TextField } from "@calcom/ui";
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
getAppData,
|
||||
setAppData,
|
||||
disabled,
|
||||
}) => {
|
||||
const trackingId = getAppData("trackingId");
|
||||
|
||||
return (
|
||||
<TextField
|
||||
name="Pixel ID"
|
||||
value={trackingId}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -21,6 +21,5 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,5 @@
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static",
|
||||
"dirName": "mirotalk",
|
||||
"isOAuth": false
|
||||
"dirName": "mirotalk"
|
||||
}
|
||||
@@ -10,6 +10,5 @@
|
||||
"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,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export const metadata = {
|
||||
dirName: "office365calendar",
|
||||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -104,10 +104,6 @@ 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" })
|
||||
);
|
||||
|
||||
@@ -22,6 +22,5 @@
|
||||
}
|
||||
},
|
||||
"dirName": "office365video",
|
||||
"concurrentMeetings": true,
|
||||
"isOAuth": true
|
||||
"concurrentMeetings": true
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useEffect, 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 EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface";
|
||||
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
app,
|
||||
@@ -23,13 +31,40 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
[pathname, searchParams]
|
||||
);
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
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 [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||
const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||
const { t } = useLocale();
|
||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||
const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||
|
||||
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 (
|
||||
<AppCard
|
||||
returnTo={WEBAPP_URL + asPath}
|
||||
@@ -42,13 +77,81 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
disableSwitch={shouldDisableSwitch}
|
||||
switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}>
|
||||
<>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
{recurringEventDefined ? (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
) : (
|
||||
requirePayment && (
|
||||
<>
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<TextField
|
||||
label="Price"
|
||||
labelSrOnly
|
||||
addOnLeading={currencySymbol}
|
||||
addOnSuffix={currency}
|
||||
step="0.01"
|
||||
min="0.5"
|
||||
type="number"
|
||||
required
|
||||
className="block w-full rounded-sm pl-2 text-sm"
|
||||
placeholder="Price"
|
||||
data-testid="paypal-price-input"
|
||||
onChange={(e) => {
|
||||
setAppData("price", Number(e.target.value) * 100);
|
||||
if (selectedCurrency) {
|
||||
setAppData("currency", selectedCurrency.value);
|
||||
}
|
||||
}}
|
||||
value={price > 0 ? price / 100 : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
variant="default"
|
||||
data-testid="paypal-currency-select"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setCurrencySymbol(currencySymbols[e.value]);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import {
|
||||
currencyOptions,
|
||||
currencySymbols,
|
||||
isAcceptedCurrencyCode,
|
||||
} from "@calcom/app-store/paypal/lib/currencyOptions";
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Select, TextField } from "@calcom/ui";
|
||||
|
||||
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
getAppData,
|
||||
setAppData,
|
||||
eventType,
|
||||
}) => {
|
||||
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 [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||
const { t } = useLocale();
|
||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (requirePayment) {
|
||||
if (!getAppData("currency")) {
|
||||
setAppData("currency", currencyOptions[0].value);
|
||||
}
|
||||
if (!getAppData("paymentOption")) {
|
||||
setAppData("paymentOption", paymentOptions[0].value);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (recurringEventDefined) {
|
||||
return <Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />;
|
||||
}
|
||||
|
||||
if (!requirePayment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<TextField
|
||||
label="Price"
|
||||
labelSrOnly
|
||||
addOnLeading={currencySymbol}
|
||||
addOnSuffix={currency}
|
||||
step="0.01"
|
||||
min="0.5"
|
||||
type="number"
|
||||
required
|
||||
className="block w-full rounded-sm pl-2 text-sm"
|
||||
placeholder="Price"
|
||||
data-testid="paypal-price-input"
|
||||
onChange={(e) => {
|
||||
setAppData("price", Number(e.target.value) * 100);
|
||||
if (selectedCurrency) {
|
||||
setAppData("currency", selectedCurrency.value);
|
||||
}
|
||||
}}
|
||||
value={price > 0 ? price / 100 : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
variant="default"
|
||||
data-testid="paypal-currency-select"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setCurrencySymbol(currencySymbols[e.value]);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -14,6 +14,5 @@
|
||||
"__createdUsingCli": true,
|
||||
"imageSrc": "icon.svg",
|
||||
"__template": "event-type-app-card",
|
||||
"dirName": "paypal",
|
||||
"isOAuth": true
|
||||
"dirName": "paypal"
|
||||
}
|
||||
@@ -20,6 +20,5 @@
|
||||
"organizerInputPlaceholder": "https://www.ping.gg/call/theo",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,5 @@
|
||||
"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,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -2,13 +2,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 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<typeof appDataSchema>();
|
||||
|
||||
const plausibleUrl = getAppData("PLAUSIBLE_URL");
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
@@ -20,13 +21,29 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<TextField
|
||||
dataTestid={`${app.slug}-url`}
|
||||
name="Plausible URL"
|
||||
defaultValue="https://plausible.io/js/script.js"
|
||||
placeholder="https://plausible.io/js/script.js"
|
||||
value={plausibleUrl}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("PLAUSIBLE_URL", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
dataTestid={`${app.slug}-tracking-id`}
|
||||
disabled={disabled}
|
||||
name="Tracked Domain"
|
||||
placeholder="yourdomain.com"
|
||||
value={trackingId}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
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 (
|
||||
<div className="flex flex-col gap-2">
|
||||
<TextField
|
||||
dataTestid={`${slug}-url`}
|
||||
name="Plausible URL"
|
||||
defaultValue="https://plausible.io/js/script.js"
|
||||
placeholder="https://plausible.io/js/script.js"
|
||||
value={plausibleUrl}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("PLAUSIBLE_URL", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
dataTestid={`${slug}-tracking-id`}
|
||||
disabled={disabled}
|
||||
name="Tracked Domain"
|
||||
placeholder="yourdomain.com"
|
||||
value={trackingId}
|
||||
onChange={(e) => {
|
||||
setAppData("trackingId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -23,6 +23,5 @@
|
||||
}
|
||||
},
|
||||
"description": "Simple, privacy-friendly Google Analytics alternative.",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -1,14 +1,39 @@
|
||||
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<typeof appDataSchema>();
|
||||
const [additionalParameters, setAdditionalParameters] = useState("");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
const { disabled, getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
|
||||
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 (
|
||||
<Tooltip content={eventTypeURL}>
|
||||
<a download href={QR_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
className="hover:bg-muted border-default border hover:shadow-sm"
|
||||
style={{ padding: size / 16, borderRadius: size / 20 }}
|
||||
width={size}
|
||||
src={QR_URL}
|
||||
alt={eventTypeURL}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
@@ -18,13 +43,24 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
<div className="flex w-full flex-col gap-5 text-sm">
|
||||
<div className="flex w-full">
|
||||
<TextField
|
||||
name="hello"
|
||||
disabled={disabled}
|
||||
value={additionalParameters}
|
||||
onChange={(e) => setAdditionalParameters(e.target.value)}
|
||||
label={t("additional_url_parameters")}
|
||||
containerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-60 flex items-baseline gap-2">
|
||||
<QRCode size={256} data={eventTypeURL} />
|
||||
<QRCode size={128} data={eventTypeURL} />
|
||||
<QRCode size={64} data={eventTypeURL} />
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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 (
|
||||
<Tooltip content={eventTypeURL}>
|
||||
<a download href={QR_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
className={classNames(
|
||||
"hover:bg-muted border-default border hover:shadow-sm",
|
||||
size >= 256 && "min-h-32"
|
||||
)}
|
||||
style={{ padding: size / 16, borderRadius: size / 20 }}
|
||||
width={size}
|
||||
src={QR_URL}
|
||||
alt={eventTypeURL}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 text-sm">
|
||||
<div className="flex w-full">
|
||||
<TextField
|
||||
name="hello"
|
||||
disabled={disabled}
|
||||
value={additionalParameters}
|
||||
onChange={(e) => setAdditionalParameters(e.target.value)}
|
||||
label={t("additional_url_parameters")}
|
||||
containerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-60 flex items-baseline gap-2">
|
||||
<QRCode size={256} data={eventTypeURL} />
|
||||
<QRCode size={128} data={eventTypeURL} />
|
||||
<QRCode size={64} data={eventTypeURL} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -11,6 +11,5 @@
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "Easily generate a QR code for your links to print, share, or embed.",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -10,6 +10,5 @@
|
||||
"publisher": "Eric Luce",
|
||||
"email": "info@restlessmindstech.com",
|
||||
"description": "Quickly share your Cal.com meeting links with Raycast",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -18,6 +18,5 @@
|
||||
"type": "integrations:riverside_video",
|
||||
"linkType": "static"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,5 @@
|
||||
"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,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -42,11 +42,6 @@ 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" })
|
||||
);
|
||||
|
||||
@@ -10,6 +10,5 @@
|
||||
"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,
|
||||
"isOAuth": true
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -10,6 +10,5 @@
|
||||
"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,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -19,6 +19,5 @@
|
||||
"organizerInputPlaceholder": "https://signal.me/#p/+11234567890",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?signal.me\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,5 @@
|
||||
"organizerInputPlaceholder": "https://sirius.video/sebastian",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?sirius.video\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export const metadata = {
|
||||
extendsFeature: "EventType",
|
||||
email: "help@cal.com",
|
||||
dirName: "stripepayment",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -53,10 +53,6 @@ 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" }));
|
||||
}
|
||||
@@ -1,48 +1,157 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
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 { 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 useIsAppEnabled from "../../_utils/useIsAppEnabled";
|
||||
import { paymentOptions } from "../lib/constants";
|
||||
import {
|
||||
convertToSmallestCurrencyUnit,
|
||||
convertFromSmallestToPresentableCurrencyUnit,
|
||||
} from "../lib/currencyConversions";
|
||||
import { currencyOptions } from "../lib/currencyOptions";
|
||||
import type { appDataSchema } from "../zod";
|
||||
import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
app,
|
||||
eventType,
|
||||
eventTypeFormMetadata,
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const pathname = usePathname();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||
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 otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||
|
||||
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 (
|
||||
<AppCard
|
||||
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
|
||||
app={app}
|
||||
switchChecked={enabled}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
switchChecked={requirePayment}
|
||||
switchOnClick={(enabled) => {
|
||||
setRequirePayment(enabled);
|
||||
}}
|
||||
teamId={eventType.team?.id || undefined}
|
||||
disableSwitch={shouldDisableSwitch}
|
||||
switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
<>
|
||||
{recurringEventDefined && (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
)}
|
||||
{!recurringEventDefined && requirePayment && (
|
||||
<>
|
||||
<div className="mt-4 block items-center justify-start sm:flex sm:space-x-2">
|
||||
<TextField
|
||||
data-testid="stripe-price-input"
|
||||
label={t("price")}
|
||||
className="h-[38px]"
|
||||
addOnLeading={
|
||||
<>{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}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
data-testid="stripe-currency-select"
|
||||
variant="default"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Select, TextField } from "@calcom/ui";
|
||||
|
||||
import { paymentOptions } from "../lib/constants";
|
||||
import {
|
||||
convertToSmallestCurrencyUnit,
|
||||
convertFromSmallestToPresentableCurrencyUnit,
|
||||
} from "../lib/currencyConversions";
|
||||
import { currencyOptions } from "../lib/currencyOptions";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({
|
||||
getAppData,
|
||||
setAppData,
|
||||
disabled,
|
||||
eventType,
|
||||
}) => {
|
||||
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 = getAppData("enabled");
|
||||
|
||||
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 (
|
||||
<>
|
||||
{recurringEventDefined && (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
)}
|
||||
{!recurringEventDefined && requirePayment && (
|
||||
<>
|
||||
<div className="mt-4 block items-center justify-start sm:flex sm:space-x-2">
|
||||
<TextField
|
||||
data-testid="stripe-price-input"
|
||||
label={t("price")}
|
||||
className="h-[38px]"
|
||||
addOnLeading={
|
||||
<>{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}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
data-testid="stripe-currency-select"
|
||||
variant="default"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
||||
@@ -18,6 +18,5 @@
|
||||
"type": "integrations:sylaps_video",
|
||||
"label": "Sylaps"
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ export const metadata = {
|
||||
},
|
||||
},
|
||||
dirName: "tandemvideo",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
@@ -10,6 +10,5 @@
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "Adds a link to copy Typeform Redirect URL",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
Vendored
-20
@@ -10,7 +10,6 @@ export type IntegrationOAuthCallbackState = {
|
||||
onErrorReturnTo: string;
|
||||
fromApp: boolean;
|
||||
installGoogleVideo?: boolean;
|
||||
appOnboardingRedirectUrl?: string;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
@@ -57,23 +56,4 @@ export type EventTypeAppCardComponentProps = {
|
||||
LockedIcon?: JSX.Element | false;
|
||||
eventTypeFormMetadata?: z.infer<typeof EventTypeFormMetadataSchema>;
|
||||
};
|
||||
|
||||
export type EventTypeAppSettingsComponentProps = {
|
||||
// Limit what data should be accessible to apps\
|
||||
eventType: Pick<
|
||||
z.infer<typeof EventTypeModel>,
|
||||
"id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot" | "team"
|
||||
> & {
|
||||
URL: string;
|
||||
};
|
||||
getAppData: GetAppData;
|
||||
setAppData: SetAppData;
|
||||
disabled?: boolean;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
||||
|
||||
export type EventTypeAppSettingsComponent = React.FC<EventTypeAppSettingsComponentProps>;
|
||||
|
||||
export type EventTypeModel = z.infer<typeof EventTypeModel>;
|
||||
@@ -11,6 +11,5 @@
|
||||
"email": "support@cal.com",
|
||||
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"],
|
||||
"isOAuth": false
|
||||
"dependencies": ["google-calendar"]
|
||||
}
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user